# -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 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: issu de ScoDoc7 / ZAbsences.py Emmanuel Viennet, 2021 Gestion des absences (v4) Code dérivé de la partie la plus ancienne de ScoDoc, et à revoir. L'API de plus bas niveau est en gros: AnnuleAbsencesDatesNoJust( dates) count_abs(etudid, debut, fin, matin=None, moduleimpl_id=None) count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) list_abs_just(etudid, datedebut) [pas de fin ?] list_abs_non_just(etudid, datedebut) [pas de fin ?] list_abs_justifs(etudid, datedebut, datefin=None, only_no_abs=True) list_abs_jour(date, am=True, pm=True, is_abs=None, is_just=None) list_abs_non_just_jour(date, am=True, pm=True) """ import calendar import datetime import dateutil import dateutil.parser import re import time import urllib from xml.etree import ElementTree 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.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.decorators import ( scodoc, scodoc7func, permission_required, permission_required_compat_scodoc7, ) from app.models import FormSemestre, GroupDescr, Partition 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 import notesdb as ndb from app.scodoc.scolog import logdb from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ScoValueError, APIInvalidParams from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_abs from app.scodoc import sco_abs_billets from app.scodoc import sco_abs_views from app.scodoc import sco_etud from app.scodoc import sco_find_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_groups_view from app.scodoc import sco_moduleimpl from app.scodoc import sco_preferences from app.scodoc import sco_xml CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS def sco_publish(route, function, permission, methods=["GET"]): """Declare a route for a python function, protected by permission and called following ScoDoc 7 Zope standards. """ return bp.route(route, methods=methods)( scodoc(permission_required(permission)(scodoc7func(function))) ) # -------------------------------------------------------------------- # # 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="Saisie des absences", cssstyles=["css/calabs.css"], javascripts=["js/calabs.js"], ), """

Traitement des absences

Pour saisir des absences ou consulter les états, il est recommandé par passer par le semestre concerné (saisie par jours nommés ou par semaines).

""", ] H.append( """

Pour signaler, annuler ou justifier une absence pour un seul étudiant, choisissez d'abord concerné:

""" ) H.append(sco_find_etud.form_search_etud()) if current_user.has_permission( Permission.ScoAbsChange ) 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) @bp.route("/choix_semaine") @scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func def choix_semaine(group_id): """Page choix semaine sur calendrier pour saisie absences d'un groupe""" group = ( GroupDescr.query.filter_by(id=group_id) .join(Partition) .join(FormSemestre) .filter_by(dept_id=g.scodoc_dept_id) .first_or_404() ) H = [ html_sco_header.sco_header( page_title="Saisie des absences", cssstyles=["css/calabs.css"], javascripts=["js/calabs.js"], ), f"""

Saisie des Absences

Saisie par semaine - Groupe: {group.get_nom_with_part()}

""", cal_select_week(), """

Sélectionner le groupe d'étudiants, puis cliquez sur une semaine pour saisir les absences de toute cette semaine.

