ScoDoc/app/views/absences.py

440 lines
14 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# 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
2021-08-01 10:16:16 +02:00
import flask
from flask import g, request
2022-09-11 09:35:47 +02:00
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 (
2021-08-13 00:34:58 +02:00
scodoc,
scodoc7func,
permission_required,
permission_required_compat_scodoc7,
)
from app.models.absences import BilletAbsence
2022-08-18 15:43:14 +02:00
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
2021-06-12 22:43:22 +02:00
# --------------------------------------------------------------------
#
# ABSENCES (/ScoDoc/<dept>/Scolarite/Absences/...)
#
# --------------------------------------------------------------------
2021-07-09 10:26:31 +02:00
@bp.route("/")
@bp.route("/index_html")
2021-08-13 00:34:58 +02:00
@scodoc
2021-07-09 10:26:31 +02:00
@permission_required(Permission.ScoView)
@scodoc7func
2021-09-24 12:10:53 +02:00
def index_html():
2021-07-09 10:26:31 +02:00
"""Gestionnaire absences, page principale"""
H = [
html_sco_header.sco_header(
page_title="Billets d'absences",
2021-07-09 10:26:31 +02:00
),
]
if current_user.has_permission(
Permission.AbsChange
) and sco_preferences.get_preference("handle_billets_abs"):
2021-07-09 10:26:31 +02:00
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())
2021-07-09 10:26:31 +02:00
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,
2022-08-18 15:43:14 +02:00
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")
2021-10-07 23:21:30 +02:00
begin = str(begin)
end = str(end)
code_nip = str(code_nip) if code_nip else None
2022-08-18 15:43:14 +02:00
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"])
2021-08-13 00:34:58 +02:00
@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).
"""
2021-08-22 13:24:36 +02:00
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
H = [
2021-06-13 18:29:53 +02:00
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>")
2021-08-13 00:34:58 +02:00
@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
2022-08-18 15:43:14 +02:00
def XMLgetBilletsEtud(etudid=False, code_nip=False):
"""Liste billets pour un etudiant"""
log("Warning: called deprecated XMLgetBilletsEtud")
2022-08-18 15:43:14 +02:00
if etudid is False:
2022-09-11 09:35:47 +02:00
etud = Identite.query.filter_by(
code_nip=str(code_nip), dept_id=g.scodoc_dept_id
).first_or_404()
2022-08-18 15:43:14 +02:00
etudid = etud.id
table = sco_abs_billets.table_billets_etud(etudid)
if table:
return table.make_page(fmt="xml")
return ""
2023-03-19 10:26:03 +01:00
@bp.route("/list_billets", methods=["GET", "POST"])
2021-08-13 00:34:58 +02:00
@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 = [
2022-09-22 14:58:46 +02:00
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(),
2022-09-11 09:35:47 +02:00
(("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:
2021-07-31 18:01:10 +02:00
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"])
2021-08-13 00:34:58 +02:00
@scodoc
@permission_required(Permission.AbsChange)
@scodoc7func
def delete_billets_absence(billet_id, dialog_confirmed=False):
"""Supprime un billet."""
2022-09-11 09:35:47 +02:00
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:
2022-09-11 09:35:47 +02:00
tab = sco_abs_billets.table_billets([billet])
2021-06-21 12:13:25 +02:00
return scu.confirm_dialog(
"""<h2>Supprimer ce billet ?</h2>""" + tab.html(),
dest_url="",
cancel_url="list_billets",
parameters={"billet_id": billet_id},
)
2022-09-11 09:35:47 +02:00
db.session.delete(billet)
db.session.commit()
2022-09-11 09:35:47 +02:00
flash("Billet supprimé")
return flask.redirect(url_for("absences.list_billets", scodoc_dept=g.scodoc_dept))
2022-09-11 09:35:47 +02:00
def _ProcessBilletAbsence(
billet: BilletAbsence, estjust: bool, description: str
) -> int:
"""Traite un billet: ajoute absence(s) et éventuellement justificatifs,
2022-09-11 09:35:47 +02:00
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.
2024-01-18 17:05:43 +01:00
TODO: Vérifier l'intégration avec le module Assiduité
"""
2022-09-11 09:35:47 +02:00
if billet.etat:
log(f"billet deja traite: {billet} !")
return -1
n = 0 # nombre de demi-journées d'absence ajoutées
2022-09-11 09:35:47 +02:00
# 1-- Ajout des absences (et justifs)
datedebut = billet.abs_begin
datefin = billet.abs_end
log(f"Gestion du billet n°{billet.id}")
2024-01-18 17:05:43 +01:00
n = scass.create_absence_billet(
date_debut=datedebut,
date_fin=datefin,
etudid=billet.etudid,
description=description,
est_just=estjust,
)
2022-09-11 09:35:47 +02:00
# 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"])
2021-08-13 00:34:58 +02:00
@scodoc
@permission_required(Permission.AbsChange)
@scodoc7func
2022-11-13 18:36:07 +01:00
def process_billet_absence_form(billet_id: int):
"""Formulaire traitement d'un billet"""
2022-11-13 18:36:07 +01:00
if not isinstance(billet_id, int):
raise abort(404, "billet_id invalide")
2022-09-11 09:35:47 +02:00
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),
)
2022-09-11 09:35:47 +02:00
etud = billet.etudiant
H = [
2021-06-13 18:29:53 +02:00
html_sco_header.sco_header(
2022-09-11 09:35:47 +02:00
page_title=f"Traitement billet d'absence de {etud.nomprenom}",
),
2022-09-11 09:35:47 +02:00
f"""<h2>Traitement du billet {billet.id} : <a class="discretelink" href="{
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
2022-09-11 09:35:47 +02:00
}">{etud.nomprenom}</a></h2>
""",
]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("billet_id", {"input_type": "hidden"}),
(
"etudid",
{"input_type": "hidden"},
2022-09-11 09:35:47 +02:00
),
(
"estjust",
{"input_type": "boolcheckbox", "title": "Absences justifiées"},
),
("description", {"input_type": "text", "size": 42, "title": "Raison"}),
),
initvalues={
2022-09-11 09:35:47 +02:00
"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())
2022-09-11 09:35:47 +02:00
if billet.justified:
H.append(
"""<p>L'étudiant pense pouvoir justifier cette absence.<br>
2022-09-11 09:35:47 +02:00
<em>Vérifiez le justificatif avant d'enregistrer.</em></p>"""
)
2022-09-11 09:35:47 +02:00
F = f"""<p><a class="stdlink" href="{
2023-12-31 23:04:06 +01:00
url_for("absences.delete_billets_absence",
2022-09-11 09:35:47 +02:00
scodoc_dept=g.scodoc_dept, billet_id=billet_id)
2023-12-31 23:04:06 +01:00
}">Supprimer ce billet</a>
2022-09-11 09:35:47 +02:00
(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:
2021-09-24 01:07:00 +02:00
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(
2022-09-11 09:35:47 +02:00
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()