# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2021 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 # ############################################################################## """ Gestion des absences (v4) C'est la partie la plus ancienne de ScoDoc, et elle est à revoir. L'API de plus bas niveau est en gros: AnnuleAbsencesDatesNoJust(etudid, dates) CountAbs(etudid, debut, fin, matin=None, moduleimpl_id=None) CountAbsJust(etudid, debut, fin, matin=None, moduleimpl_id=None) ListeAbsJust(etudid, datedebut) [pas de fin ?] ListeAbsNonJust(etudid, datedebut) [pas de fin ?] ListeJustifs(etudid, datedebut, datefin=None, only_no_abs=True) ListeAbsJour(date, am=True, pm=True, is_abs=None, is_just=None) ListeAbsNonJustJour(date, am=True, pm=True) """ import string import re import time import datetime import dateutil import dateutil.parser import calendar import urllib import cgi import jaxml # --------------- from sco_zope import * # --------------- import sco_utils as scu import notesdb from notes_log import log from scolog import logdb from sco_permissions import ScoAbsAddBillet, ScoAbsChange, ScoView from sco_exceptions import ScoValueError, ScoInvalidDateError from TrivialFormulator import TrivialFormulator, TF from gen_tables import GenTable import scolars import sco_formsemestre import sco_moduleimpl import sco_groups import sco_groups_view import sco_excel import sco_abs_notification, sco_abs_views import sco_compute_moy import sco_abs from sco_abs import ddmmyyyy def _toboolean(x): "convert a value to boolean (ensure backward compat with OLD intranet code)" if type(x) == type(""): x = x.lower() if x and x != "false": # backward compat... return True else: return False class ZAbsences( ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit ): "ZAbsences object" meta_type = "ZAbsences" security = ClassSecurityInfo() # This is the list of the methods associated to 'tabs' in the ZMI # Be aware that The first in the list is the one shown by default, so if # the 'View' tab is the first, you will never see your tabs by cliquing # on the object. manage_options = ( ({"label": "Contents", "action": "manage_main"},) + PropertyManager.manage_options # add the 'Properties' tab + ({"label": "View", "action": "index_html"},) + Item.manage_options # add the 'Undo' & 'Owner' tab + RoleManager.manage_options # add the 'Security' tab ) # no permissions, only called from python def __init__(self, id, title): "initialise a new instance" self.id = id self.title = title # The form used to edit this object # def manage_editZAbsences(self, title, RESPONSE=None): # "Changes the instance values" # self.title = title # self._p_changed = 1 # RESPONSE.redirect("manage_editForm") # -------------------------------------------------------------------- # # ABSENCES (top level) # # -------------------------------------------------------------------- # used to view content of the object security.declareProtected(ScoView, "index_html") index_html = sco_abs_views.absences_index_html security.declareProtected(ScoView, "EtatAbsences") EtatAbsences = sco_abs_views.EtatAbsences security.declareProtected(ScoView, "CalAbs") CalAbs = sco_abs_views.CalAbs security.declareProtected(ScoAbsChange, "SignaleAbsenceEtud") SignaleAbsenceEtud = sco_abs_views.SignaleAbsenceEtud security.declareProtected(ScoAbsChange, "doSignaleAbsence") doSignaleAbsence = sco_abs_views.doSignaleAbsence security.declareProtected(ScoAbsChange, "JustifAbsenceEtud") JustifAbsenceEtud = sco_abs_views.JustifAbsenceEtud security.declareProtected(ScoAbsChange, "doJustifAbsence") doJustifAbsence = sco_abs_views.doJustifAbsence security.declareProtected(ScoAbsChange, "AnnuleAbsenceEtud") AnnuleAbsenceEtud = sco_abs_views.AnnuleAbsenceEtud security.declareProtected(ScoAbsChange, "doAnnuleAbsence") doAnnuleAbsence = sco_abs_views.doAnnuleAbsence security.declareProtected(ScoAbsChange, "doAnnuleJustif") doAnnuleJustif = sco_abs_views.doAnnuleJustif security.declareProtected(ScoView, "ListeAbsEtud") ListeAbsEtud = sco_abs_views.ListeAbsEtud # -------------------------------------------------------------------- # # SQL METHODS # # -------------------------------------------------------------------- def _AddAbsence( self, etudid, jour, matin, estjust, REQUEST, description=None, moduleimpl_id=None, ): "Ajoute une absence dans la bd" # unpublished if self._isFarFutur(jour): raise ScoValueError("date absence trop loin dans le futur !") estjust = _toboolean(estjust) matin = _toboolean(matin) cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( "insert into absences (etudid,jour,estabs,estjust,matin,description, moduleimpl_id) values (%(etudid)s, %(jour)s, TRUE, %(estjust)s, %(matin)s, %(description)s, %(moduleimpl_id)s )", vars(), ) logdb( REQUEST, cnx, "AddAbsence", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s,ESTJUST=%(estjust)s,description=%(description)s,moduleimpl_id=%(moduleimpl_id)s" % vars(), ) cnx.commit() sco_abs.invalidateAbsEtudDate(self, etudid, jour) sco_abs_notification.abs_notify(self, etudid, jour) def _AddJustif(self, etudid, jour, matin, REQUEST, description=None): "Ajoute un justificatif dans la base" # unpublished if self._isFarFutur(jour): raise ScoValueError("date justificatif trop loin dans le futur !") matin = _toboolean(matin) cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( "insert into absences (etudid,jour,estabs,estjust,matin, description) values (%(etudid)s,%(jour)s, FALSE, TRUE, %(matin)s, %(description)s )", vars(), ) logdb( REQUEST, cnx, "AddJustif", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), ) cnx.commit() sco_abs.invalidateAbsEtudDate(self, etudid, jour) def _AnnuleAbsence(self, etudid, jour, matin, moduleimpl_id=None, REQUEST=None): """Annule une absence ds base Si moduleimpl_id, n'annule que pour ce module """ # unpublished matin = _toboolean(matin) cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) req = "delete from absences where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s and estabs" if moduleimpl_id: req += " and moduleimpl_id=%(moduleimpl_id)s" cursor.execute(req, vars()) logdb( REQUEST, cnx, "AnnuleAbsence", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s,moduleimpl_id=%(moduleimpl_id)s" % vars(), ) cnx.commit() sco_abs.invalidateAbsEtudDate(self, etudid, jour) def _AnnuleJustif(self, etudid, jour, matin, REQUEST=None): "Annule un justificatif" # unpublished matin = _toboolean(matin) cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( "delete from absences where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s and ESTJUST AND NOT ESTABS", vars(), ) cursor.execute( "update absences set estjust=false where jour=%(jour)s and matin=%(matin)s and etudid=%(etudid)s", vars(), ) logdb( REQUEST, cnx, "AnnuleJustif", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), ) cnx.commit() sco_abs.invalidateAbsEtudDate(self, etudid, jour) # Fonction inutile à supprimer (gestion moduleimpl_id incorrecte): # def _AnnuleAbsencesPeriodNoJust(self, etudid, datedebut, datefin, # moduleimpl_id=None, REQUEST=None): # """Supprime les absences entre ces dates (incluses). # mais ne supprime pas les justificatifs. # """ # # unpublished # cnx = self.GetDBConnexion() # cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) # # supr les absences non justifiees # cursor.execute("delete from absences where etudid=%(etudid)s and (not estjust) and moduleimpl_id=(moduleimpl_id)s and jour BETWEEN %(datedebut)s AND %(datefin)s", # vars() ) # # s'assure que les justificatifs ne sont pas "absents" # cursor.execute("update absences set estabs=FALSE where etudid=%(etudid)s and jour and moduleimpl_id=(moduleimpl_id)s BETWEEN %(datedebut)s AND %(datefin)s", vars()) # logdb(REQUEST, cnx, 'AnnuleAbsencesPeriodNoJust', etudid=etudid, # msg='%(datedebut)s - %(datefin)s - (moduleimpl_id)s'%vars()) # cnx.commit() # sco_abs.invalidateAbsEtudDate(self, etudid, datedebut) # sco_abs.invalidateAbsEtudDate(self, etudid, datefin) # si un semestre commence apres datedebut et termine avant datefin, il ne sera pas invalide. Tant pis ;-) security.declareProtected(ScoAbsChange, "AnnuleAbsencesDatesNoJust") def AnnuleAbsencesDatesNoJust( self, etudid, dates, moduleimpl_id=None, REQUEST=None ): """Supprime les absences aux dates indiquées mais ne supprime pas les justificatifs. """ # 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 !") self._AnnuleAbsence(etudid, jour, matin, moduleimpl_id, REQUEST) return cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.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.invalidateAbsEtudDate(self, 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( REQUEST, cnx, "AnnuleAbsencesDatesNoJust", etudid=etudid, msg="%s - %s - %s" % (date0, date1, moduleimpl_id), ) cnx.commit() security.declareProtected(ScoView, "CountAbs") def CountAbs(self, etudid, debut, fin, matin=None, moduleimpl_id=None): """CountAbs matin= 1 ou 0. """ if matin != None: matin = _toboolean(matin) ismatin = " AND A.MATIN = %(matin)s " else: ismatin = "" if moduleimpl_id: modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " else: modul = "" cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( """SELECT COUNT(*) AS NbAbs FROM ( SELECT DISTINCT A.JOUR, A.MATIN FROM ABSENCES A WHERE A.ETUDID = %(etudid)s AND A.ESTABS""" + ismatin + modul + """ AND A.JOUR BETWEEN %(debut)s AND %(fin)s ) AS tmp """, vars(), ) res = cursor.fetchone()[0] return res security.declareProtected(ScoView, "CountAbsJust") def CountAbsJust(self, etudid, debut, fin, matin=None, moduleimpl_id=None): "Count just. abs" if matin != None: matin = _toboolean(matin) ismatin = " AND A.MATIN = %(matin)s " else: ismatin = "" if moduleimpl_id: modul = " AND A.MODULEIMPL_ID = %(moduleimpl_id)s " else: modul = "" cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( """SELECT COUNT(*) AS NbAbsJust FROM ( SELECT DISTINCT A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B WHERE A.ETUDID = %(etudid)s AND A.ETUDID = B.ETUDID AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR BETWEEN %(debut)s AND %(fin)s AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST)""" + ismatin + modul + """ ) AS tmp """, vars(), ) res = cursor.fetchone()[0] return res def _ListeAbsDate(self, etudid, beg_date, end_date): # Liste des absences et justifs entre deux dates cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( """SELECT jour, matin, estabs, estjust, description FROM ABSENCES A WHERE A.ETUDID = %(etudid)s AND A.jour >= %(beg_date)s AND A.jour <= %(end_date)s """, vars(), ) Abs = cursor.dictfetchall() # remove duplicates A = {} # { (jour, matin) : abs } for a in Abs: jour, matin = a["jour"], a["matin"] if (jour, matin) in A: # garde toujours la description a["description"] = a["description"] or A[(jour, matin)]["description"] # et la justif: a["estjust"] = a["estjust"] or A[(jour, matin)]["estjust"] a["estabs"] = a["estabs"] or A[(jour, matin)]["estabs"] A[(jour, matin)] = a else: A[(jour, matin)] = a if A[(jour, matin)]["description"] is None: A[(jour, matin)]["description"] = "" # add hours: matin = 8:00 - 12:00, apresmidi = 12:00 - 18:00 dat = "%04d-%02d-%02d" % (a["jour"].year, a["jour"].month, a["jour"].day) if a["matin"]: A[(jour, matin)]["begin"] = dat + " 08:00:00" A[(jour, matin)]["end"] = dat + " 11:59:59" else: A[(jour, matin)]["begin"] = dat + " 12:00:00" A[(jour, matin)]["end"] = dat + " 17:59:59" # sort R = A.values() R.sort(key=lambda x: (x["begin"])) return R security.declareProtected(ScoView, "ListeAbsJust") def ListeAbsJust(self, etudid, datedebut): "Liste des absences justifiees (par ordre chronologique)" cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( """SELECT DISTINCT A.ETUDID, A.JOUR, A.MATIN FROM ABSENCES A, ABSENCES B WHERE A.ETUDID = %(etudid)s AND A.ETUDID = B.ETUDID AND A.JOUR = B.JOUR AND A.MATIN = B.MATIN AND A.JOUR >= %(datedebut)s AND A.ESTABS AND (A.ESTJUST OR B.ESTJUST) ORDER BY A.JOUR """, vars(), ) A = cursor.dictfetchall() for a in A: a["description"] = self._GetAbsDescription(a, cursor=cursor) return A security.declareProtected(ScoView, "ListeAbsNonJust") def ListeAbsNonJust(self, etudid, datedebut): "Liste des absences NON justifiees (par ordre chronologique)" cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) cursor.execute( """SELECT ETUDID, JOUR, MATIN FROM ABSENCES A WHERE A.ETUDID = %(etudid)s AND A.estabs AND A.jour >= %(datedebut)s EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B WHERE B.estjust AND B.ETUDID = %(etudid)s ORDER BY JOUR """, vars(), ) A = cursor.dictfetchall() for a in A: a["description"] = self._GetAbsDescription(a, cursor=cursor) return A security.declareProtected(ScoView, "ListeJustifs") def ListeJustifs(self, etudid, datedebut, datefin=None, only_no_abs=False): """Liste des justificatifs (sans absence relevée) à partir d'une date, ou, si datefin spécifié, entre deux dates. Si only_no_abs: seulement les justificatifs correspondant aux jours sans absences relevées. """ cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) req = """SELECT DISTINCT ETUDID, JOUR, MATIN FROM ABSENCES A WHERE A.ETUDID = %(etudid)s AND A.ESTJUST AND A.JOUR >= %(datedebut)s""" if datefin: req += """AND A.JOUR <= %(datefin)s""" if only_no_abs: req += """ EXCEPT SELECT ETUDID, JOUR, MATIN FROM ABSENCES B WHERE B.estabs AND B.ETUDID = %(etudid)s """ cursor.execute(req, vars()) A = cursor.dictfetchall() for a in A: a["description"] = self._GetAbsDescription(a, cursor=cursor) return A def _GetAbsDescription(self, a, cursor=None): "Description associee a l'absence" if not cursor: cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) a = a.copy() # a['jour'] = a['jour'].date() if a["matin"]: # devrait etre booleen... :-( a["matin"] = True else: a["matin"] = False cursor.execute( """select * from absences where etudid=%(etudid)s and jour=%(jour)s and matin=%(matin)s order by entry_date desc""", a, ) A = cursor.dictfetchall() desc = None module = "" for a in A: if a["description"]: desc = a["description"] if a["moduleimpl_id"] and a["moduleimpl_id"] != "NULL": # Trouver le nom du module Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( self.Notes, moduleimpl_id=a["moduleimpl_id"] ) if Mlist: M = Mlist[0] module += "%s " % M["module"]["code"] if desc: return "(%s) %s" % (desc, module) if module: return module return "" security.declareProtected(ScoView, "ListeAbsJour") def ListeAbsJour(self, date, am=True, pm=True, is_abs=True, is_just=None): """Liste des absences et/ou justificatifs ce jour. is_abs: None (peu importe), True, False is_just: idem """ cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) req = """SELECT DISTINCT etudid, jour, matin FROM ABSENCES A WHERE A.jour = %(date)s """ if is_abs != None: req += " AND A.estabs = %(is_abs)s" if is_just != None: req += " AND A.estjust = %(is_just)s" if not am: req += " AND NOT matin " if not pm: req += " AND matin" cursor.execute(req, {"date": date, "is_just": is_just, "is_abs": is_abs}) A = cursor.dictfetchall() for a in A: a["description"] = self._GetAbsDescription(a, cursor=cursor) return A security.declareProtected(ScoView, "ListeAbsNonJustJour") def ListeAbsNonJustJour(self, date, am=True, pm=True): "Liste des absences non justifiees ce jour" cnx = self.GetDBConnexion() cursor = cnx.cursor(cursor_factory=notesdb.ScoDocCursor) reqa = "" if not am: reqa += " AND NOT matin " if not pm: reqa += " AND matin " req = ( """SELECT etudid, jour, matin FROM ABSENCES A WHERE A.estabs AND A.jour = %(date)s """ + reqa + """EXCEPT SELECT etudid, jour, matin FROM ABSENCES B WHERE B.estjust AND B.jour = %(date)s""" + reqa ) cursor.execute(req, {"date": date}) A = cursor.dictfetchall() for a in A: a["description"] = self._GetAbsDescription(a, cursor=cursor) return A security.declareProtected(ScoAbsChange, "doSignaleAbsenceGrSemestre") def doSignaleAbsenceGrSemestre( self, moduleimpl_id=None, abslist=[], dates="", etudids="", destination=None, REQUEST=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. """ if etudids: etudids = etudids.split(",") else: etudids = [] if dates: dates = dates.split(",") else: dates = [] # 1- Efface les absences if dates: for etudid in etudids: self.AnnuleAbsencesDatesNoJust(etudid, dates, moduleimpl_id, REQUEST) return "Absences effacées" # 2- Ajoute les absences if abslist: self._add_abslist(abslist, REQUEST, moduleimpl_id) return "Absences ajoutées" return "" def _add_abslist(self, abslist, REQUEST, moduleimpl_id=None): for a in abslist: etudid, jour, ampm = a.split(":") if ampm == "am": matin = 1 elif ampm == "pm": matin = 0 else: raise ValueError("invalid ampm !") # ajoute abs si pas deja absent if self.CountAbs(etudid, jour, jour, matin, moduleimpl_id) == 0: self._AddAbsence(etudid, jour, matin, 0, REQUEST, "", moduleimpl_id) # --- Misc tools.... ------------------ def _isFarFutur(self, jour): # check si jour est dans le futur "lointain" # pour autoriser les saisies dans le futur mais pas a plus de 6 mois y, m, d = [int(x) for x in jour.split("-")] j = datetime.date(y, m, d) # 6 mois ~ 182 jours: return j - datetime.date.today() > datetime.timedelta(182) # ------------ HTML Interfaces security.declareProtected(ScoAbsChange, "SignaleAbsenceGrHebdo") def SignaleAbsenceGrHebdo( self, datelundi, group_ids=[], destination="", moduleimpl_id=None, REQUEST=None ): "Saisie hebdomadaire des absences" if not moduleimpl_id: moduleimpl_id = None groups_infos = sco_groups_view.DisplayedGroupsInfos( self, group_ids, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST ) if not groups_infos.members: return ( self.sco_header(page_title="Saisie des absences", REQUEST=REQUEST) + "
Saisie des absences %s %s, semaine du lundi %s
|
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é :
Matin | Après-midi | |
---|---|---|
%(nomprenom)s | """ % etud ) # """ 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_nbabsjustam += 1 else: H.append("Abs.") t_nbabspm += 1 else: H.append("") H.append(" |
%d abs, %d just. | %d abs, %d just. |
Aucune absence !
") else: H.append( """L'étudiant pense pouvoir justifier cette absence.
Vérifiez le justificatif avant d'enregistrer.
Supprimer ce billet (utiliser en cas d'erreur, par ex. billet en double)
""" % billet_id ) F += 'Liste de tous les billets en attente
' return "\n".join(H) + "