""", html_sco_header.sco_footer(), ] return "\n".join(H) def cal_select_week(year=None): "display calendar allowing week selection" if not year: year = scu.annee_scolaire() sems = sco_formsemestre.do_formsemestre_list() if not sems: js = "" else: js = 'onmouseover="highlightweek(this);" onmouseout="deselectweeks();" onclick="wclick(this);"' C = sco_abs.YearTable(int(year), dayattributes=js) return C sco_publish("/EtatAbsences", sco_abs_views.EtatAbsences, Permission.ScoView) sco_publish("/CalAbs", sco_abs_views.CalAbs, Permission.ScoView) sco_publish( "/SignaleAbsenceEtud", sco_abs_views.SignaleAbsenceEtud, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/doSignaleAbsence", sco_abs_views.doSignaleAbsence, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/JustifAbsenceEtud", sco_abs_views.JustifAbsenceEtud, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/doJustifAbsence", sco_abs_views.doJustifAbsence, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/AnnuleAbsenceEtud", sco_abs_views.AnnuleAbsenceEtud, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/doAnnuleAbsence", sco_abs_views.doAnnuleAbsence, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/doAnnuleJustif", sco_abs_views.doAnnuleJustif, Permission.ScoAbsChange, methods=["GET", "POST"], ) sco_publish( "/AnnuleAbsencesDatesNoJust", sco_abs_views.AnnuleAbsencesDatesNoJust, Permission.ScoAbsChange, methods=["GET", "POST"], ) # Antédiluvienne fonction: #deprecated @bp.route("/ListeAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def ListeAbsEtud( etudid=None, code_nip=None, with_evals=True, format="html", absjust_only=0, sco_year=None, ): return sco_abs_views.ListeAbsEtud( etudid=etudid, code_nip=str(code_nip), with_evals=with_evals, format=format, absjust_only=absjust_only, sco_year=sco_year, ) # -------------------------------------------------------------------- # # SQL METHODS (xxx #sco8 not views => à déplacer) # # -------------------------------------------------------------------- # API backward compatibility sco_publish("/CountAbs", sco_abs.count_abs, Permission.ScoView) sco_publish("/CountAbsJust", sco_abs.count_abs_just, Permission.ScoView) # TODO nouvel appel rendnat les deux valeurs et utilisant le cache @bp.route("/doSignaleAbsenceGrSemestre", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func def doSignaleAbsenceGrSemestre( moduleimpl_id=None, abslist=[], dates="", etudids="", destination=None, ): """Enregistre absences aux dates indiquees (abslist et dates). dates est une liste de dates ISO (séparées par des ','). Efface les absences aux dates indiquées par dates, ou bien ajoute celles de abslist. """ moduleimpl_id = moduleimpl_id or None if etudids: etudids = [int(x) for x in str(etudids).split(",")] else: etudids = [] if dates: dates = dates.split(",") else: dates = [] # 1- Efface les absences if dates: for etudid in etudids: sco_abs_views.AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id) return "Absences effacées" # 2- Ajoute les absences if abslist: sco_abs.add_abslist(abslist, moduleimpl_id) return "Absences ajoutées" return ("", 204) # ------------ HTML Interfaces @bp.route("/SignaleAbsenceGrHebdo", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func def SignaleAbsenceGrHebdo( datelundi, group_ids=[], destination="", moduleimpl_id=None, formsemestre_id=None ): "Saisie hebdomadaire des absences" if not moduleimpl_id: moduleimpl_id = None groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, moduleimpl_id=moduleimpl_id, formsemestre_id=formsemestre_id ) if not groups_infos.members: return ( html_sco_header.sco_header(page_title="Saisie des absences") + "

Aucun étudiant !

" + html_sco_header.sco_footer() ) base_url = "SignaleAbsenceGrHebdo?datelundi=%s&%s&destination=%s" % ( datelundi, groups_infos.groups_query_args, urllib.parse.quote(destination), ) formsemestre_id = groups_infos.formsemestre_id formsemestre = FormSemestre.query.get_or_404(formsemestre_id) if formsemestre.dept_id != g.scodoc_dept_id: abort(404, "groupes inexistants dans ce département") require_module = sco_preferences.get_preference( "abs_require_module", formsemestre_id ) etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] # Restreint aux inscrits au module sélectionné if moduleimpl_id: mod_inscrits = set( [ x["etudid"] for x in sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=moduleimpl_id ) ] ) etuds_inscrits_module = [e for e in etuds if e["etudid"] in mod_inscrits] if etuds_inscrits_module: etuds = etuds_inscrits_module else: # Si aucun etudiant n'est inscrit au module choisi... moduleimpl_id = None nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) sem = formsemestre.to_dict() # calcule dates jours de cette semaine # liste de dates iso "yyyy-mm-dd" datessem = [ndb.DateDMYtoISO(datelundi)] for _ in sco_abs.day_names()[1:]: datessem.append(sco_abs.next_iso_day(datessem[-1])) # if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" else: if len(groups_infos.group_ids) > 1: p = "des groupes" else: p = "du groupe" gr_tit = p + ' ' + groups_infos.groups_titles + "" H = [ html_sco_header.sco_header( page_title="Saisie hebdomadaire des absences", init_qtip=True, javascripts=html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ "js/etud_info.js", "js/abs_ajax.js", "js/groups_view.js", ], cssstyles=CSSSTYLES, no_side_bar=1, ), """

Saisie des absences %s %s, semaine du lundi %s

