# -*- mode: python -*- # -*- 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 # ############################################################################## """Pages HTML gestion absences (la plupart portées du DTML) """ import datetime from flask import url_for, g, request, abort from app import log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Identite, FormSemestre import app.scodoc.sco_utils as scu from app.scodoc import notesdb as ndb from app.scodoc.scolog import logdb 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_etud from app.scodoc import sco_find_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc.sco_exceptions import ScoValueError def doSignaleAbsence( datedebut, datefin, moduleimpl_id=None, demijournee=2, estjust=False, description=None, etudid=False, ): # etudid implied """Signalement d'une absence. Args: datedebut: dd/mm/yyyy datefin: dd/mm/yyyy (non incluse) moduleimpl_id: module auquel imputer les absences demijournee: 2 si journée complète, 1 matin, 0 après-midi estjust: absence justifiée description: str etudid: etudiant concerné. Si non spécifié, cherche dans les paramètres de la requête courante. """ etud = Identite.from_request(etudid) if not moduleimpl_id: moduleimpl_id = None description_abs = description dates = sco_abs.DateRangeISO(datedebut, datefin) nbadded = 0 demijournee = int(demijournee) for jour in dates: if demijournee == 2: sco_abs.add_absence( etud.id, jour, False, estjust, description_abs, moduleimpl_id, ) sco_abs.add_absence( etud.id, jour, True, estjust, description_abs, moduleimpl_id, ) nbadded += 2 else: sco_abs.add_absence( etud.id, jour, demijournee, estjust, description_abs, moduleimpl_id, ) nbadded += 1 # if estjust: just_str = "" else: just_str = "NON " indication_module = "" if moduleimpl_id and moduleimpl_id != "NULL": mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] formsemestre_id = mod["formsemestre_id"] formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ues = nt.get_ues_stat_dict() for ue in ues: modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"]) for modimpl in modimpls: if modimpl["moduleimpl_id"] == moduleimpl_id: indication_module = "dans le module %s" % ( modimpl["module"]["code"] or "(pas de code)" ) H = [ html_sco_header.sco_header( page_title=f"Signalement d'une absence pour {etud.nomprenom}", ), """

Signalement d'absences

""", ] if dates: H.append( f"""

Ajout de {nbadded} absences {just_str}justifiées du {datedebut} au {datefin} {indication_module}

""" ) else: H.append( f"""

Aucune date ouvrable entre le {datedebut} et le {datefin} !

""" ) H.append( f"""
{sco_find_etud.form_search_etud()} {html_sco_header.sco_footer()} """ ) return "\n".join(H) def SignaleAbsenceEtud(): # etudid implied """Formulaire individuel simple de signalement d'une absence""" # brute-force portage from very old dtml code ... etud = sco_etud.get_etud_info(filled=True)[0] etudid = etud["etudid"] disabled = False if not etud["cursem"]: require_module = sco_preferences.get_preference( "abs_require_module" ) # on utilise la pref globale car pas de sem courant if require_module: menu_module = """
Pas inscrit dans un semestre courant, et l'indication du module est requise. Donc pas de saisie d'absence possible !
""" disabled = True else: menu_module = "" else: formsemestre_id = etud["cursem"]["formsemestre_id"] formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ues = nt.get_ues_stat_dict() require_module = sco_preferences.get_preference( "abs_require_module", formsemestre_id ) if require_module: menu_module = """

Module: """ ) menu_module += """""" for ue in ues: modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"]) for modimpl in modimpls: menu_module += ( """\n""" % { "modimpl_id": modimpl["moduleimpl_id"], "modname": modimpl["module"]["code"] or "", } ) menu_module += """

""" H = [ html_sco_header.sco_header( page_title="Signalement d'une absence pour %(nomprenom)s" % etud, ), """

Signalement d'une absence pour %(nomprenom)s

""" % etud, """""" % url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]), sco_photos.etud_photo_html( etudid=etudid, title="fiche de " + etud["nomprenom"], ), """
""", """

Date début : j/m/a    Date fin (optionnelle): j/m/a

Journée(s)  Matin(s)  Après-midi %(menu_module)s

Absence justifiée.
Raison: (optionnel)

Seuls les modules du semestre en cours apparaissent.

Évitez de saisir une absence pour un module qui n'est pas en place à cette date.

Toutes les dates sont au format jour/mois/annee.

