1
0
forked from ScoDoc/ScoDoc
ScoDoc/app/views/absences.py

438 lines
14 KiB
Python

# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
#
# 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 2 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, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""
Module absences: remplacé par assiduité en août 2023,
reste ici seulement la gestion des "billets"
"""
import dateutil
import dateutil.parser
import flask
from flask import abort, flash, g, render_template, request, url_for
from flask_login import current_user
from app import db, log
from app import api
from app.decorators import (
scodoc,
scodoc7func,
permission_required,
permission_required_compat_scodoc7,
)
from app.models.absences import BilletAbsence
from app.models.etudiants import Identite
from app.views import absences_bp as bp
# ---------------
from app.scodoc import sco_utils as scu
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_abs_billets
from app.scodoc import sco_preferences
# --------------------------------------------------------------------
#
# ABSENCES (/ScoDoc/<dept>/Scolarite/Absences/...)
#
# --------------------------------------------------------------------
@bp.route("/")
@bp.route("/index_html")
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def index_html():
"""Gestionnaire absences, page principale"""
H = []
if current_user.has_permission(
Permission.AbsChange
) and sco_preferences.get_preference("handle_billets_abs"):
H.append(
f"""
<h2 style="margin-top: 30px;">Billets d'absence</h2>
<ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Traitement des billets d'absence en attente</a>
</li></ul>
"""
)
return render_template(
"sco_page_dept.j2", title="Billets d'absences", content="\n".join(H)
)
# ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail)
@bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat
@scodoc
@permission_required_compat_scodoc7(Permission.AbsAddBillet)
@scodoc7func
def AddBilletAbsence(
begin,
end,
description,
etudid=None,
code_nip=None,
code_ine=None,
justified=True,
fmt="json",
xml_reply=True, # deprecated
):
"""Mémorise un "billet"
begin et end sont au format ISO (eg "1999-01-08 04:05:06")
"""
log("Warning: calling deprecated AddBilletAbsence")
begin = str(begin)
end = str(end)
code_nip = str(code_nip) if code_nip else None
etud = api.tools.get_etud(etudid=etudid, nip=code_nip, ine=code_ine)
# check dates
begin_date = dateutil.parser.isoparse(begin) # may raises ValueError
end_date = dateutil.parser.isoparse(end)
if begin_date > end_date:
raise ValueError("invalid dates")
#
justified = bool(justified)
xml_reply = bool(xml_reply)
if xml_reply: # backward compat
fmt = "xml"
#
billet = BilletAbsence(
etudid=etud.id,
abs_begin=begin,
abs_end=end,
description=description,
etat=False,
justified=justified,
)
db.session.add(billet)
db.session.commit()
# Renvoie le nouveau billet au format demandé
table = sco_abs_billets.table_billets([billet], etud=etud)
log(f"AddBilletAbsence: new billet_id={billet.id}")
return table.make_page(fmt=fmt)
@bp.route("/add_billets_absence_form", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.AbsAddBillet)
@scodoc7func
def add_billets_absence_form(etudid):
"""Formulaire ajout billet (pour tests seulement, le vrai
formulaire accessible aux etudiants étant sur le portail étudiant).
"""
_ = Identite.get_etud(etudid) # check
H = ["""<h2>Formulaire ajout billet (pour tests)</h2>"""]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("etudid", {"input_type": "hidden"}),
("begin", {"input_type": "datedmy"}),
("end", {"input_type": "datedmy"}),
(
"justified",
{"input_type": "boolcheckbox", "default": 0, "title": "Justifiée"},
),
("description", {"input_type": "textarea"}),
),
)
if tf[0] == 0:
return render_template(
"sco_page_dept.j2",
title="""Billet d'absence de {etud["nomprenom"]}""",
content="\n".join(H) + tf[1],
)
elif tf[0] == -1:
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else:
e = tf[2]["begin"].split("/")
begin = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
e = tf[2]["end"].split("/")
end = e[2] + "-" + e[1] + "-" + e[0] + " 00:00:00"
log(
AddBilletAbsence(
begin,
end,
tf[2]["description"],
etudid=etudid,
xml_reply=True,
justified=tf[2]["justified"],
)
)
return flask.redirect("billets_etud?etudid=" + str(etudid))
@bp.route("/billets_etud/<int:etudid>")
@scodoc
@permission_required(Permission.ScoView)
def billets_etud(etudid=False, fmt=False):
"""Liste billets pour un étudiant"""
fmt = fmt or request.args.get("fmt", "html")
if not fmt in {"html", "json", "xml", "xls", "xlsx"}:
return ScoValueError("Format invalide")
table = sco_abs_billets.table_billets_etud(etudid)
if table:
return table.make_page(fmt=fmt)
return ""
# DEEPRECATED: pour compat anciens clients PHP
@bp.route("/XMLgetBilletsEtud", methods=["GET", "POST"])
@scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func
def XMLgetBilletsEtud(etudid=False, code_nip=False):
"""Liste billets pour un etudiant"""
log("Warning: called deprecated XMLgetBilletsEtud")
if etudid is False:
etud = Identite.query.filter_by(
code_nip=str(code_nip), dept_id=g.scodoc_dept_id
).first_or_404()
etudid = etud.id
table = sco_abs_billets.table_billets_etud(etudid)
if table:
return table.make_page(fmt="xml")
return ""
@bp.route("/list_billets", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def list_billets():
"""Page liste des billets non traités pour tous les étudiants du département
et formulaire recherche d'un billet.
"""
table = sco_abs_billets.table_billets_etud(etat=False)
table_html = table.html()
H = [
f"<h2>Billets d'absence en attente de traitement ({table.get_nb_rows()})</h2>",
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(("billet_id", {"input_type": "text", "title": "Numéro du billet :"}),),
submitbutton=False,
)
if tf[0] == 0:
return render_template(
"sco_page.j2",
title="Billet d'absence non traités",
content="\n".join(H) + tf[1] + table_html,
)
return flask.redirect(
url_for(
"absences.process_billet_absence_form",
billet_id=tf[2]["billet_id"],
scodoc_dept=g.scodoc_dept,
)
)
@bp.route("/delete_billets_absence", methods=["POST", "GET"])
@scodoc
@permission_required(Permission.AbsChange)
@scodoc7func
def delete_billets_absence(billet_id, dialog_confirmed=False):
"""Supprime un billet."""
billet: BilletAbsence = (
BilletAbsence.query.filter_by(id=billet_id)
.join(Identite)
.filter_by(dept_id=g.scodoc_dept_id)
.first_or_404()
)
if not dialog_confirmed:
tab = sco_abs_billets.table_billets([billet])
return scu.confirm_dialog(
"""<h2>Supprimer ce billet ?</h2>""" + tab.html(),
dest_url="",
cancel_url="list_billets",
parameters={"billet_id": billet_id},
)
db.session.delete(billet)
db.session.commit()
flash("Billet supprimé")
return flask.redirect(url_for("absences.list_billets", scodoc_dept=g.scodoc_dept))
def _ProcessBilletAbsence(
billet: BilletAbsence, estjust: bool, description: str
) -> int:
"""Traite un billet: ajoute absence(s) et éventuellement justificatifs,
et change l'état du billet à True.
return: nombre de demi-journées d'absence ajoutées, -1 si billet déjà traité.
NB: actuellement, les heures ne sont utilisées que pour déterminer
si matin et/ou après-midi.
TODO: Vérifier l'intégration avec le module Assiduité
"""
if billet.etat:
log(f"billet deja traite: {billet} !")
return -1
n = 0 # nombre de demi-journées d'absence ajoutées
# 1-- Ajout des absences (et justifs)
datedebut = billet.abs_begin
datefin = billet.abs_end
log(f"Gestion du billet n°{billet.id}")
n = scass.create_absence_billet(
date_debut=datedebut,
date_fin=datefin,
etudid=billet.etudid,
description=description,
est_just=estjust,
)
# 2- Change état du billet
billet.etat = True
db.session.add(billet)
db.session.commit()
return n
@bp.route("/process_billet_absence_form", methods=["POST", "GET"])
@scodoc
@permission_required(Permission.AbsChange)
@scodoc7func
def process_billet_absence_form(billet_id: int):
"""Formulaire traitement d'un billet"""
if not isinstance(billet_id, int):
abort(404, "billet_id invalide")
return # safety guard
billet: BilletAbsence = (
BilletAbsence.query.filter_by(id=billet_id)
.join(Identite)
.filter_by(dept_id=g.scodoc_dept_id)
.first()
)
if billet is None:
raise ScoValueError(
f"Aucun billet avec le numéro <tt>{billet_id}</tt> dans ce département.",
dest_url=url_for("absences.list_billets", scodoc_dept=g.scodoc_dept),
)
etud = billet.etudiant
H = [
f"""<h2>Traitement du billet {billet.id} : <a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}">{etud.nomprenom}</a></h2>
""",
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("billet_id", {"input_type": "hidden"}),
(
"etudid",
{"input_type": "hidden"},
),
(
"estjust",
{"input_type": "boolcheckbox", "title": "Absences justifiées"},
),
("description", {"input_type": "text", "size": 42, "title": "Raison"}),
),
initvalues={
"description": billet.description or "",
"estjust": billet.justified,
"etudid": etud.id,
},
submitlabel="Enregistrer ces absences",
)
if tf[0] == 0:
tab = sco_abs_billets.table_billets([billet], etud=etud)
H.append(tab.html())
if billet.justified:
H.append(
"""<p>L'étudiant pense pouvoir justifier cette absence.<br>
<em>Vérifiez le justificatif avant d'enregistrer.</em></p>"""
)
F = f"""<p><a class="stdlink" href="{
url_for("absences.delete_billets_absence",
scodoc_dept=g.scodoc_dept, billet_id=billet_id)
}">Supprimer ce billet</a>
(utiliser en cas d'erreur, par ex. billet en double)
</p>
<p><a class="stdlink" href="{
url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Liste de tous les billets en attente</a>
</p>
"""
return render_template(
"sco_page.j2",
title=f"Traitement billet d'absence de {etud.nomprenom}",
content="\n".join(H) + "<br>" + tf[1] + F,
)
elif tf[0] == -1:
return flask.redirect(url_for("scolar.index_html", scodoc_dept=g.scodoc_dept))
else:
n = _ProcessBilletAbsence(billet, tf[2]["estjust"], tf[2]["description"])
if tf[2]["estjust"]:
j = "justifiées"
else:
j = "non justifiées"
H.append('<div class="head_message">')
if n > 0:
H.append(f"{n} absences (1/2 journées) {j} ajoutées")
elif n == 0:
H.append("Aucun jour d'absence dans les dates indiquées !")
elif n < 0:
H.append("Ce billet avait déjà été traité !")
H.append(
f"""</div><p><a class="stdlink" href="{
url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Autre billets en attente</a>
</p>
<h4>Billets déclarés par {etud.nomprenom}</h4>
"""
)
billets = (
BilletAbsence.query.filter_by(etudid=etud.id)
.join(Identite)
.filter_by(dept_id=g.scodoc_dept_id)
)
tab = sco_abs_billets.table_billets(billets, etud=etud)
H.append(tab.html())
return render_template(
"sco_page.j2",
title=f"Traitement billet d'absence de {etud.nomprenom}",
content="\n".join(H),
)