Groupes: %s
""" % ( gr_tit, sem["titre_num"], datelundi, groups_infos.formsemestre_id, datelundi, destination, moduleimpl_id or "", sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True), ), ] # modimpls_list = [] ues = nt.get_ues_stat_dict() for ue in ues: modimpls_list += nt.get_modimpls_dict(ue_id=ue["ue_id"]) menu_module = "" for modimpl in modimpls_list: if modimpl["moduleimpl_id"] == moduleimpl_id: sel = "selected" else: sel = "" menu_module += ( """\n""" % { "modimpl_id": modimpl["moduleimpl_id"], "modname": (modimpl["module"]["code"] or "") + " " + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or ""), "sel": sel, } ) if moduleimpl_id: sel = "" else: sel = "selected" # aucun module specifie H.append( """Module concerné:
""" % {"menu_module": menu_module, "url": base_url, "sel": sel} ) H += _gen_form_saisie_groupe( etuds, datessem, destination, moduleimpl_id, require_module ) H.append(html_sco_header.sco_footer()) return "\n".join(H) @bp.route("/SignaleAbsenceGrSemestre", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func def SignaleAbsenceGrSemestre( datedebut, datefin, destination="", group_ids=(), # list of groups to display nbweeks=4, # ne montre que les nbweeks dernieres semaines moduleimpl_id=None, ): """Saisie des absences sur une journée sur un semestre (ou intervalle de dates) entier""" groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) if not groups_infos.members: return ( html_sco_header.sco_header(page_title="Saisie des absences") + "

Aucun étudiant !

" + html_sco_header.sco_footer() ) formsemestre_id = groups_infos.formsemestre_id formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if formsemestre.dept_id != g.scodoc_dept_id: return abort(404, "groupes inexistants dans ce département") sem = formsemestre.to_dict() require_module = sco_preferences.get_preference( "abs_require_module", formsemestre_id ) etuds = [ sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] for m in groups_infos.members ] # Restreint aux inscrits au module sélectionné if moduleimpl_id: mod_inscrits = set( [ x["etudid"] for x in sco_moduleimpl.do_moduleimpl_inscription_list( moduleimpl_id=moduleimpl_id ) ] ) etuds = [e for e in etuds if e["etudid"] in mod_inscrits] if not moduleimpl_id: moduleimpl_id = None base_url_noweeks = ( "SignaleAbsenceGrSemestre?datedebut=%s&datefin=%s&%s&destination=%s" % ( datedebut, datefin, groups_infos.groups_query_args, urllib.parse.quote(destination), ) ) base_url = base_url_noweeks + "&nbweeks=%s" % nbweeks # sans le moduleimpl_id if etuds: nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) work_saturday = sco_abs.is_work_saturday() jourdebut = sco_abs.ddmmyyyy(datedebut, work_saturday=work_saturday) jourfin = sco_abs.ddmmyyyy(datefin, work_saturday=work_saturday) today = sco_abs.ddmmyyyy( time.strftime("%d/%m/%Y", time.localtime()), work_saturday=work_saturday, ) today.next_day() if jourfin > today: # ne propose jamais les semaines dans le futur jourfin = today if jourdebut > today: raise ScoValueError("date de début dans le futur (%s) !" % jourdebut) # if not jourdebut.iswork() or jourdebut > jourfin: raise ValueError( "date debut invalide (%s, ouvrable=%d)" % (str(jourdebut), jourdebut.iswork()) ) # calcule dates dates = [] # sco_abs.ddmmyyyy instances d = sco_abs.ddmmyyyy(datedebut, work_saturday=work_saturday) while d <= jourfin: dates.append(d) d = d.next_day(7) # avance d'une semaine # msg = "Montrer seulement les 4 dernières semaines" nwl = 4 if nbweeks: nbweeks = int(nbweeks) if nbweeks > 0: dates = dates[-nbweeks:] msg = "Montrer toutes les semaines" nwl = 0 url_link_semaines = base_url_noweeks + "&nbweeks=%s" % nwl if moduleimpl_id: url_link_semaines += "&moduleimpl_id=" + str(moduleimpl_id) # dates = [x.ISO() for x in dates] day_name = sco_abs.day_names()[jourdebut.weekday] if groups_infos.tous_les_etuds_du_sem: gr_tit = "en" else: if len(groups_infos.group_ids) > 1: p = "des groupes " else: p = "du groupe " gr_tit = p + '' + groups_infos.groups_titles + "" H = [ html_sco_header.sco_header( page_title=f"Saisie des absences du {day_name}", init_qtip=True, javascripts=["js/etud_info.js", "js/abs_ajax.js"], no_side_bar=1, ), f"""

Saisie des absences {gr_tit} {sem["titre_num"]}, les {day_name}s

