forked from ScoDoc/ScoDoc
438 lines
14 KiB
Python
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),
|
|
)
|