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

Billets d'absence

""" ) 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 = ["""

Formulaire ajout billet (pour tests)

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

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

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): 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 {billet_id} dans ce département.", dest_url=url_for("absences.list_billets", scodoc_dept=g.scodoc_dept), ) etud = billet.etudiant H = [ 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 render_template( "sco_page.j2", title=f"Traitement billet d'absence de {etud.nomprenom}", content="\n".join(H) + "
" + 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('
') 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"""

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 render_template( "sco_page.j2", title=f"Traitement billet d'absence de {etud.nomprenom}", content="\n".join(H), )