{msg} """, ] # if etuds: modimpls_list = [] ues = nt.get_ues_stat_dict() for ue in ues: modimpls_list += nt.get_modimpls_dict(ue_id=ue["ue_id"]) menu_module = "" for modimpl in modimpls_list: if modimpl["moduleimpl_id"] == moduleimpl_id: sel = "selected" else: sel = "" menu_module += ( """\n""" % { "modimpl_id": modimpl["moduleimpl_id"], "modname": (modimpl["module"]["code"] or "") + " " + (modimpl["module"]["abbrev"] or modimpl["module"]["titre"] or ""), "sel": sel, } ) if moduleimpl_id: sel = "" else: sel = "selected" # aucun module specifie H.append( """

Module concerné par ces absences (%(optionel_txt)s):

""" % { "menu_module": menu_module, "url": base_url, "sel": sel, "optionel_txt": 'requis' if require_module else "optionnel", } ) H += _gen_form_saisie_groupe( etuds, dates, destination, moduleimpl_id, require_module ) H.append(html_sco_header.sco_footer()) return "\n".join(H) def _gen_form_saisie_groupe( etuds, dates, destination="", moduleimpl_id=None, require_module=False ): """Formulaire saisie absences Args: etuds: liste des étudiants dates: liste ordonnée de dates iso, par exemple: [ '2020-12-24', ... ] moduleimpl_id: optionnel, module concerné. """ H = [ f"""

""" ] # Dates odates = [datetime.date(*[int(x) for x in d.split("-")]) for d in dates] begin = dates[0] end = dates[-1] # Titres colonnes noms_jours = [] # eg [ "Lundi", "mardi", "Samedi", ... ] jn = sco_abs.day_names() for d in odates: idx_jour = d.weekday() if 0 <= idx_jour < len(jn): noms_jours.append(jn[idx_jour]) else: noms_jours.append("???") # jour non travaillé for jour in noms_jours: H.append( f"""""" ) H.append("") for d in odates: H.append( f"""""" ) H.append("") H.append("" * len(dates)) H.append("") # if not etuds: H.append( '' ) i = 1 cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) for etud in etuds: i += 1 etudid = etud["etudid"] etud_class = "etudinfo" # css # UE capitalisee dans semestre courant ? cap = [] if etud["cursem"]: formsemestre = FormSemestre.query.get_or_404( etud["cursem"]["formsemestre_id"] ) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) for ue in nt.get_ues_stat_dict(): ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) if ue_status and ue_status["is_capitalized"]: cap.append(ue["acronyme"]) if cap: capstr = ' (%s cap.)' % ", ".join(cap) else: capstr = "" if etud["etatincursem"] == "D": capstr += ' (dém.)' etud_class += " etuddem" tr_class = ("row_1", "row_2", "row_3")[i % 3] td_matin_class = ("matin_1", "matin_2", "matin_3")[i % 3] H.append( '' % ( tr_class, etud_class, etudid, url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), etud["nomprenom"], capstr, ) ) etud_abs = sco_abs.list_abs_in_range( etudid, begin, end, moduleimpl_id=moduleimpl_id, cursor=cursor ) for d in odates: date = d.strftime("%Y-%m-%d") # matin is_abs = {"jour": d, "matin": True} in etud_abs if is_abs: checked = "checked" else: checked = "" # bulle lors du passage souris coljour = sco_abs.DAYNAMES[(calendar.weekday(d.year, d.month, d.day))] datecol = coljour + " " + d.strftime("%d/%m/%Y") bulle_am = '"' + etud["nomprenom"] + " - " + datecol + ' (matin)"' bulle_pm = '"' + etud["nomprenom"] + " - " + datecol + ' (ap.midi)"' H.append( '' % ( td_matin_class, bulle_am, str(etudid) + ":" + date + ":" + "am", checked, etudid, date + ":am", ) ) # après-midi is_abs = {"jour": d, "matin": False} in etud_abs if is_abs: checked = "checked" else: checked = "" H.append( '' % ( bulle_pm, str(etudid) + ":" + date + ":" + "pm", checked, etudid, date + ":pm", ) ) H.append("") H.append("
{len(etuds)} étudiants { jour }
  { d.strftime("%d/%m/%Y") }
 AMPM
Aucun étudiant inscrit !
%s%s
") # place la liste des etudiants et les dates pour pouvoir effacer les absences H.append( '' % ",".join([str(etud["etudid"]) for etud in etuds]) ) H.append('' % dates[0]) H.append('' % dates[-1]) H.append('' % ",".join(dates)) H.append( '' % urllib.parse.quote(destination) ) # # version pour formulaire avec AJAX (Yann LB) H.append( """

Les cases cochées correspondent à des absences. Les absences saisies ne sont pas justifiées (sauf si un justificatif a été entré par ailleurs).

