# -*- 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 time import datetime import calendar import cgi from scodoc_manager import sco_mgr from app.scodoc import notesdb as ndb from app.scodoc.scolog import logdb from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidDateError from app.scodoc import sco_abs_notification from app.scodoc import sco_core from app.scodoc import sco_etud from app.scodoc import sco_formsemestre from app.scodoc import sco_preferences # --- Misc tools.... ------------------ def _isFarFutur(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) def _toboolean(x): "convert a value to boolean" return x # not necessary anymore ! def is_work_saturday(context): "Vrai si le samedi est travaillé" return int(sco_preferences.get_preference(context, "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") def list_abs_in_range( context, etudid, debut, fin, matin=None, moduleimpl_id=None, cursor=None ): """Liste des absences entre deux dates. Args: etudid debut string iso date ("2020-03-12") end string iso date ("2020-03-12") matin None, True, False moduleimpl_id """ 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 = "" if not cursor: cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """ 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 """, {"etudid": etudid, "debut": debut, "fin": fin, "moduleimpl_id": moduleimpl_id}, ) res = cursor.dictfetchall() return res def CountAbs(context, etudid, debut, fin, matin=None, moduleimpl_id=None): """CountAbs matin= 1 ou 0. Returns: An integer. """ return len( list_abs_in_range( context, etudid, debut, fin, matin=matin, moduleimpl_id=moduleimpl_id ) ) def CountAbsJust(context, 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 = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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(context, etudid, beg_date, end_date): """Liste des absences et justifs entre deux dates (inclues).""" cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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 def GetAbsDescription(context, a, cursor=None): "Description associee a l'absence" from app.scodoc import sco_moduleimpl if not cursor: cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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( context, 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 "" def ListeAbsJour(context, 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 = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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"] = GetAbsDescription(context, a, cursor=cursor) return A def ListeAbsNonJustJour(context, date, am=True, pm=True): "Liste des absences non justifiees ce jour" cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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"] = GetAbsDescription(context, a, cursor=cursor) return A def ListeAbsNonJust(context, etudid, datedebut): "Liste des absences NON justifiees (par ordre chronologique)" cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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"] = GetAbsDescription(context, a, cursor=cursor) return A def ListeAbsJust(context, etudid, datedebut): "Liste des absences justifiees (par ordre chronologique)" cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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"] = GetAbsDescription(context, a, cursor=cursor) return A def ListeJustifs(context, 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 = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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"] = GetAbsDescription(context, a, cursor=cursor) return A def add_absence( context, etudid, jour, matin, estjust, REQUEST, description=None, moduleimpl_id=None, ): "Ajoute une absence dans la bd" if _isFarFutur(jour): raise ScoValueError("date absence trop loin dans le futur !") estjust = _toboolean(estjust) matin = _toboolean(matin) cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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() invalidateAbsEtudDate(context, etudid, jour) sco_abs_notification.abs_notify(context, etudid, jour) def add_justif(context, etudid, jour, matin, REQUEST, description=None): "Ajoute un justificatif dans la base" # unpublished if _isFarFutur(jour): raise ScoValueError("date justificatif trop loin dans le futur !") matin = _toboolean(matin) cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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() invalidateAbsEtudDate(context, etudid, jour) def _add_abslist(context, 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 CountAbs(context, etudid, jour, jour, matin, moduleimpl_id) == 0: add_absence(context, etudid, jour, matin, 0, REQUEST, "", moduleimpl_id) def annule_absence(context, 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 = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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() invalidateAbsEtudDate(context, etudid, jour) def annule_justif(context, etudid, jour, matin, REQUEST=None): "Annule un justificatif" # unpublished matin = _toboolean(matin) cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.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() invalidateAbsEtudDate(context, etudid, jour) # ---- BILLETS _billet_absenceEditor = ndb.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"] sco_core.get_notes_cache(context).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 = ndb.DateDMYtoISO(self.sem["date_debut"]) fin_sem = ndb.DateDMYtoISO(self.sem["date_fin"]) self._CountAbs = CountAbs( self.context, etudid=self.etudid, debut=debut_sem, fin=fin_sem ) self._CountAbsJust = CountAbsJust( self.context, 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 = sco_mgr.get_db_uri() # 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 """ import sco_compute_moy # Semestres a cette date: etud = sco_etud.get_etud_info(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 ) sco_core.inval_cache( context, pdfonly=pdfonly, formsemestre_id=sem["formsemestre_id"] ) # Inval cache compteurs absences: AbsSemEtuds = getAbsSemEtuds(context, sem) if etudid in AbsSemEtuds: AbsSemEtuds[etudid].invalidate()