1
0
forked from ScoDoc/ScoDoc
ScoDoc-Front/app/scodoc/sco_abs.py

1122 lines
34 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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 string
import time
import types
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
from app.scodoc import sco_formsemestre_inscriptions
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("-")]
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 ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"]
else:
return ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi"]
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, 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: restreint le comptage aux absences dans ce module
Returns:
List of absences
"""
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,
"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 != 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, 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 = 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 != 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"] = _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)
# ---- 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",
input_formators={
"etat": bool,
"justified": bool,
},
)
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&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 != 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 != 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] != 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"' % 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)