# -*- 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 # ############################################################################## """Fonctions sur les absences """ # Anciennement dans ZAbscences.py, séparé pour migration import string import datetime import re import time import calendar import cgi import notesdb from sco_exceptions import ScoValueError, ScoInvalidDateError import sco_formsemestre import sco_compute_moy def is_work_saturday(context): "Vrai si le samedi est travaillé" return int(context.get_preference("work_saturday")) def MonthNbDays(month, year): "returns nb of days in month" if month > 7: month = month + 1 if month % 2: return 31 elif month == 2: if calendar.isleap(year): return 29 else: return 28 else: return 30 class ddmmyyyy: """immutable dates""" def __init__(self, date=None, fmt="ddmmyyyy", work_saturday=False): self.work_saturday = work_saturday if date is None: return try: if fmt == "ddmmyyyy": self.day, self.month, self.year = string.split(date, "/") elif fmt == "iso": self.year, self.month, self.day = string.split(date, "-") else: raise ValueError("invalid format spec. (%s)" % fmt) self.year = string.atoi(self.year) self.month = string.atoi(self.month) self.day = string.atoi(self.day) except: raise ScoValueError("date invalide: %s" % date) # accept years YYYY or YY, uses 1970 as pivot if self.year < 1970: if self.year > 100: raise ScoInvalidDateError("Année invalide: %s" % self.year) if self.year < 70: self.year = self.year + 2000 else: self.year = self.year + 1900 if self.month < 1 or self.month > 12: raise ScoInvalidDateError("Mois invalide: %s" % self.month) if self.day < 1 or self.day > MonthNbDays(self.month, self.year): raise ScoInvalidDateError("Jour invalide: %s" % self.day) # weekday in 0-6, where 0 is monday self.weekday = calendar.weekday(self.year, self.month, self.day) self.time = time.mktime((self.year, self.month, self.day, 0, 0, 0, 0, 0, 0)) def iswork(self): "returns true if workable day" if self.work_saturday: nbdays = 6 else: nbdays = 5 if ( self.weekday >= 0 and self.weekday < nbdays ): # monday-friday or monday-saturday return 1 else: return 0 def __repr__(self): return "'%02d/%02d/%04d'" % (self.day, self.month, self.year) def __str__(self): return "%02d/%02d/%04d" % (self.day, self.month, self.year) def ISO(self): "iso8601 representation of the date" return "%04d-%02d-%02d" % (self.year, self.month, self.day) def next(self, days=1): "date for the next day (nota: may be a non workable day)" day = self.day + days month = self.month year = self.year while day > MonthNbDays(month, year): day = day - MonthNbDays(month, year) month = month + 1 if month > 12: month = 1 year = year + 1 return self.__class__( "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday ) def prev(self, days=1): "date for previous day" day = self.day - days month = self.month year = self.year while day <= 0: month = month - 1 if month == 0: month = 12 year = year - 1 day = day + MonthNbDays(month, year) return self.__class__( "%02d/%02d/%04d" % (day, month, year), work_saturday=self.work_saturday ) def next_monday(self): "date of next monday" return self.next((7 - self.weekday) % 7) def prev_monday(self): "date of last monday, but on sunday, pick next monday" if self.weekday == 6: return self.next_monday() else: return self.prev(self.weekday) def __cmp__(self, other): """return a negative integer if self < other, zero if self == other, a positive integer if self > other""" return int(self.time - other.time) def __hash__(self): "we are immutable !" return hash(self.time) ^ hash(str(self)) # d = ddmmyyyy( '21/12/99' ) def DateRangeISO(context, date_beg, date_end, workable=1): """returns list of dates in [date_beg,date_end] workable = 1 => keeps only workable days""" if not date_beg: raise ScoValueError("pas de date spécifiée !") if not date_end: date_end = date_beg r = [] work_saturday = is_work_saturday(context) cur = ddmmyyyy(date_beg, work_saturday=work_saturday) end = ddmmyyyy(date_end, work_saturday=work_saturday) while cur <= end: if (not workable) or cur.iswork(): r.append(cur) cur = cur.next() return map(lambda x: x.ISO(), r) def day_names(context): """Returns week day names. If work_saturday property is set, include saturday """ if is_work_saturday(context): return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"] else: return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"] def next_iso_day(context, date): "return date after date" d = ddmmyyyy(date, fmt="iso", work_saturday=is_work_saturday(context)) return d.next().ISO() def YearTable( context, year, events=[], firstmonth=9, lastmonth=7, halfday=0, dayattributes="", pad_width=8, ): """Generate a calendar table events = list of tuples (date, text, color, href [,halfday]) where date is a string in ISO format (yyyy-mm-dd) halfday is boolean (true: morning, false: afternoon) text = text to put in calendar (must be short, 1-5 cars) (optional) if halfday, generate 2 cells per day (morning, afternoon) """ T = [ '<table id="maincalendar" class="maincalendar" border="3" cellpadding="1" cellspacing="1" frame="box">' ] T.append("<tr>") month = firstmonth while 1: T.append('<td valign="top">') T.append(MonthTableHead(month)) T.append( MonthTableBody( month, year, events, halfday, dayattributes, is_work_saturday(context), pad_width=pad_width, ) ) T.append(MonthTableTail()) T.append("</td>") if month == lastmonth: break month = month + 1 if month > 12: month = 1 year = year + 1 T.append("</table>") return string.join(T, "\n") # ---- BILLETS _billet_absenceEditor = notesdb.EditableTable( "billet_absence", "billet_id", ( "billet_id", "etudid", "abs_begin", "abs_end", "description", "etat", "entry_date", "justified", ), sortkey="entry_date desc", ) billet_absence_create = _billet_absenceEditor.create billet_absence_delete = _billet_absenceEditor.delete billet_absence_list = _billet_absenceEditor.list billet_absence_edit = _billet_absenceEditor.edit # ------ HTML Calendar functions (see YearTable function) # MONTH/DAY NAMES: MONTHNAMES = ( "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Aout", "Septembre", "Octobre", "Novembre", "Décembre", ) MONTHNAMES_ABREV = ( "Jan.", "Fév.", "Mars", "Avr.", "Mai ", "Juin", "Juil", "Aout", "Sept", "Oct.", "Nov.", "Déc.", ) DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D") # COLORS: WHITE = "#FFFFFF" GRAY1 = "#EEEEEE" GREEN3 = "#99CC99" WEEKDAYCOLOR = GRAY1 WEEKENDCOLOR = GREEN3 def MonthTableHead(month): color = WHITE return """<table class="monthcalendar" border="0" cellpadding="0" cellspacing="0" frame="box"> <tr bgcolor="%s"><td class="calcol" colspan="2" align="center">%s</td></tr>\n""" % ( color, MONTHNAMES_ABREV[month - 1], ) def MonthTableTail(): return "</table>\n" def MonthTableBody( month, year, events=[], halfday=0, trattributes="", work_saturday=False, pad_width=8 ): firstday, nbdays = calendar.monthrange(year, month) localtime = time.localtime() current_weeknum = time.strftime("%U", localtime) current_year = localtime[0] T = [] # cherche date du lundi de la 1ere semaine de ce mois monday = ddmmyyyy("1/%d/%d" % (month, year)) while monday.weekday != 0: monday = monday.prev() if work_saturday: weekend = ("D",) else: weekend = ("S", "D") if not halfday: for d in range(1, nbdays + 1): weeknum = time.strftime( "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y") ) day = DAYNAMES_ABREV[(firstday + d - 1) % 7] if day in weekend: bgcolor = WEEKENDCOLOR weekclass = "wkend" attrs = "" else: bgcolor = WEEKDAYCOLOR weekclass = "wk" + str(monday).replace("/", "_") attrs = trattributes color = None legend = "" href = "" descr = "" # event this day ? # each event is a tuple (date, text, color, href) # where date is a string in ISO format (yyyy-mm-dd) for ev in events: ev_year = int(ev[0][:4]) ev_month = int(ev[0][5:7]) ev_day = int(ev[0][8:10]) if year == ev_year and month == ev_month and ev_day == d: if ev[1]: legend = ev[1] if ev[2]: color = ev[2] if ev[3]: href = ev[3] if len(ev) > 4 and ev[4]: descr = ev[4] # cc = [] if color != None: cc.append('<td bgcolor="%s" class="calcell">' % color) else: cc.append('<td class="calcell">') if href: href = 'href="%s"' % href if descr: descr = 'title="%s"' % cgi.escape(descr, quote=True) if href or descr: cc.append("<a %s %s>" % (href, descr)) if legend or d == 1: if pad_width != None: n = pad_width - len(legend) # pad to 8 cars if n > 0: legend = " " * (n / 2) + legend + " " * ((n + 1) / 2) else: legend = " " # empty cell cc.append(legend) if href or descr: cc.append("</a>") cc.append("</td>") cell = string.join(cc, "") if day == "D": monday = monday.next(7) if ( weeknum == current_weeknum and current_year == year and weekclass != "wkend" ): weekclass += " currentweek" T.append( '<tr bgcolor="%s" class="%s" %s><td class="calday">%d%s</td>%s</tr>' % (bgcolor, weekclass, attrs, d, day, cell) ) else: # Calendar with 2 cells / day for d in range(1, nbdays + 1): weeknum = time.strftime( "%U", time.strptime("%d/%d/%d" % (d, month, year), "%d/%m/%Y") ) day = DAYNAMES_ABREV[(firstday + d - 1) % 7] if day in weekend: bgcolor = WEEKENDCOLOR weekclass = "wkend" attrs = "" else: bgcolor = WEEKDAYCOLOR weekclass = "wk" + str(monday).replace("/", "_") attrs = trattributes if ( weeknum == current_weeknum and current_year == year and weekclass != "wkend" ): weeknum += " currentweek" if day == "D": monday = monday.next(7) T.append( '<tr bgcolor="%s" class="wk%s" %s><td class="calday">%d%s</td>' % (bgcolor, weekclass, attrs, d, day) ) cc = [] for morning in (1, 0): color = None legend = "" href = "" descr = "" for ev in events: ev_year = int(ev[0][:4]) ev_month = int(ev[0][5:7]) ev_day = int(ev[0][8:10]) if ev[4] != None: ev_half = int(ev[4]) else: ev_half = 0 if ( year == ev_year and month == ev_month and ev_day == d and morning == ev_half ): if ev[1]: legend = ev[1] if ev[2]: color = ev[2] if ev[3]: href = ev[3] if len(ev) > 5 and ev[5]: descr = ev[5] # if color != None: cc.append('<td bgcolor="%s" class="calcell">' % (color)) else: cc.append('<td class="calcell">') if href: href = 'href="%s"' % href if descr: descr = 'title="%s"' % cgi.escape(descr, quote=True) if href or descr: cc.append("<a %s %s>" % (href, descr)) if legend or d == 1: n = 3 - len(legend) # pad to 3 cars if n > 0: legend = " " * (n / 2) + legend + " " * ((n + 1) / 2) else: legend = " " # empty cell cc.append(legend) if href or descr: cc.append("</a>") cc.append("</td>\n") T.append(string.join(cc, "") + "</tr>") return string.join(T, "\n") # -------------------------------------------------------------------- # # Cache absences # # On cache simplement (à la demande) le nombre d'absences de chaque etudiant # dans un semestre donné. # Toute modification du semestre (invalidation) invalide le cache # (simple mécanisme de "listener" sur le cache de semestres) # Toute modification des absences d'un étudiant invalide les caches des semestres # concernés à cette date (en général un seul semestre) # # On ne cache pas la liste des absences car elle est rarement utilisée (calendrier, # absences à une date donnée). # # -------------------------------------------------------------------- class CAbsSemEtud: """Comptes d'absences d'un etudiant dans un semestre""" def __init__(self, context, sem, etudid): self.context = context self.sem = sem self.etudid = etudid self._loaded = False formsemestre_id = sem["formsemestre_id"] context.Notes._getNotesCache().add_listener( self.invalidate, formsemestre_id, (etudid, formsemestre_id) ) def CountAbs(self): if not self._loaded: self.load() return self._CountAbs def CountAbsJust(self): if not self._loaded: self.load() return self._CountAbsJust def load(self): "Load state from DB" # log('loading CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id'])) # Reload sem, it may have changed self.sem = sco_formsemestre.get_formsemestre( self.context, self.sem["formsemestre_id"] ) debut_sem = notesdb.DateDMYtoISO(self.sem["date_debut"]) fin_sem = notesdb.DateDMYtoISO(self.sem["date_fin"]) self._CountAbs = self.context.Absences.CountAbs( etudid=self.etudid, debut=debut_sem, fin=fin_sem ) self._CountAbsJust = self.context.Absences.CountAbsJust( etudid=self.etudid, debut=debut_sem, fin=fin_sem ) self._loaded = True def invalidate(self, args=None): "Notify me that DB has been modified" # log('invalidate CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id'])) self._loaded = False # Accès au cache des absences ABS_CACHE_INST = {} # { DeptId : { formsemestre_id : { etudid : CAbsEtudSem } } } def getAbsSemEtud(context, sem, etudid): AbsSemEtuds = getAbsSemEtuds(context, sem) if not etudid in AbsSemEtuds: AbsSemEtuds[etudid] = CAbsSemEtud(context, sem, etudid) return AbsSemEtuds[etudid] def getAbsSemEtuds(context, sem): u = context.GetDBConnexionString() # identifie le dept de facon fiable if not u in ABS_CACHE_INST: ABS_CACHE_INST[u] = {} C = ABS_CACHE_INST[u] if sem["formsemestre_id"] not in C: C[sem["formsemestre_id"]] = {} return C[sem["formsemestre_id"]] def invalidateAbsEtudDate(context, etudid, date): """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. Invalide cache absence et PDF bulletins si nécessaire. date: date au format ISO """ # Semestres a cette date: etud = context.getEtudInfo(etudid=etudid, filled=True)[0] sems = [ sem for sem in etud["sems"] if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date ] # Invalide les PDF et les abscences: for sem in sems: # Inval cache bulletin et/ou note_table if sco_compute_moy.formsemestre_expressions_use_abscounts( context, sem["formsemestre_id"] ): pdfonly = False # seules certaines formules utilisent les absences else: pdfonly = ( True # efface toujours le PDF car il affiche en général les absences ) context.Notes._inval_cache( pdfonly=pdfonly, formsemestre_id=sem["formsemestre_id"] ) # Inval cache compteurs absences: AbsSemEtuds = getAbsSemEtuds(context, sem) if etudid in AbsSemEtuds: AbsSemEtuds[etudid].invalidate()