ScoDoc-PE/app/scodoc/sco_abs.py

1110 lines
33 KiB
Python
Executable File

# -*- 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):
"""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):
"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&eacute;vrier",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Aout",
"Septembre",
"Octobre",
"Novembre",
"D&eacute;cembre",
)
MONTHNAMES_ABREV = (
"Jan.",
"F&eacute;v.",
"Mars",
"Avr.",
"Mai&nbsp;",
"Juin",
"Juil",
"Aout",
"Sept",
"Oct.",
"Nov.",
"D&eacute;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 = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;" # 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 = (
"&nbsp;" * (n // 2) + legend + "&nbsp;" * ((n + 1) // 2)
)
else:
legend = "&nbsp;&nbsp;&nbsp;" # 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)