ScoDocMM/app/views/absences.py

440 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 g, request
from flask import abort, flash, 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 html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_assiduites as scass
from app.scodoc import sco_abs_billets
from app.scodoc import sco_etud
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 = [
html_sco_header.sco_header(
page_title="Billets d'absences",
),
]
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>
"""
)
H.append(html_sco_header.sco_footer())
return "\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).
"""
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
H = [
html_sco_header.sco_header(
page_title="Billet d'absence de %s" % etud["nomprenom"]
)
]
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 "\n".join(H) + tf[1] + html_sco_header.sco_footer()
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)
T = table.html()
H = [
html_sco_header.sco_header(
page_title="Billet d'absence non traités",
javascripts=["js/etud_info.js"],
init_qtip=True,
),
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 "\n".join(H) + tf[1] + T + html_sco_header.sco_footer()
else:
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):
raise abort(404, "billet_id invalide")
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 = [
html_sco_header.sco_header(
page_title=f"Traitement billet d'absence de {etud.nomprenom}",
),
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 "\n".join(H) + "<br>" + tf[1] + F + html_sco_header.sco_footer()
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("%d absences (1/2 journées) %s ajoutées" % (n, j))
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 "\n".join(H) + html_sco_header.sco_footer()