# -*- 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 # ############################################################################## """Fonctions sur les absences """ import calendar import datetime import html import time from app.scodoc import notesdb as ndb from app import log 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_cache from app.scodoc import sco_etud from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_preferences import app.scodoc.sco_utils as scu # --- 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("-")] try: j = datetime.date(y, m, d) except ValueError: # les dates erronées, genre année 20022, sont considéres dans le futur return True # 6 mois ~ 182 jours: return j - datetime.date.today() > datetime.timedelta(182) def _toboolean(x): "convert a value to boolean" return bool(x) def is_work_saturday(): "Vrai si le samedi est travaillé" return int(sco_preferences.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(object): """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 = date.split("/") elif fmt == "iso": self.year, self.month, self.day = date.split("-") else: raise ValueError("invalid format spec. (%s)" % fmt) self.year = int(self.year) self.month = int(self.month) self.day = int(self.day) except ValueError: 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_day(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_day((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): # #py3 TODO à supprimer """return a negative integer if self < other, zero if self == other, a positive integer if self > other""" return int(self.time - other.time) def __eq__(self, other): return self.time == other.time def __ne__(self, other): return self.time != other.time def __lt__(self, other): return self.time < other.time def __le__(self, other): return self.time <= other.time def __gt__(self, other): return self.time > other.time def __ge__(self, other): return self.time >= other.time def __hash__(self): "we are immutable !" return hash(self.time) ^ hash(str(self)) # d = ddmmyyyy( '21/12/99' ) def DateRangeISO(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() try: cur = ddmmyyyy(date_beg, work_saturday=work_saturday) end = ddmmyyyy(date_end, work_saturday=work_saturday) except (AttributeError, ValueError) as e: raise ScoValueError("date invalide !") from e while cur <= end: if (not workable) or cur.iswork(): r.append(cur) cur = cur.next_day() return [x.ISO() for x in r] def day_names(): """Returns week day names. If work_saturday property is set, include saturday """ if is_work_saturday(): return scu.DAY_NAMES[:-1] else: return scu.DAY_NAMES[:-2] def next_iso_day(date): "return date after date" d = ddmmyyyy(date, fmt="iso", work_saturday=is_work_saturday()) return d.next_day().ISO() def YearTable( 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(), 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 "\n".join(T) def list_abs_in_range( etudid, debut=None, fin=None, matin=None, moduleimpl_id=None, cursor=None ): """Liste des absences entre deux dates. Args: etudid: debut: string iso date ("2020-03-12") ou None end: string iso date ("2020-03-12") matin: None, True, False moduleimpl_id: restreint le comptage aux absences dans ce module Returns: List of absences """ if matin is not 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 + ( "" if debut is None else """ AND A.JOUR BETWEEN %(debut)s AND %(fin)s """ ), { "etudid": etudid, "debut": debut, "fin": fin, "matin": matin, "moduleimpl_id": moduleimpl_id, }, ) res = cursor.dictfetchall() return res def count_abs(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int: """compte le nombre d'absences Args: etudid: l'étudiant considéré debut: date, chaîne iso, eg "2021-06-15" fin: date de fin, incluse matin: True (compte les matinées), False (les après-midi), None (les deux) moduleimpl_id: restreint le comptage aux absences dans ce module. Returns: An integer. """ return len( list_abs_in_range(etudid, debut, fin, matin=matin, moduleimpl_id=moduleimpl_id) ) def count_abs_just(etudid, debut, fin, matin=None, moduleimpl_id=None) -> int: """compte le nombre d'absences justifiées Args: etudid: l'étudiant considéré debut: date, chaîne iso, eg "2021-06-15" fin: date de fin, incluse matin: True (compte les matinées), False (les après-midi), None (les deux) moduleimpl_id: restreint le comptage aux absences dans ce module. Returns: An integer. """ if matin is not 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 list_abs_date(etudid, beg_date=None, end_date=None): """Liste des absences et justifs entre deux dates (inclues).""" cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) req = """SELECT jour, matin, estabs, estjust, description FROM ABSENCES A WHERE A.ETUDID = %(etudid)s""" + ( "" if beg_date is None else """ AND A.jour >= %(beg_date)s AND A.jour <= %(end_date)s """ ) cursor.execute( req, vars(), ) absences = cursor.dictfetchall() # remove duplicates A = {} # { (jour, matin) : abs } for a in absences: 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 = list(A.values()) R.sort(key=lambda x: (x["begin"])) return R def _get_abs_description(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.moduleimpl_withmodule_list( moduleimpl_id=a["moduleimpl_id"] ) if Mlist: M = Mlist[0] module += "%s " % (M["module"]["code"] or "(module sans code)") if desc: return "(%s) %s" % (desc, module) if module: return module return "" def list_abs_jour(date, am=True, pm=True, is_abs=True, is_just=None) -> list[dict]: """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 is not None: req += " AND A.estabs = %(is_abs)s" if is_just is not 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"] = _get_abs_description(a, cursor=cursor) return A def list_abs_non_just_jour(date, am=True, pm=True) -> list[dict]: "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"] = _get_abs_description(a, cursor=cursor) return A def list_abs_non_just(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(), ) abs_list = cursor.dictfetchall() for a in abs_list: a["description"] = _get_abs_description(a, cursor=cursor) return abs_list def list_abs_just(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"] = _get_abs_description(a, cursor=cursor) return A def list_abs_justifs(etudid, datedebut, datefin=None, only_no_abs=False): """Liste des justificatifs (avec ou sans absence relevée) à partir d'une date, ou, si datefin spécifié, entre deux dates. Args: etudid: datedebut: date de début, iso, eg "2002-03-15" datefin: date de fin, incluse, eg "2002-03-15" only_no_abs: si vrai, seulement les justificatifs correspondant aux jours sans absences relevées. Returns: Liste de dict absences {'etudid': 'EID214', 'jour': datetime.date(2021, 1, 15), 'matin': True, 'description': '' } """ 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"] = _get_abs_description(a, cursor=cursor) return A def add_absence( etudid, jour, matin, estjust, description=None, moduleimpl_id=None, ): "Ajoute une absence dans la bd" if _isFarFutur(jour): raise ScoValueError("date absence erronée ou 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( 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() invalidate_abs_etud_date(etudid, jour) sco_abs_notification.abs_notify(etudid, jour) def add_justif(etudid, jour, matin, 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( cnx, "AddJustif", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), ) cnx.commit() invalidate_abs_etud_date(etudid, jour) def add_abslist(abslist, 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 count_abs(etudid, jour, jour, matin, moduleimpl_id) == 0: add_absence(etudid, jour, matin, 0, "", moduleimpl_id) def annule_absence(etudid, jour, matin, moduleimpl_id=None): """Annule une absence dans la base. N'efface pas l'éventuel justificatif. Args: etudid: jour: date, chaîne iso, eg "1999-12-31" matin: moduleimpl_id: si spécifié, n'annule que pour ce module. Returns: None """ # 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( cnx, "AnnuleAbsence", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s,moduleimpl_id=%(moduleimpl_id)s" % vars(), ) cnx.commit() invalidate_abs_etud_date(etudid, jour) def annule_justif(etudid, jour, matin): "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( cnx, "AnnuleJustif", etudid=etudid, msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), ) cnx.commit() invalidate_abs_etud_date(etudid, jour) # ------ 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 is not 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"' % html.escape(descr, quote=True) if href or descr: cc.append("<a %s %s>" % (href, descr)) if legend or d == 1: if pad_width is not 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 = "".join(cc) if day == "D": monday = monday.next_day(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_day(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 (True, False): 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] is not 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 is not 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"' % html.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("".join(cc) + "</tr>") return "\n".join(T) # -------------------------------------------------------------------- # # Cache absences # # On cache (via REDIS ou autre, voir sco_cache.py) les _nombres_ d'absences # (justifiées et non justifiées) de chaque etudiant dans un semestre donné. # Le cache peut être invalidé soit par étudiant/semestre, soit pour tous # les étudiant d'un semestre. # # On ne cache pas la liste des absences car elle est rarement utilisée (calendrier, # absences à une date donnée). # # -------------------------------------------------------------------- def get_abs_count(etudid, sem): """Les comptes d'absences de cet étudiant dans ce semestre: tuple (nb abs non justifiées, nb abs justifiées) Utilise un cache. """ return get_abs_count_in_interval(etudid, sem["date_debut_iso"], sem["date_fin_iso"]) def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): """Les comptes d'absences de cet étudiant entre ces deux dates, incluses: tuple (nb abs, nb abs justifiées) Utilise un cache. """ key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso r = sco_cache.AbsSemEtudCache.get(key) if not r: nb_abs = count_abs( etudid=etudid, debut=date_debut_iso, fin=date_fin_iso, ) nb_abs_just = count_abs_just( etudid=etudid, debut=date_debut_iso, fin=date_fin_iso, ) r = (nb_abs, nb_abs_just) ans = sco_cache.AbsSemEtudCache.set(key, r) if not ans: log("warning: get_abs_count failed to cache") return r def invalidate_abs_count(etudid, sem): """Invalidate (clear) cached counts""" date_debut = sem["date_debut_iso"] date_fin = sem["date_fin_iso"] key = str(etudid) + "_" + date_debut + "_" + date_fin sco_cache.AbsSemEtudCache.delete(key) def invalidate_abs_count_sem(sem): """Invalidate (clear) cached abs counts for all the students of this semestre""" inscriptions = ( sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( sem["formsemestre_id"] ) ) for ins in inscriptions: invalidate_abs_count(ins["etudid"], sem) def invalidate_abs_etud_date(etudid, date): # was invalidateAbsEtudDate """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. Invalide cache absence et caches semestre date: date au format ISO """ from app.scodoc 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 absences: for sem in sems: # Inval cache bulletin et/ou note_table if sco_compute_moy.formsemestre_expressions_use_abscounts( sem["formsemestre_id"] ): # certaines formules utilisent les absences pdfonly = False else: # efface toujours le PDF car il affiche en général les absences pdfonly = True sco_cache.invalidate_formsemestre( formsemestre_id=sem["formsemestre_id"], pdfonly=pdfonly ) # Inval cache compteurs absences: invalidate_abs_count_sem(sem)