# -*- 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//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"""

Billets d'absence

""" ) 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/") @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", ), f"

Billets d'absence en attente de traitement ({table.get_nb_rows()})

", ] 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( """

Supprimer ce billet ?

""" + 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 {billet_id} 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"""

Traitement du billet {billet.id} : {etud.nomprenom}

""", ] 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( """

L'étudiant pense pouvoir justifier cette absence.
Vérifiez le justificatif avant d'enregistrer.

""" ) F = f"""

Supprimer ce billet (utiliser en cas d'erreur, par ex. billet en double)

Liste de tous les billets en attente

""" return "\n".join(H) + "
" + 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('
') 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"""

Autre billets en attente

Billets déclarés par {etud.nomprenom}

""" ) 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()