""" % { "etudid": etud["etudid"], "menu_module": menu_module, "disabled": "disabled" if disabled else "", }, html_sco_header.sco_footer(), ] return "\n".join(H) def doJustifAbsence( datedebut, datefin, demijournee, description=None, etudid=False, ): # etudid implied """Justification d'une absence Args: datedebut: dd/mm/yyyy datefin: dd/mm/yyyy (non incluse) demijournee: 2 si journée complète, 1 matin, 0 après-midi estjust: absence justifiée description: str etudid: etudiant concerné. Si non spécifié, cherche dans les paramètres de la requête. """ etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etudid = etud["etudid"] description_abs = description dates = sco_abs.DateRangeISO(datedebut, datefin) nbadded = 0 demijournee = int(demijournee) for jour in dates: if demijournee == 2: sco_abs.add_justif( etudid=etudid, jour=jour, matin=False, description=description_abs, ) sco_abs.add_justif( etudid=etudid, jour=jour, matin=True, description=description_abs, ) nbadded += 2 else: sco_abs.add_justif( etudid=etudid, jour=jour, matin=demijournee, description=description_abs, ) nbadded += 1 # H = [ html_sco_header.sco_header( page_title="Justification d'une absence pour %(nomprenom)s" % etud, ), """

Justification d'absences

""", ] if dates: H.append( """

Ajout de %d justifications du %s au %s

""" % (nbadded, datedebut, datefin) ) else: H.append( """

Aucune date ouvrable entre le %s et le %s !

""" % (datedebut, datefin) ) H.append( """
""" % etud ) H.append(sco_find_etud.form_search_etud()) H.append(html_sco_header.sco_footer()) return "\n".join(H) def JustifAbsenceEtud(): # etudid implied """Formulaire individuel simple de justification d'une absence""" # brute-force portage from very old dtml code ... etud = sco_etud.get_etud_info(filled=True)[0] etudid = etud["etudid"] H = [ html_sco_header.sco_header( page_title="Justification d'une absence pour %(nomprenom)s" % etud, ), """

Justification d'une absence pour %(nomprenom)s

""" % etud, """""" % url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), sco_photos.etud_photo_html( etudid=etudid, title="fiche de " + etud["nomprenom"], ), """
""", """

Date début :    Date Fin (optionnel):

Journée(s)  Matin(s)  Après midi

Raison: (optionnel)

""" % etud, html_sco_header.sco_footer(), ] return "\n".join(H) def doAnnuleAbsence(datedebut, datefin, demijournee, etudid=False): # etudid implied """Annulation des absences pour une demi journée""" etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etudid = etud["etudid"] dates = sco_abs.DateRangeISO(datedebut, datefin) nbadded = 0 demijournee = int(demijournee) for jour in dates: if demijournee == 2: sco_abs.annule_absence(etudid, jour, False) sco_abs.annule_absence(etudid, jour, True) nbadded += 2 else: sco_abs.annule_absence(etudid, jour, demijournee) nbadded += 1 # H = [ html_sco_header.sco_header( page_title="Annulation d'une absence pour %(nomprenom)s" % etud, ), """

Annulation d'absences pour %(nomprenom)s

""" % etud, ] if dates: H.append( "

Annulation sur %d demi-journées du %s au %s" % (nbadded, datedebut, datefin) ) else: H.append( """

Aucune date ouvrable entre le %s et le %s !

""" % (datedebut, datefin) ) H.append( """
""" % etud ) H.append(sco_find_etud.form_search_etud()) H.append(html_sco_header.sco_footer()) return "\n".join(H) def AnnuleAbsenceEtud(): # etudid implied """Formulaire individuel simple d'annulation d'une absence""" # brute-force portage from very old dtml code ... etud = sco_etud.get_etud_info(filled=True)[0] etudid = etud["etudid"] H = [ html_sco_header.sco_header( page_title="Annulation d'une absence pour %(nomprenom)s" % etud, ), """

Annulation d'une absence pour %(nomprenom)s

""" % etud, """""" % url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), sco_photos.etud_photo_html( etudid=etudid, title="fiche de " + etud["nomprenom"], ), """
""", """

A n'utiliser que suite à une erreur de saisie ou lorsqu'il s'avère que l'étudiant était en fait présent.

Si plusieurs modules sont affectés, les absences seront toutes effacées.

""" % etud, """

Date début : j/m/a    Date Fin (optionnel): j/m/a
journée(s)  Matin(s)  Après midi

Date début : j/m/a    Date Fin (optionnel): j/m/a

journée(s)  Matin(s)  Après midi