Si vous "décochez" une case, l'absence correspondante sera supprimée. Attention, les modifications sont automatiquement entregistrées au fur et à mesure.

""" ) return H @bp.route("/EtatAbsencesGr") @scodoc @permission_required(Permission.ScoView) @scodoc7func # ported from dtml def EtatAbsencesGr( group_ids=[], # list of groups to display debut="", fin="", with_boursier=True, # colonne boursier format="html", ): """Liste les absences de groupes""" datedebut = ndb.DateDMYtoISO(debut) datefin = ndb.DateDMYtoISO(fin) # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) formsemestre_id = groups_infos.formsemestre_id sem = groups_infos.formsemestre # Construit tableau (etudid, statut, nomprenom, nbJust, nbNonJust, NbTotal) T = [] for m in groups_infos.members: etud = sco_etud.get_etud_info(etudid=m["etudid"], filled=True)[0] nbabs = sco_abs.count_abs(etudid=etud["etudid"], debut=datedebut, fin=datefin) nbabsjust = sco_abs.count_abs_just( etudid=etud["etudid"], debut=datedebut, fin=datefin ) nbjustifs_noabs = len( sco_abs.list_abs_justifs( etudid=etud["etudid"], datedebut=datedebut, datefin=datefin, only_no_abs=True, ) ) # retrouve sem dans etud['sems'] s = None for s in etud["sems"]: if s["formsemestre_id"] == formsemestre_id: break if not s or s["formsemestre_id"] != formsemestre_id: raise ValueError( "EtatAbsencesGr: can't retreive sem" ) # bug or malicious arg T.append( { "etudid": etud["etudid"], "etatincursem": s["ins"]["etat"], "nomprenom": etud["nomprenom"], "nbabsjust": nbabsjust, "nbabsnonjust": nbabs - nbabsjust, "nbabs": nbabs, "nbjustifs_noabs": nbjustifs_noabs, "_nomprenom_target": "CalAbs?etudid=%s" % etud["etudid"], "_nomprenom_td_attrs": 'id="%s" class="etudinfo"' % etud["etudid"], "boursier": "oui" if etud["boursier"] else "non", } ) if s["ins"]["etat"] == "D": T[-1]["_css_row_class"] = "etuddem" T[-1]["nomprenom"] += " (dem)" columns_ids = [ "nomprenom", "nbjustifs_noabs", "nbabsjust", "nbabsnonjust", "nbabs", ] if with_boursier: columns_ids[1:1] = ["boursier"] if groups_infos.tous_les_etuds_du_sem: gr_tit = "" else: if len(groups_infos.group_ids) > 1: p = "des groupes" else: p = "du groupe" if format == "html": h = ' ' + groups_infos.groups_titles + "" else: h = groups_infos.groups_titles gr_tit = p + h title = f"État des absences {gr_tit}" if format == "xls" or format == "xml" or format == "json": columns_ids = ["etudid"] + columns_ids # --- Formulaire choix dates début / fin form_date = ( f"""
Période du  au    (nombre de demi-journées)
""" + """ """ ) tab = GenTable( columns_ids=columns_ids, rows=T, preferences=sco_preferences.SemPreferences(formsemestre_id), titles={ "etatincursem": "Etat", "nomprenom": "Nom", "nbabsjust": "Justifiées", "nbabsnonjust": "Non justifiées", "nbabs": "Total", "nbjustifs_noabs": "Justifs non utilisés", "boursier": "Bourse", }, html_sortable=True, html_class="table_leftalign", html_header=html_sco_header.sco_header( page_title=title, init_qtip=True, javascripts=["js/etud_info.js"], ), html_title=html_sco_header.html_sem_header(title, with_page_header=False) + form_date, # "

Période du %s au %s (nombre de demi-journées)
" % (debut, fin), base_url="%s&formsemestre_id=%s&debut=%s&fin=%s" % (groups_infos.base_url, formsemestre_id, debut, fin), filename="etat_abs_" + scu.make_filename( "%s de %s" % (groups_infos.groups_filename, sem["titreannee"]) ), caption=title, html_next_section="""

Justifs non utilisés: nombre de demi-journées avec justificatif mais sans absences relevées.

Cliquez sur un nom pour afficher le calendrier des absences
ou entrez une date pour visualiser les absents un jour donné :