(utiliser ceci en cas de justificatif erroné saisi indépendemment d'une absence)

""" % etud, html_sco_header.sco_footer(), ] return "\n".join(H) def doAnnuleJustif(datedebut0, datefin0, demijournee): # etudid implied """Annulation d'une justification""" etud = sco_etud.get_etud_info(filled=True)[0] etudid = etud["etudid"] dates = sco_abs.DateRangeISO(datedebut0, datefin0) nbadded = 0 demijournee = int(demijournee) for jour in dates: # Attention: supprime matin et après-midi if demijournee == 2: sco_abs.annule_justif(etudid, jour, False) sco_abs.annule_justif(etudid, jour, True) nbadded += 2 else: sco_abs.annule_justif(etudid, jour, demijournee) nbadded += 1 # H = [ html_sco_header.sco_header( page_title="Annulation d'une justification pour %(nomprenom)s" % etud, ), """

Annulation de justifications pour %(nomprenom)s

""" % etud, ] if dates: H.append( "

Annulation sur %d demi-journées du %s au %s" % (nbadded, datedebut0, datefin0) ) else: H.append( """

Aucune date ouvrable entre le %s et le %s !

""" % (datedebut0, datefin0) ) H.append( """
""" % etud ) H.append(sco_find_etud.form_search_etud()) H.append(html_sco_header.sco_footer()) return "\n".join(H) def AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id=None): """Supprime les absences non justifiées aux dates indiquées Ne supprime pas les justificatifs éventuels. Args: etudid: l'étudiant dates: liste de dates iso, eg [ "2000-01-15", "2000-01-16" ] moduleimpl_id: si spécifié, n'affecte que les absences de ce module Returns: None """ # log('AnnuleAbsencesDatesNoJust: moduleimpl_id=%s' % moduleimpl_id) if not dates: return date0 = dates[0] if len(date0.split(":")) == 2: # am/pm is present for date in dates: jour, ampm = date.split(":") if ampm == "am": matin = 1 elif ampm == "pm": matin = 0 else: raise ValueError("invalid ampm !") sco_abs.annule_absence(etudid, jour, matin, moduleimpl_id) return cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) # supr les absences non justifiees for date in dates: cursor.execute( """DELETE FROM absences WHERE etudid=%(etudid)s AND (not estjust) AND jour=%(date)s AND moduleimpl_id=%(moduleimpl_id)s """, vars(), ) sco_abs.invalidate_abs_etud_date(etudid, date) # s'assure que les justificatifs ne sont pas "absents" for date in dates: cursor.execute( """UPDATE absences SET estabs=FALSE WHERE etudid=%(etudid)s AND jour=%(date)s AND moduleimpl_id=%(moduleimpl_id)s """, vars(), ) if dates: date0 = dates[0] else: date0 = None if len(dates) > 1: date1 = dates[1] else: date1 = None logdb( cnx, "AnnuleAbsencesDatesNoJust", etudid=etudid, msg="%s - %s - %s" % (date0, date1, moduleimpl_id), ) cnx.commit() def EtatAbsences(): """Etat des absences: choix du groupe""" # crude portage from 1999 DTML H = [ html_sco_header.sco_header(page_title="Etat des absences"), """

État des absences pour un groupe

""", formChoixSemestreGroupe(), """
Date de début (j/m/a) :
Date de fin :
""" % (scu.annee_scolaire(), datetime.datetime.now().strftime("%d/%m/%Y")), html_sco_header.sco_footer(), ] return "\n".join(H) def formChoixSemestreGroupe(all=False): """partie de formulaire pour le choix d'un semestre et d'un groupe. Si all, donne tous les semestres (même ceux verrouillés). """ # XXX assez primitif, à ameliorer TOTALEMENT OBSOLETE ! if all: sems = sco_formsemestre.do_formsemestre_list() else: sems = sco_formsemestre.do_formsemestre_list(args={"etat": "1"}) if not sems: raise ScoValueError("aucun semestre !") H = ['") return "\n".join(H) def _convert_sco_year(year) -> int: try: year = int(year) if year > 1900 and year < 2999: return year except ValueError: raise ScoValueError("année scolaire invalide") def CalAbs(etudid, sco_year=None): """Calendrier des absences d'un etudiant""" # crude portage from 1999 DTML etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etudid = etud["etudid"] if sco_year: annee_scolaire = _convert_sco_year(sco_year) else: annee_scolaire = scu.annee_scolaire() datedebut = str(annee_scolaire) + "-08-01" datefin = str(annee_scolaire + 1) + "-07-31" annee_courante = scu.annee_scolaire() nbabs = sco_abs.count_abs(etudid=etudid, debut=datedebut, fin=datefin) nbabsjust = sco_abs.count_abs_just(etudid=etudid, debut=datedebut, fin=datefin) events = [] for a in sco_abs.list_abs_just(etudid=etudid, datedebut=datedebut): events.append( (str(a["jour"]), "a", "#F8B7B0", "", a["matin"], a["description"]) ) for a in sco_abs.list_abs_non_just(etudid=etudid, datedebut=datedebut): events.append( (str(a["jour"]), "A", "#EE0000", "", a["matin"], a["description"]) ) justifs_noabs = sco_abs.list_abs_justifs( etudid=etudid, datedebut=datedebut, only_no_abs=True ) for a in justifs_noabs: events.append( (str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"]) ) CalHTML = sco_abs.YearTable(annee_scolaire, events=events, halfday=1) # H = [ html_sco_header.sco_header( page_title="Calendrier des absences de %(nomprenom)s" % etud, cssstyles=["css/calabs.css"], ), """

Absences de %(nomprenom)s (%(inscription)s)

""" % etud, """A : absence NON justifiée
a : absence justifiée
X : justification sans absence
%d absences sur l'année, dont %d justifiées (soit %d non justifiées)
(%d justificatifs inutilisés)

""" % (nbabs, nbabsjust, nbabs - nbabsjust, len(justifs_noabs)), """
%s
""" % ( url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), sco_photos.etud_photo_html( etudid=etudid, title="fiche de " + etud["nomprenom"], ), ), CalHTML, """
""", """""" % etudid, """Année scolaire %s-%s""" % (annee_scolaire, annee_scolaire + 1), """  Changer année:
""") H.append(html_sco_header.sco_footer()) return "\n".join(H) def ListeAbsEtud( etudid=None, code_nip=None, with_evals=True, format="html", absjust_only=0, sco_year=None, ): """Liste des absences d'un étudiant sur l'année en cours En format 'html': page avec deux tableaux (non justifiées et justifiées). En format json, xml, xls ou pdf: l'un ou l'autre des table, suivant absjust_only. En format 'text': texte avec liste d'absences (pour mails). Args: etudid: with_evals: indique les evaluations aux dates d'absences absjust_only: si vrai, renvoie table absences justifiées sco_year: année scolaire à utiliser. Si non spécifier, utilie l'année en cours. e.g. "2005" """ # si absjust_only, table absjust seule (export xls ou pdf) absjust_only = scu.to_bool(absjust_only) if sco_year: annee_scolaire = _convert_sco_year(sco_year) else: annee_scolaire = scu.annee_scolaire() datedebut = f"{annee_scolaire}-{scu.MONTH_DEBUT_ANNEE_SCOLAIRE+1}-01" etudid = etudid or False etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) if not etuds: log(f"ListeAbsEtud: no etuds with etudid={etudid} or nip={code_nip}") abort(404) etud = etuds[0] etudid = etud["etudid"] # Liste des absences et titres colonnes tables: titles, columns_ids, absnonjust, absjust = _tables_abs_etud( etudid, datedebut, with_evals=with_evals, format=format ) if request.base_url: base_url_nj = "%s?etudid=%s&absjust_only=0" % (request.base_url, etudid) base_url_j = "%s?etudid=%s&absjust_only=1" % (request.base_url, etudid) else: base_url_nj = base_url_j = "" tab_absnonjust = GenTable( titles=titles, columns_ids=columns_ids, rows=absnonjust, html_class="table_leftalign", table_id="tab_absnonjust", base_url=base_url_nj, filename="abs_" + scu.make_filename(etud["nomprenom"]), caption="Absences non justifiées de %(nomprenom)s" % etud, preferences=sco_preferences.SemPreferences(), ) tab_absjust = GenTable( titles=titles, columns_ids=columns_ids, rows=absjust, html_class="table_leftalign", table_id="tab_absjust", base_url=base_url_j, filename="absjust_" + scu.make_filename(etud["nomprenom"]), caption="Absences justifiées de %(nomprenom)s" % etud, preferences=sco_preferences.SemPreferences(), ) # Formats non HTML et demande d'une seule table: if format != "html" and format != "text": if absjust_only == 1: return tab_absjust.make_page(format=format) else: return tab_absnonjust.make_page(format=format) if format == "html": # Mise en forme HTML: H = [] H.append( html_sco_header.sco_header(page_title="Absences de %s" % etud["nomprenom"]) ) H.append( """

Absences de %s (à partir du %s)

""" % (etud["nomprenom"], ndb.DateISOtoDMY(datedebut)) ) if len(absnonjust): H.append("

%d absences non justifiées:

" % len(absnonjust)) H.append(tab_absnonjust.html()) else: H.append("""

Pas d'absences non justifiées

""") if len(absjust): H.append("""

%d absences justifiées:

""" % len(absjust)) H.append(tab_absjust.html()) else: H.append("""

Pas d'absences justifiées

""") return "\n".join(H) + html_sco_header.sco_footer() elif format == "text": T = [] if not len(absnonjust) and not len(absjust): T.append( """--- Pas d'absences enregistrées depuis le %s""" % ndb.DateISOtoDMY(datedebut) ) else: T.append( """--- Absences enregistrées à partir du %s:""" % ndb.DateISOtoDMY(datedebut) ) T.append("\n") if len(absnonjust): T.append("* %d absences non justifiées:" % len(absnonjust)) T.append(tab_absnonjust.text()) if len(absjust): T.append("* %d absences justifiées:" % len(absjust)) T.append(tab_absjust.text()) return "\n".join(T) else: raise ValueError("Invalid format !") def _tables_abs_etud( etudid, datedebut, with_evals=True, format="html", absjust_only=0, ): """Tables des absences justifiees et non justifiees d'un étudiant sur l'année en cours """ absjust = sco_abs.list_abs_just(etudid=etudid, datedebut=datedebut) absnonjust = sco_abs.list_abs_non_just(etudid=etudid, datedebut=datedebut) # examens ces jours là ? if with_evals: cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) for a in absnonjust + absjust: cursor.execute( """SELECT eval.* FROM notes_evaluation eval, notes_moduleimpl_inscription mi, notes_moduleimpl m WHERE eval.jour = %(jour)s and eval.moduleimpl_id = m.id and mi.moduleimpl_id = m.id and mi.etudid = %(etudid)s """, {"jour": a["jour"].strftime("%Y-%m-%d"), "etudid": etudid}, ) a["evals"] = cursor.dictfetchall() cursor.execute( """SELECT mi.moduleimpl_id FROM absences abs, notes_moduleimpl_inscription mi, notes_moduleimpl m WHERE abs.matin = %(matin)s and abs.jour = %(jour)s and abs.etudid = %(etudid)s and abs.moduleimpl_id = mi.moduleimpl_id and mi.moduleimpl_id = m.id and mi.etudid = %(etudid)s """, { "matin": bool(a["matin"]), "jour": a["jour"].strftime("%Y-%m-%d"), "etudid": etudid, }, ) a["absent"] = cursor.dictfetchall() def matin(x): if x: return "matin" else: return "après-midi" def descr_exams(a): if "evals" not in a: return "" ex = [] for ev in a["evals"]: mod = sco_moduleimpl.moduleimpl_withmodule_list( moduleimpl_id=ev["moduleimpl_id"] )[0] if format == "html": ex.append( f"""{mod["module"]["code"] or "(module sans code)"}""" ) else: ex.append(mod["module"]["code"] or "(module sans code)") if ex: return ", ".join(ex) return "" def descr_abs(a): ex = [] for ev in a.get("absent", []): mod = sco_moduleimpl.moduleimpl_withmodule_list( moduleimpl_id=ev["moduleimpl_id"] )[0] if format == "html": ex.append( f"""{mod["module"]["code"] or '(module sans code)'}""" ) else: ex.append(mod["module"]["code"] or "(module sans code)") if ex: return ", ".join(ex) return "" # ajoute date formatée et évaluations for L in (absnonjust, absjust): for a in L: if with_evals: a["exams"] = descr_exams(a) a["datedmy"] = a["jour"].strftime("%d/%m/%Y") a["ampm"] = int(a["matin"]) a["matin"] = matin(a["matin"]) index = a["description"].find(")") if index != -1: a["motif"] = a["description"][1:index] else: a["motif"] = "" a["description"] = descr_abs(a) or "" # ajoute lien pour justifier if format == "html": for a in absnonjust: a["justlink"] = "justifier" a[ "_justlink_target" ] = "doJustifAbsence?etudid=%s&datedebut=%s&datefin=%s&demijournee=%s" % ( etudid, a["datedmy"], a["datedmy"], a["ampm"], ) # titles = { "datedmy": "Date", "matin": "", "exams": "Examens ce jour", "justlink": "", "description": "Modules", "motif": "Motif", } columns_ids = ["datedmy", "matin"] if format in ("json", "xml"): columns_ids += ["jour", "ampm"] if with_evals: columns_ids.append("exams") columns_ids.append("description") columns_ids.append("motif") if format == "html": columns_ids.append("justlink") return titles, columns_ids, absnonjust, absjust