%s
""" % (request.base_url, formsemestre_id, groups_infos.get_form_elem()), ) return tab.make_page(format=format) @bp.route("/EtatAbsencesDate") @scodoc @permission_required(Permission.ScoView) @scodoc7func def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display # ported from dtml """Etat des absences pour un groupe à une date donnée""" # Informations sur les groupes à afficher: groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) H = [html_sco_header.sco_header(page_title="Etat des absences")] if date: dateiso = ndb.DateDMYtoISO(date) nbetud = 0 t_nbabsjustam = 0 t_nbabsam = 0 t_nbabsjustpm = 0 t_nbabspm = 0 H.append(f"

État des absences le {date}

") H.append( """ """ ) for etud in groups_infos.members: nbabsam = sco_abs.count_abs( etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=1 ) nbabspm = sco_abs.count_abs( etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=0 ) if (nbabsam != 0) or (nbabspm != 0): nbetud += 1 nbabsjustam = sco_abs.count_abs_just( etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=1 ) nbabsjustpm = sco_abs.count_abs_just( etudid=etud["etudid"], debut=dateiso, fin=dateiso, matin=0 ) H.append( f"""") H.append( f"""
  Matin Après-midi
{etud["nomprenom"]} """ ) if nbabsam != 0: if nbabsjustam: H.append("Just.") t_nbabsjustam += 1 else: H.append("Abs.") t_nbabsam += 1 else: H.append("") H.append('') if nbabspm != 0: if nbabsjustpm: H.append("Just.") t_nbabsjustpm += 1 else: H.append("Abs.") t_nbabspm += 1 else: H.append("") H.append("
{t_nbabsam} abs, {t_nbabsjustam} just. {t_nbabspm} abs, {t_nbabsjustpm} just.
""" ) if nbetud == 0: H.append("

Aucune absence !

") else: H.append( """

Erreur: vous n'avez pas choisi de date !

""" ) return "\n".join(H) + html_sco_header.sco_footer() # ----- 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.ScoAbsAddBillet) @scodoc7func def AddBilletAbsence( begin, end, description, etudid=None, code_nip=None, code_ine=None, justified=True, format="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 format = "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(format=format) @bp.route("/add_billets_absence_form", methods=["GET", "POST"]) @scodoc @permission_required(Permission.ScoAbsAddBillet) @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(scu.ScoURL()) 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, format=False): """Liste billets pour un étudiant""" fmt = format or request.args.get("format", "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(format=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(format="xml") return "" @bp.route("/list_billets", methods=["GET"]) @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"

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 :"}),), method="get", 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.ScoAbsChange) @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. """ 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.strftime("%d/%m/%Y") datefin = billet.abs_end.strftime("%d/%m/%Y") dates = sco_abs.DateRangeISO(datedebut, datefin) # commence après-midi ? if dates and billet.abs_begin.hour > 11: sco_abs.add_absence( billet.etudid, dates[0], 0, estjust, description=description, ) n += 1 dates = dates[1:] # termine matin ? if dates and billet.abs_end.hour < 12: sco_abs.add_absence( billet.etudid, dates[-1], 1, estjust, description=description, ) n += 1 dates = dates[:-1] for jour in dates: sco_abs.add_absence( billet.etudid, jour, 0, estjust, description=description, ) sco_abs.add_absence( billet.etudid, jour, 1, estjust, description=description, ) n += 2 # 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.ScoAbsChange) @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(scu.ScoURL()) 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() @bp.route("/XMLgetAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP @scodoc @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def XMLgetAbsEtud(beg_date="", end_date=""): """returns list of absences in date interval""" t0 = time.time() etuds = sco_etud.get_etud_info(filled=False) if not etuds: raise APIInvalidParams("étudiant inconnu") etud = etuds[0] exp = re.compile(r"^(\d{4})\D?(0[1-9]|1[0-2])\D?([12]\d|0[1-9]|3[01])$") if not exp.match(beg_date): raise ScoValueError("invalid date: %s" % beg_date) if not exp.match(end_date): raise ScoValueError("invalid date: %s" % end_date) abs_list = sco_abs.list_abs_date(etud["etudid"], beg_date, end_date) doc = ElementTree.Element( "absences", etudid=str(etud["etudid"]), beg_date=beg_date, end_date=end_date ) for a in abs_list: if a["estabs"]: # ne donne pas les justifications si pas d'absence doc.append( ElementTree.Element( "abs", begin=a["begin"], end=a["end"], description=a["description"], justified=str(int(a["estjust"])), ) ) log("XMLgetAbsEtud (%gs)" % (time.time() - t0)) data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) return scu.send_file(data, mime=scu.XML_MIMETYPE, attached=False)