From 3afec00b5e86ddb46a49ab36a914036a5532415c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sun, 10 Jan 2021 18:05:20 +0100 Subject: [PATCH 1/2] Small fixes --- sco_bulletins_generator.py | 42 +++++++++++++++------ sco_report.py | 77 +++++++++++++++++++++----------------- 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/sco_bulletins_generator.py b/sco_bulletins_generator.py index 717cd9be1f..691059746d 100644 --- a/sco_bulletins_generator.py +++ b/sco_bulletins_generator.py @@ -42,12 +42,22 @@ La préférence 'bul_pdf_class_name' est obsolete (inutilisée). """ +import time +import cStringIO import collections +import traceback +import reportlab +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak +from reportlab.platypus import Table, TableStyle, Image, KeepInFrame +import sco_utils +import VERSION +from sco_exceptions import NoteProcessError import sco_preferences from notes_log import log import sco_formsemestre -from sco_pdf import * +import sco_pdf +from sco_pdf import PDFLOCK BULLETIN_CLASSES = ( collections.OrderedDict() @@ -145,7 +155,7 @@ class BulletinGenerator: dt, self.infos["etud"]["nom"], ) - filename = unescape_html(filename).replace(" ", "_").replace("&", "") + filename = sco_utils.unescape_html(filename).replace(" ", "_").replace("&", "") return filename def generate(self, format="", stand_alone=True): @@ -166,8 +176,10 @@ class BulletinGenerator: def generate_html(self): """Return bulletin as an HTML string""" H = ['
'] - H.append(self.bul_table(format="html")) # table des notes - H.append(self.bul_part_below(format="html")) # infos sous la table + # table des notes: + H.append(self.bul_table(format="html")) # pylint: disable=no-member + # infos sous la table: + H.append(self.bul_part_below(format="html")) # pylint: disable=no-member H.append("
") return "\n".join(H) @@ -179,10 +191,14 @@ class BulletinGenerator: """ formsemestre_id = self.infos["formsemestre_id"] - objects = self.bul_title_pdf() # partie haute du bulletin - objects += self.bul_table(format="pdf") # table des notes - objects += self.bul_part_below(format="pdf") # infos sous la table - objects += self.bul_signatures_pdf() # signatures + # partie haute du bulletin + objects = self.bul_title_pdf() # pylint: disable=no-member + # table des notes + objects += self.bul_table(format="pdf") # pylint: disable=no-member + # infos sous la table + objects += self.bul_part_below(format="pdf") # pylint: disable=no-member + # signatures + objects += self.bul_signatures_pdf() # pylint: disable=no-member # Réduit sur une page objects = [KeepInFrame(0, 0, objects, mode="shrink")] @@ -194,13 +210,13 @@ class BulletinGenerator: # Generation du document PDF sem = sco_formsemestre.get_formsemestre(self.context, formsemestre_id) report = cStringIO.StringIO() # in-memory document, no disk file - document = BaseDocTemplate(report) + document = sco_pdf.BaseDocTemplate(report) document.addPageTemplates( - ScolarsPageTemplate( + sco_pdf.ScolarsPageTemplate( document, context=self.context, author="%s %s (E. Viennet) [%s]" - % (SCONAME, SCOVERSION, self.description), + % (VERSION.SCONAME, VERSION.SCOVERSION, self.description), title="Bulletin %s de %s" % (sem["titremois"], self.infos["etud"]["nomprenom"]), subject="Bulletin de note", @@ -222,7 +238,9 @@ class BulletinGenerator: """ try: # put each table cell in a Paragraph - Pt = [[Paragraph(SU(x), self.CellStyle) for x in line] for line in P] + Pt = [ + [Paragraph(sco_pdf.SU(x), self.CellStyle) for x in line] for line in P + ] except: # enquête sur exception intermittente... log("*** bug in PDF buildTableObject:") diff --git a/sco_report.py b/sco_report.py index 4e1d13ee56..3f4a69b035 100644 --- a/sco_report.py +++ b/sco_report.py @@ -29,16 +29,22 @@ - statistiques decisions - suivi cohortes """ +import os +import tempfile +import urllib +import re +import time +import mx import mx.DateTime from mx.DateTime import DateTime as mxDateTime -import tempfile, urllib, re - -from notesdb import * -from sco_utils import * +import sco_utils as scu +import VERSION from notes_log import log from gen_tables import GenTable import sco_excel, sco_pdf +from notesdb import DateDMYtoISO +from sco_exceptions import ScoValueError import sco_codes_parcours from sco_codes_parcours import code_semestre_validant import sco_parcours_dut @@ -129,7 +135,7 @@ def _results_by_category( if Count.has_key(etud[category]): Count[etud[category]][etud[result]] += 1 else: - Count[etud[category]] = DictDefault(kv_dict={etud[result]: 1}) + Count[etud[category]] = scu.DictDefault(kv_dict={etud[result]: 1}) # conversion en liste de dict C = [Count[cat] for cat in categories] # Totaux par lignes et colonnes @@ -214,9 +220,9 @@ def formsemestre_report( formsemestre_id=formsemestre_id, ) # - tab.filename = make_filename("stats " + sem["titreannee"]) + tab.filename = scu.make_filename("stats " + sem["titreannee"]) - tab.origin = "Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "" + tab.origin = "Généré par %s le " % VERSION.SCONAME + scu.timedate_human_repr() + "" tab.caption = "Répartition des résultats par %s, semestre %s" % ( category_name, sem["titreannee"], @@ -259,7 +265,7 @@ def formsemestre_report_counts( Tableau comptage avec choix des categories """ sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) - category_name = strcapitalize(category) + category_name = scu.strcapitalize(category) title = "Comptages " + category_name etuds = formsemestre_etuds_stats(context, sem, only_primo=only_primo) tab = formsemestre_report( @@ -484,7 +490,7 @@ def table_suivi_cohorte( indices_sems.sort() for p in P: p.nb_etuds = 0 # nombre total d'etudiants dans la periode - p.sems_by_id = DictDefault(defaultvalue=[]) + p.sems_by_id = scu.DictDefault(defaultvalue=[]) for s in p.sems: p.sems_by_id[s["semestre_id"]].append(s) p.nb_etuds += len(s["members"]) @@ -623,8 +629,8 @@ def table_suivi_cohorte( rows=L, html_col_width="4em", html_sortable=True, - filename=make_filename("cohorte " + sem["titreannee"]), - origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "", + filename=scu.make_filename("cohorte " + sem["titreannee"]), + origin="Généré par %s le " % VERSION.SCONAME + scu.timedate_human_repr() + "", caption="Suivi cohorte " + pp + sem["titreannee"] + dbac, page_title="Suivi cohorte " + sem["titreannee"], html_class="table_cohorte", @@ -666,7 +672,6 @@ def formsemestre_suivi_cohorte( ): """Affiche suivi cohortes par numero de semestre""" percent = int(percent) - sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) tab, expl, bacs, bacspecialites, annee_bacs, sexes, statuts = table_suivi_cohorte( context, formsemestre_id, @@ -1049,7 +1054,7 @@ def table_suivi_parcours( etuds, bacs, bacspecialites, annee_bacs, sexes, statuts = tsp_etud_list( context, formsemestre_id, only_primo=only_primo ) - codes_etuds = DictDefault(defaultvalue=[]) + codes_etuds = scu.DictDefault(defaultvalue=[]) for etud in etuds: etud["codeparcours"], etud["decisions_jury"] = get_codeparcoursetud( context, etud @@ -1105,7 +1110,7 @@ def table_suivi_parcours( columns_ids=columns_ids, rows=L, titles=titles, - origin="Généré par %s le " % VERSION.SCONAME + timedate_human_repr() + "", + origin="Généré par %s le " % VERSION.SCONAME + scu.timedate_human_repr() + "", caption="Parcours suivis, étudiants %s semestre " % primostr + sem["titreannee"], page_title="Parcours " + sem["titreannee"], @@ -1164,7 +1169,6 @@ def formsemestre_suivi_parcours( REQUEST=None, ): """Effectifs dans les differents parcours possibles.""" - sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) tab = table_suivi_parcours( context, formsemestre_id, @@ -1211,9 +1215,8 @@ def graph_parcours( statut="", ): """""" - if not WITH_PYDOT: + if not scu.WITH_PYDOT: raise ScoValueError("pydot module is not installed") - sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) etuds, bacs, bacspecialites, annee_bacs, sexes, statuts = tsp_etud_list( context, formsemestre_id, @@ -1227,12 +1230,12 @@ def graph_parcours( # log('graph_parcours: %s etuds (only_primo=%s)' % (len(etuds), only_primo)) if not etuds: return "", etuds, bacs, bacspecialites, annee_bacs, sexes, statuts - edges = DictDefault( + edges = scu.DictDefault( defaultvalue=set() ) # {(formsemestre_id_origin, formsemestre_id_dest) : etud_set} sems = {} - effectifs = DictDefault(defaultvalue=set()) # formsemestre_id : etud_set - decisions = DictDefault(defaultvalue={}) # formsemestre_id : { code : nb_etud } + effectifs = scu.DictDefault(defaultvalue=set()) # formsemestre_id : etud_set + decisions = scu.DictDefault(defaultvalue={}) # formsemestre_id : { code : nb_etud } isolated_nodes = [] connected_nodes = set() diploma_nodes = [] @@ -1279,7 +1282,11 @@ def graph_parcours( dem_nodes[s["formsemestre_id"]] = nid edges[(s["formsemestre_id"], nid)].add(etudid) # ajout noeud pour NAR (seulement pour noeud de depart) - if s["formsemestre_id"] == formsemestre_id and dec and dec["code"] == NAR: + if ( + s["formsemestre_id"] == formsemestre_id + and dec + and dec["code"] == sco_codes_parcours.NAR + ): nid = "_nar_" + s["formsemestre_id"] nar_nodes[s["formsemestre_id"]] = nid edges[(s["formsemestre_id"], nid)].add(etudid) @@ -1295,10 +1302,10 @@ def graph_parcours( edges[(s["formsemestre_id"], nid)].add(etudid) diploma_nodes.append(nid) # - g = pydot.graph_from_edges(edges.keys()) + g = scu.pydot.graph_from_edges(edges.keys()) for fid in isolated_nodes: if not fid in connected_nodes: - n = pydot.Node(name=fid) + n = scu.pydot.Node(name=fid) g.add_node(n) g.set("rankdir", "LR") # left to right g.set_fontname("Helvetica") @@ -1306,7 +1313,7 @@ def graph_parcours( g.set_bgcolor("#fffff0") # ou 'transparent' # titres des semestres: for s in sems.values(): - n = pydot_get_node(g, s["formsemestre_id"]) + n = scu.pydot_get_node(g, s["formsemestre_id"]) log("s['formsemestre_id'] = %s" % s["formsemestre_id"]) log("n=%s" % n) log("get=%s" % g.get_node(s["formsemestre_id"])) @@ -1324,31 +1331,31 @@ def graph_parcours( s["annee_fin"][2:], len(effectifs[s["formsemestre_id"]]), ) - n.set("label", suppress_accents(label)) + n.set("label", scu.suppress_accents(label)) n.set_fontname("Helvetica") n.set_fontsize(8.0) n.set_width(1.2) n.set_shape("box") n.set_URL("formsemestre_status?formsemestre_id=" + s["formsemestre_id"]) # semestre de depart en vert - n = pydot_get_node(g, formsemestre_id) + n = scu.pydot_get_node(g, formsemestre_id) n.set_color("green") # demissions en rouge, octagonal for nid in dem_nodes.values(): - n = pydot_get_node(g, nid) + n = scu.pydot_get_node(g, nid) n.set_color("red") n.set_shape("octagon") n.set("label", "Dem.") # NAR en rouge, Mcircle for nid in nar_nodes.values(): - n = pydot_get_node(g, nid) + n = scu.pydot_get_node(g, nid) n.set_color("red") n.set_shape("Mcircle") - n.set("label", NAR) + n.set("label", sco_codes_parcours.NAR) # diplomes: for nid in diploma_nodes: - n = pydot_get_node(g, nid) + n = scu.pydot_get_node(g, nid) n.set_color("red") n.set_shape("ellipse") n.set("label", "Diplome") # bug si accent (pas compris pourquoi) @@ -1367,7 +1374,7 @@ def graph_parcours( bubbles[src_id + ":" + dst_id] = etud_descr e.set_URL("__xxxetudlist__?" + src_id + ":" + dst_id) # Genere graphe - f, path = tempfile.mkstemp(".gr") + _, path = tempfile.mkstemp(".gr") g.write(path=path, format=format) data = open(path, "r").read() log("dot generated %d bytes in %s format" % (len(data), format)) @@ -1403,7 +1410,7 @@ def graph_parcours( ) return ( ' Date: Sun, 10 Jan 2021 18:54:39 +0100 Subject: [PATCH 2/2] code refactoring: sco_abs --- ZAbsences.py | 602 ++------------------------------- ZEntreprises.py | 86 ++--- ZScolar.py | 17 +- html_sidebar.py | 2 +- sco_abs.py | 595 ++++++++++++++++++++++++++++++++ sco_abs_views.py | 4 +- sco_bulletins.py | 4 +- sco_bulletins_json.py | 4 +- sco_bulletins_xml.py | 4 +- sco_compute_moy.py | 4 +- sco_evaluations.py | 4 +- sco_formsemestre_validation.py | 2 +- sco_groups_view.py | 6 +- sco_moduleimpl_status.py | 4 +- sco_poursuite_dut.py | 4 +- sco_prepajury.py | 2 +- sco_trombino_tours.py | 6 +- 17 files changed, 699 insertions(+), 651 deletions(-) create mode 100644 sco_abs.py diff --git a/ZAbsences.py b/ZAbsences.py index 6ebf844e0b..8e50e72ae5 100644 --- a/ZAbsences.py +++ b/ZAbsences.py @@ -48,6 +48,13 @@ import urllib import datetime import jaxml import cgi +import string +import re +import time +import calendar + +from mx.DateTime import DateTime as mxDateTime +from mx.DateTime.ISO import ParseDateTimeUTC # --------------- from sco_zope import * @@ -68,10 +75,8 @@ import sco_groups_view import sco_excel import sco_abs_notification, sco_abs_views import sco_compute_moy -import string, re -import time, calendar -from mx.DateTime import DateTime as mxDateTime -from mx.DateTime.ISO import ParseDateTimeUTC +import sco_abs +from sco_abs import ddmmyyyy def _toboolean(x): @@ -84,189 +89,6 @@ def _toboolean(x): return False -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 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 = [ - '' - ] - T.append("") - month = firstmonth - while 1: - T.append('") - if month == lastmonth: - break - month = month + 1 - if month > 12: - month = 1 - year = year + 1 - T.append("
') - T.append(MonthTableHead(month)) - T.append( - MonthTableBody( - month, - year, - events, - halfday, - dayattributes, - context.is_work_saturday(), - pad_width=pad_width, - ) - ) - T.append(MonthTableTail()) - T.append("
") - return string.join(T, "\n") - - -# --------------- - - class ZAbsences( ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit ): @@ -373,7 +195,7 @@ class ZAbsences( % vars(), ) cnx.commit() - invalidateAbsEtudDate(self, etudid, jour) + sco_abs.invalidateAbsEtudDate(self, etudid, jour) sco_abs_notification.abs_notify(self, etudid, jour) def _AddJustif(self, etudid, jour, matin, REQUEST, description=None): @@ -396,7 +218,7 @@ class ZAbsences( msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), ) cnx.commit() - invalidateAbsEtudDate(self, etudid, jour) + sco_abs.invalidateAbsEtudDate(self, etudid, jour) def _AnnuleAbsence(self, etudid, jour, matin, moduleimpl_id=None, REQUEST=None): """Annule une absence ds base @@ -419,7 +241,7 @@ class ZAbsences( % vars(), ) cnx.commit() - invalidateAbsEtudDate(self, etudid, jour) + sco_abs.invalidateAbsEtudDate(self, etudid, jour) def _AnnuleJustif(self, etudid, jour, matin, REQUEST=None): "Annule un justificatif" @@ -443,7 +265,7 @@ class ZAbsences( msg="JOUR=%(jour)s,MATIN=%(matin)s" % vars(), ) cnx.commit() - invalidateAbsEtudDate(self, etudid, jour) + sco_abs.invalidateAbsEtudDate(self, etudid, jour) # Fonction inutile à supprimer (gestion moduleimpl_id incorrecte): # def _AnnuleAbsencesPeriodNoJust(self, etudid, datedebut, datefin, @@ -462,8 +284,8 @@ class ZAbsences( # logdb(REQUEST, cnx, 'AnnuleAbsencesPeriodNoJust', etudid=etudid, # msg='%(datedebut)s - %(datefin)s - (moduleimpl_id)s'%vars()) # cnx.commit() - # invalidateAbsEtudDate(self, etudid, datedebut) - # invalidateAbsEtudDate(self, etudid, datefin) # si un semestre commence apres datedebut et termine avant datefin, il ne sera pas invalide. Tant pis ;-) + # sco_abs.invalidateAbsEtudDate(self, etudid, datedebut) + # sco_abs.invalidateAbsEtudDate(self, etudid, datefin) # si un semestre commence apres datedebut et termine avant datefin, il ne sera pas invalide. Tant pis ;-) security.declareProtected(ScoAbsChange, "AnnuleAbsencesDatesNoJust") @@ -497,7 +319,7 @@ class ZAbsences( "delete from absences where etudid=%(etudid)s and (not estjust) and jour=%(date)s and moduleimpl_id=%(moduleimpl_id)s", vars(), ) - invalidateAbsEtudDate(self, etudid, date) + sco_abs.invalidateAbsEtudDate(self, etudid, date) # s'assure que les justificatifs ne sont pas "absents" for date in dates: cursor.execute( @@ -855,7 +677,7 @@ class ZAbsences( js = "" else: js = 'onmouseover="highlightweek(this);" onmouseout="deselectweeks();" onclick="wclick(this);"' - C = YearTable(self, int(year), dayattributes=js) + C = sco_abs.YearTable(self, int(year), dayattributes=js) return C # --- Misc tools.... ------------------ @@ -1352,7 +1174,7 @@ class ZAbsences( else: checked = "" # bulle lors du passage souris - coljour = DAYNAMES[ + coljour = sco_abs.DAYNAMES[ (calendar.weekday(int(date[:4]), int(date[5:7]), int(date[8:]))) ] datecol = coljour + " " + date[8:] + "/" + date[5:7] + "/" + date[:4] @@ -1798,7 +1620,7 @@ ou entrez une date pour visualiser les absents un jour donné : justified = int(justified) # cnx = self.GetDBConnexion() - billet_id = billet_absence_create( + billet_id = sco_abs.billet_absence_create( cnx, { "etudid": etud["etudid"], @@ -1814,7 +1636,7 @@ ou entrez une date pour visualiser les absents un jour donné : if REQUEST: REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) - billets = billet_absence_list(cnx, {"billet_id": billet_id}) + billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) tab = self._tableBillets(billets, etud=etud) log( "AddBilletAbsence: new billet_id=%s (%gs)" @@ -1940,7 +1762,7 @@ ou entrez une date pour visualiser les absents un jour donné : etud = etuds[0] cnx = self.GetDBConnexion() - billets = billet_absence_list(cnx, {"etudid": etud["etudid"]}) + billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]}) tab = self._tableBillets(billets, etud=etud) return tab.make_page(self, REQUEST=REQUEST, format=format) @@ -1960,7 +1782,7 @@ ou entrez une date pour visualiser les absents un jour donné : def listeBillets(self, REQUEST=None): """Page liste des billets non traités et formulaire recherche d'un billet""" cnx = self.GetDBConnexion() - billets = billet_absence_list(cnx, {"etat": 0}) + billets = sco_abs.billet_absence_list(cnx, {"etat": 0}) tab = self._tableBillets(billets) T = tab.html() H = [ @@ -1986,7 +1808,7 @@ ou entrez une date pour visualiser les absents un jour donné : def deleteBilletAbsence(self, billet_id, REQUEST=None, dialog_confirmed=False): """Supprime un billet.""" cnx = self.GetDBConnexion() - billets = billet_absence_list(cnx, {"billet_id": billet_id}) + billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) if not billets: return REQUEST.RESPONSE.redirect( "listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id @@ -2001,7 +1823,7 @@ ou entrez une date pour visualiser les absents un jour donné : parameters={"billet_id": billet_id}, ) - billet_absence_delete(cnx, billet_id) + sco_abs.billet_absence_delete(cnx, billet_id) return REQUEST.RESPONSE.redirect("listeBillets?head_message=Billet%20supprimé") @@ -2050,7 +1872,7 @@ ou entrez une date pour visualiser les absents un jour donné : n += 2 # 2- change etat du billet - billet_absence_edit(cnx, {"billet_id": billet["billet_id"], "etat": 1}) + sco_abs.billet_absence_edit(cnx, {"billet_id": billet["billet_id"], "etat": 1}) return n @@ -2059,7 +1881,7 @@ ou entrez une date pour visualiser les absents un jour donné : def ProcessBilletAbsenceForm(self, billet_id, REQUEST=None): """Formulaire traitement d'un billet""" cnx = self.GetDBConnexion() - billets = billet_absence_list(cnx, {"billet_id": billet_id}) + billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) if not billets: return REQUEST.RESPONSE.redirect( "listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id @@ -2134,7 +1956,7 @@ ou entrez une date pour visualiser les absents un jour donné : '

Autre billets en attente

Billets déclarés par %s

' % (etud["nomprenom"]) ) - billets = billet_absence_list(cnx, {"etudid": etud["etudid"]}) + billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]}) tab = self._tableBillets(billets, etud=etud) H.append(tab.html()) return "\n".join(H) + self.sco_footer(REQUEST) @@ -2172,258 +1994,6 @@ ou entrez une date pour visualiser les absents un jour donné : return repr(doc) -_billet_absenceEditor = notesdb.EditableTable( - "billet_absence", - "billet_id", - ( - "billet_id", - "etudid", - "abs_begin", - "abs_end", - "description", - "etat", - "entry_date", - "justified", - ), - sortkey="entry_date desc", -) - -billet_absence_create = _billet_absenceEditor.create -billet_absence_delete = _billet_absenceEditor.delete -billet_absence_list = _billet_absenceEditor.list -billet_absence_edit = _billet_absenceEditor.edit - -# ------ HTML Calendar functions (see YearTable function) - -# MONTH/DAY NAMES: - -MONTHNAMES = ( - "Janvier", - "Février", - "Mars", - "Avril", - "Mai", - "Juin", - "Juillet", - "Aout", - "Septembre", - "Octobre", - "Novembre", - "Décembre", -) - -MONTHNAMES_ABREV = ( - "Jan.", - "Fév.", - "Mars", - "Avr.", - "Mai ", - "Juin", - "Juil", - "Aout", - "Sept", - "Oct.", - "Nov.", - "Déc.", -) - -DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") - -DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D") - -# COLORS: - -WHITE = "#FFFFFF" -GRAY1 = "#EEEEEE" -GREEN3 = "#99CC99" -WEEKDAYCOLOR = GRAY1 -WEEKENDCOLOR = GREEN3 - - -def MonthTableHead(month): - color = WHITE - return """ - \n""" % ( - color, - MONTHNAMES_ABREV[month - 1], - ) - - -def MonthTableTail(): - return "
%s
\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('' % color) - else: - cc.append('') - - if href: - href = 'href="%s"' % href - if descr: - descr = 'title="%s"' % cgi.escape(descr, quote=True) - if href or descr: - cc.append("" % (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("") - cc.append("") - 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( - '%d%s%s' - % (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( - '%d%s' - % (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('' % (color)) - else: - cc.append('') - if href: - href = 'href="%s"' % href - if descr: - descr = 'title="%s"' % cgi.escape(descr, quote=True) - if href or descr: - cc.append("" % (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("") - cc.append("\n") - T.append(string.join(cc, "") + "") - return string.join(T, "\n") - - # -------------------------------------------------------------------- # # Zope Product Administration @@ -2441,121 +2011,3 @@ def manage_addZAbsences( # The form used to get the instance id from the user. # manage_addZAbsencesForm = DTMLFile('dtml/manage_addZAbsencesForm', globals()) - - -# -------------------------------------------------------------------- -# -# Cache absences -# -# On cache simplement (à la demande) le nombre d'absences de chaque etudiant -# dans un semestre donné. -# Toute modification du semestre (invalidation) invalide le cache -# (simple mécanisme de "listener" sur le cache de semestres) -# Toute modification des absences d'un étudiant invalide les caches des semestres -# concernés à cette date (en général un seul semestre) -# -# On ne cache pas la liste des absences car elle est rarement utilisée (calendrier, -# absences à une date donnée). -# -# -------------------------------------------------------------------- -class CAbsSemEtud: - """Comptes d'absences d'un etudiant dans un semestre""" - - def __init__(self, context, sem, etudid): - self.context = context - self.sem = sem - self.etudid = etudid - self._loaded = False - formsemestre_id = sem["formsemestre_id"] - context.Notes._getNotesCache().add_listener( - self.invalidate, formsemestre_id, (etudid, formsemestre_id) - ) - - def CountAbs(self): - if not self._loaded: - self.load() - return self._CountAbs - - def CountAbsJust(self): - if not self._loaded: - self.load() - return self._CountAbsJust - - def load(self): - "Load state from DB" - # log('loading CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id'])) - # Reload sem, it may have changed - self.sem = sco_formsemestre.get_formsemestre( - self.context, self.sem["formsemestre_id"] - ) - debut_sem = notesdb.DateDMYtoISO(self.sem["date_debut"]) - fin_sem = notesdb.DateDMYtoISO(self.sem["date_fin"]) - - self._CountAbs = self.context.Absences.CountAbs( - etudid=self.etudid, debut=debut_sem, fin=fin_sem - ) - self._CountAbsJust = self.context.Absences.CountAbsJust( - etudid=self.etudid, debut=debut_sem, fin=fin_sem - ) - self._loaded = True - - def invalidate(self, args=None): - "Notify me that DB has been modified" - # log('invalidate CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id'])) - self._loaded = False - - -# Accès au cache des absences -ABS_CACHE_INST = {} # { DeptId : { formsemestre_id : { etudid : CAbsEtudSem } } } - - -def getAbsSemEtud(context, sem, etudid): - AbsSemEtuds = getAbsSemEtuds(context, sem) - if not etudid in AbsSemEtuds: - AbsSemEtuds[etudid] = CAbsSemEtud(context, sem, etudid) - return AbsSemEtuds[etudid] - - -def getAbsSemEtuds(context, sem): - u = context.GetDBConnexionString() # identifie le dept de facon fiable - if not u in ABS_CACHE_INST: - ABS_CACHE_INST[u] = {} - C = ABS_CACHE_INST[u] - if sem["formsemestre_id"] not in C: - C[sem["formsemestre_id"]] = {} - return C[sem["formsemestre_id"]] - - -def invalidateAbsEtudDate(context, etudid, date): - """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. - Invalide cache absence et PDF bulletins si nécessaire. - date: date au format ISO - """ - # Semestres a cette date: - etud = context.getEtudInfo(etudid=etudid, filled=True)[0] - sems = [ - sem - for sem in etud["sems"] - if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date - ] - - # Invalide les PDF et les abscences: - for sem in sems: - # Inval cache bulletin et/ou note_table - if sco_compute_moy.formsemestre_expressions_use_abscounts( - context, sem["formsemestre_id"] - ): - pdfonly = False # seules certaines formules utilisent les absences - else: - pdfonly = ( - True # efface toujours le PDF car il affiche en général les absences - ) - - context.Notes._inval_cache( - pdfonly=pdfonly, formsemestre_id=sem["formsemestre_id"] - ) - - # Inval cache compteurs absences: - AbsSemEtuds = getAbsSemEtuds(context, sem) - if etudid in AbsSemEtuds: - AbsSemEtuds[etudid].invalidate() diff --git a/ZEntreprises.py b/ZEntreprises.py index e835c80dc1..3e8a44682d 100644 --- a/ZEntreprises.py +++ b/ZEntreprises.py @@ -107,7 +107,7 @@ class EntreprisesEditor(EditableTable): "select E.*, I.nom as etud_nom, I.prenom as etud_prenom, C.date from entreprises E, entreprise_contact C, identite I where C.entreprise_id = E.entreprise_id and C.etudid = I.etudid and I.nom ~* %(etud_nom)s ORDER BY E.nom", args, ) - titles, res = [x[0] for x in cursor.description], cursor.dictfetchall() + _, res = [x[0] for x in cursor.description], cursor.dictfetchall() R = [] for r in res: r["etud_prenom"] = r["etud_prenom"] or "" @@ -450,7 +450,6 @@ class ZEntreprises( def do_entreprise_correspondant_listnames(self, args={}): "-> liste des noms des correspondants (pour affichage menu)" - cnx = self.GetDBConnexion() C = self.do_entreprise_correspondant_list(args=args) return [ (x["prenom"] + " " + x["nom"], str(x["entreprise_corresp_id"])) for x in C @@ -538,43 +537,48 @@ class ZEntreprises( # (fonction ad-hoc car requete sur plusieurs tables) raise NotImplementedError # XXXXX fonction non achevee , non testee... - cnx = self.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ScoDocCursor) - vals = dictfilter(args, self.dbfields) - # DBSelect - what = ["*"] - operator = " " + operator + " " - cond = " E.entreprise_id = C.entreprise_id " - if vals: - cond += " where " + operator.join( - ["%s%s%%(%s)s" % (x, test, x) for x in vals.keys() if vals[x] != None] - ) - cnuls = " and ".join( - ["%s is NULL" % x for x in vals.keys() if vals[x] is None] - ) - if cnuls: - cond = cond + " and " + cnuls - else: - cond += "" - cursor.execute( - "select distinct" - + ", ".join(what) - + " from entreprises E, entreprise_contact C " - + cond - + orderby, - vals, - ) - titles, res = [x[0] for x in cursor.description], cursor.fetchall() - # - R = [] - for r in res: - d = {} - for i in range(len(titles)): - v = r[i] - # value not formatted ! (see EditableTable.list()) - d[titles[i]] = v - R.append(d) - return R + + # cnx = self.GetDBConnexion() + # cursor = cnx.cursor(cursor_factory=ScoDocCursor) + # if sortkey: + # orderby = " order by " + sortkey + # else: + # orderby = "" + # vals = dictfilter(args, self.dbfields) + # # DBSelect + # what = ["*"] + # operator = " " + operator + " " + # cond = " E.entreprise_id = C.entreprise_id " + # if vals: + # cond += " where " + operator.join( + # ["%s%s%%(%s)s" % (x, test, x) for x in vals.keys() if vals[x] != None] + # ) + # cnuls = " and ".join( + # ["%s is NULL" % x for x in vals.keys() if vals[x] is None] + # ) + # if cnuls: + # cond = cond + " and " + cnuls + # else: + # cond += "" + # cursor.execute( + # "select distinct" + # + ", ".join(what) + # + " from entreprises E, entreprise_contact C " + # + cond + # + orderby, + # vals, + # ) + # titles, res = [x[0] for x in cursor.description], cursor.fetchall() + # # + # R = [] + # for r in res: + # d = {} + # for i in range(len(titles)): + # v = r[i] + # # value not formatted ! (see EditableTable.list()) + # d[titles[i]] = v + # R.append(d) + # return R # -------- Formulaires: traductions du DTML security.declareProtected(ScoEntrepriseChange, "entreprise_create") @@ -892,7 +896,3 @@ def manage_addZEntreprises( if REQUEST is not None: return self.manage_main(self, REQUEST) # return self.manage_editForm(self, REQUEST) - - -# The form used to get the instance id from the user. -# manage_addZAbsencesForm = DTMLFile('dtml/manage_addZAbsencesForm', globals()) diff --git a/ZScolar.py b/ZScolar.py index 7a63557009..d09e2ac50c 100644 --- a/ZScolar.py +++ b/ZScolar.py @@ -89,6 +89,7 @@ import ZEntreprises import ZScoUsers import sco_modalites import ImportScolars +import sco_abs import sco_portal_apogee import sco_synchro_etuds import sco_page_etud @@ -274,13 +275,13 @@ UE11 Découverte métiers (code UCOD46, 16 ECTS, Apo """ - return ( - self.sco_header(REQUEST) - + """
%s
""" % x - + self.sco_footer(REQUEST) - ) - b = "

Hello, World !


" - raise ValueError("essai exception") + # return ( + # self.sco_header(REQUEST) + # + """
%s
""" % x + # + self.sco_footer(REQUEST) + # ) + # b = "

Hello, World !


" + # raise ValueError("essai exception") # raise ScoValueError('essai exception !', dest_url='totoro', REQUEST=REQUEST) # cursor = cnx.cursor(cursor_factory=ScoDocCursor) @@ -592,7 +593,7 @@ UE11 Découverte métiers (code UCOD46, 16 ECTS, Apo
' diff --git a/html_sidebar.py b/html_sidebar.py index ea05e71c79..df7bb59bcf 100644 --- a/html_sidebar.py +++ b/html_sidebar.py @@ -26,7 +26,7 @@ ############################################################################## from sco_utils import * -from ZAbsences import getAbsSemEtud +from sco_abs import getAbsSemEtud """ Génération de la "sidebar" (marge gauche des pages HTML) diff --git a/sco_abs.py b/sco_abs.py new file mode 100644 index 0000000000..8d97d949e8 --- /dev/null +++ b/sco_abs.py @@ -0,0 +1,595 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Fonctions sur les absences +""" + +# Anciennement dans ZAbscences.py, séparé pour migration + +import string +import datetime +import re +import time +import calendar +import cgi + +import notesdb +from sco_exceptions import ScoValueError, ScoInvalidDateError +import sco_formsemestre +import sco_compute_moy + + +def 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 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 = [ + '' + ] + T.append("") + month = firstmonth + while 1: + T.append('") + if month == lastmonth: + break + month = month + 1 + if month > 12: + month = 1 + year = year + 1 + T.append("
') + T.append(MonthTableHead(month)) + T.append( + MonthTableBody( + month, + year, + events, + halfday, + dayattributes, + context.is_work_saturday(), + pad_width=pad_width, + ) + ) + T.append(MonthTableTail()) + T.append("
") + return string.join(T, "\n") + + +# ---- BILLETS + +_billet_absenceEditor = notesdb.EditableTable( + "billet_absence", + "billet_id", + ( + "billet_id", + "etudid", + "abs_begin", + "abs_end", + "description", + "etat", + "entry_date", + "justified", + ), + sortkey="entry_date desc", +) + +billet_absence_create = _billet_absenceEditor.create +billet_absence_delete = _billet_absenceEditor.delete +billet_absence_list = _billet_absenceEditor.list +billet_absence_edit = _billet_absenceEditor.edit + +# ------ HTML Calendar functions (see YearTable function) + +# MONTH/DAY NAMES: + +MONTHNAMES = ( + "Janvier", + "Février", + "Mars", + "Avril", + "Mai", + "Juin", + "Juillet", + "Aout", + "Septembre", + "Octobre", + "Novembre", + "Décembre", +) + +MONTHNAMES_ABREV = ( + "Jan.", + "Fév.", + "Mars", + "Avr.", + "Mai ", + "Juin", + "Juil", + "Aout", + "Sept", + "Oct.", + "Nov.", + "Déc.", +) + +DAYNAMES = ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche") + +DAYNAMES_ABREV = ("L", "M", "M", "J", "V", "S", "D") + +# COLORS: + +WHITE = "#FFFFFF" +GRAY1 = "#EEEEEE" +GREEN3 = "#99CC99" +WEEKDAYCOLOR = GRAY1 +WEEKENDCOLOR = GREEN3 + + +def MonthTableHead(month): + color = WHITE + return """ + \n""" % ( + color, + MONTHNAMES_ABREV[month - 1], + ) + + +def MonthTableTail(): + return "
%s
\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('' % color) + else: + cc.append('') + + if href: + href = 'href="%s"' % href + if descr: + descr = 'title="%s"' % cgi.escape(descr, quote=True) + if href or descr: + cc.append("" % (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("") + cc.append("") + 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( + '%d%s%s' + % (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( + '%d%s' + % (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('' % (color)) + else: + cc.append('') + if href: + href = 'href="%s"' % href + if descr: + descr = 'title="%s"' % cgi.escape(descr, quote=True) + if href or descr: + cc.append("" % (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("") + cc.append("\n") + T.append(string.join(cc, "") + "") + return string.join(T, "\n") + + +# -------------------------------------------------------------------- +# +# Cache absences +# +# On cache simplement (à la demande) le nombre d'absences de chaque etudiant +# dans un semestre donné. +# Toute modification du semestre (invalidation) invalide le cache +# (simple mécanisme de "listener" sur le cache de semestres) +# Toute modification des absences d'un étudiant invalide les caches des semestres +# concernés à cette date (en général un seul semestre) +# +# On ne cache pas la liste des absences car elle est rarement utilisée (calendrier, +# absences à une date donnée). +# +# -------------------------------------------------------------------- +class CAbsSemEtud: + """Comptes d'absences d'un etudiant dans un semestre""" + + def __init__(self, context, sem, etudid): + self.context = context + self.sem = sem + self.etudid = etudid + self._loaded = False + formsemestre_id = sem["formsemestre_id"] + context.Notes._getNotesCache().add_listener( + self.invalidate, formsemestre_id, (etudid, formsemestre_id) + ) + + def CountAbs(self): + if not self._loaded: + self.load() + return self._CountAbs + + def CountAbsJust(self): + if not self._loaded: + self.load() + return self._CountAbsJust + + def load(self): + "Load state from DB" + # log('loading CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id'])) + # Reload sem, it may have changed + self.sem = sco_formsemestre.get_formsemestre( + self.context, self.sem["formsemestre_id"] + ) + debut_sem = notesdb.DateDMYtoISO(self.sem["date_debut"]) + fin_sem = notesdb.DateDMYtoISO(self.sem["date_fin"]) + + self._CountAbs = self.context.Absences.CountAbs( + etudid=self.etudid, debut=debut_sem, fin=fin_sem + ) + self._CountAbsJust = self.context.Absences.CountAbsJust( + etudid=self.etudid, debut=debut_sem, fin=fin_sem + ) + self._loaded = True + + def invalidate(self, args=None): + "Notify me that DB has been modified" + # log('invalidate CAbsEtudSem(%s,%s)' % (self.etudid, self.sem['formsemestre_id'])) + self._loaded = False + + +# Accès au cache des absences +ABS_CACHE_INST = {} # { DeptId : { formsemestre_id : { etudid : CAbsEtudSem } } } + + +def getAbsSemEtud(context, sem, etudid): + AbsSemEtuds = getAbsSemEtuds(context, sem) + if not etudid in AbsSemEtuds: + AbsSemEtuds[etudid] = CAbsSemEtud(context, sem, etudid) + return AbsSemEtuds[etudid] + + +def getAbsSemEtuds(context, sem): + u = context.GetDBConnexionString() # identifie le dept de facon fiable + if not u in ABS_CACHE_INST: + ABS_CACHE_INST[u] = {} + C = ABS_CACHE_INST[u] + if sem["formsemestre_id"] not in C: + C[sem["formsemestre_id"]] = {} + return C[sem["formsemestre_id"]] + + +def invalidateAbsEtudDate(context, etudid, date): + """Doit etre appelé à chaque modification des absences pour cet étudiant et cette date. + Invalide cache absence et PDF bulletins si nécessaire. + date: date au format ISO + """ + # Semestres a cette date: + etud = context.getEtudInfo(etudid=etudid, filled=True)[0] + sems = [ + sem + for sem in etud["sems"] + if sem["date_debut_iso"] <= date and sem["date_fin_iso"] >= date + ] + + # Invalide les PDF et les abscences: + for sem in sems: + # Inval cache bulletin et/ou note_table + if sco_compute_moy.formsemestre_expressions_use_abscounts( + context, sem["formsemestre_id"] + ): + pdfonly = False # seules certaines formules utilisent les absences + else: + pdfonly = ( + True # efface toujours le PDF car il affiche en général les absences + ) + + context.Notes._inval_cache( + pdfonly=pdfonly, formsemestre_id=sem["formsemestre_id"] + ) + + # Inval cache compteurs absences: + AbsSemEtuds = getAbsSemEtuds(context, sem) + if etudid in AbsSemEtuds: + AbsSemEtuds[etudid].invalidate() diff --git a/sco_abs_views.py b/sco_abs_views.py index af23bfd049..9ede1d384f 100644 --- a/sco_abs_views.py +++ b/sco_abs_views.py @@ -40,7 +40,7 @@ import sco_find_etud import sco_formsemestre import sco_photos -import ZAbsences +import sco_abs def doSignaleAbsence( @@ -631,7 +631,7 @@ def CalAbs(context, REQUEST=None): # etud implied events.append( (str(a["jour"]), "X", "#8EA2C6", "", a["matin"], a["description"]) ) - CalHTML = ZAbsences.YearTable(context, anneescolaire, events=events, halfday=1) + CalHTML = sco_abs.YearTable(context, anneescolaire, events=events, halfday=1) # H = [ diff --git a/sco_bulletins.py b/sco_bulletins.py index ab6e216160..1a8fe6d6f8 100644 --- a/sco_bulletins.py +++ b/sco_bulletins.py @@ -44,7 +44,7 @@ import sco_groups import sco_pvjury import sco_formsemestre_status import sco_photos -import ZAbsences +import sco_abs import sco_abs_views import sco_preferences import sco_codes_parcours @@ -150,7 +150,7 @@ def formsemestre_bulletinetud_dict( context, pid ) # --- Absences - AbsSemEtud = ZAbsences.getAbsSemEtud(context, nt.sem, etudid) + AbsSemEtud = sco_abs.getAbsSemEtud(context, nt.sem, etudid) I["nbabs"] = AbsSemEtud.CountAbs() I["nbabsjust"] = AbsSemEtud.CountAbsJust() diff --git a/sco_bulletins_json.py b/sco_bulletins_json.py index d284ce82dd..1d19a8c085 100644 --- a/sco_bulletins_json.py +++ b/sco_bulletins_json.py @@ -34,7 +34,7 @@ from notes_table import * import sco_formsemestre import sco_groups import sco_photos -import ZAbsences +import sco_abs import sco_bulletins # -------- Bulletin en JSON @@ -321,7 +321,7 @@ def formsemestre_bulletinetud_published_dict( if context.get_preference("bul_show_abs", formsemestre_id): debut_sem = DateDMYtoISO(sem["date_debut"]) fin_sem = DateDMYtoISO(sem["date_fin"]) - AbsEtudSem = ZAbsences.getAbsSemEtud(context, sem, etudid) + AbsEtudSem = sco_abs.getAbsSemEtud(context, sem, etudid) nbabs = AbsEtudSem.CountAbs() nbabsjust = AbsEtudSem.CountAbsJust() diff --git a/sco_bulletins_xml.py b/sco_bulletins_xml.py index 8c65b8213b..7edd20aeef 100644 --- a/sco_bulletins_xml.py +++ b/sco_bulletins_xml.py @@ -41,7 +41,7 @@ from notes_table import * import sco_formsemestre import sco_groups import sco_photos -import ZAbsences +import sco_abs import sco_bulletins # -------- Bulletin en XML @@ -323,7 +323,7 @@ def make_xml_formsemestre_bulletinetud( if context.get_preference("bul_show_abs", formsemestre_id): debut_sem = DateDMYtoISO(sem["date_debut"]) fin_sem = DateDMYtoISO(sem["date_fin"]) - AbsEtudSem = ZAbsences.getAbsSemEtud(context, sem, etudid) + AbsEtudSem = sco_abs.getAbsSemEtud(context, sem, etudid) nbabs = AbsEtudSem.CountAbs() nbabsjust = AbsEtudSem.CountAbsJust() doc._push() diff --git a/sco_compute_moy.py b/sco_compute_moy.py index 57a74dab0b..108f764fbf 100644 --- a/sco_compute_moy.py +++ b/sco_compute_moy.py @@ -37,7 +37,7 @@ import sco_formsemestre import sco_groups import sco_evaluations from sco_formulas import * -import ZAbsences +import sco_abs def moduleimpl_has_expression(context, mod): @@ -124,7 +124,7 @@ def compute_user_formula( Retourne moy, et en cas d'erreur met à jour diag_info (msg) """ if use_abs: - AbsSemEtud = ZAbsences.getAbsSemEtud(context, sem, etudid) + AbsSemEtud = sco_abs.getAbsSemEtud(context, sem, etudid) nbabs = AbsSemEtud.CountAbs() nbabs_just = AbsSemEtud.CountAbsJust() else: diff --git a/sco_evaluations.py b/sco_evaluations.py index e3db6d00fe..e877b62c00 100644 --- a/sco_evaluations.py +++ b/sco_evaluations.py @@ -42,7 +42,7 @@ from TrivialFormulator import TrivialFormulator import sco_news import sco_formsemestre import sco_groups -import ZAbsences +import sco_abs import sco_evaluations # -------------------------------------------------------------------- @@ -487,7 +487,7 @@ def formsemestre_evaluations_cal(context, formsemestre_id, REQUEST=None): if day > today: e[2] = color_futur - CalHTML = ZAbsences.YearTable( + CalHTML = sco_abs.YearTable( context.Absences, year, events=events.values(), halfday=False, pad_width=None ) diff --git a/sco_formsemestre_validation.py b/sco_formsemestre_validation.py index afee3e8c6c..097397797c 100644 --- a/sco_formsemestre_validation.py +++ b/sco_formsemestre_validation.py @@ -35,7 +35,7 @@ from notes_log import log from scolog import logdb from notes_table import * import notes_table -from ZAbsences import getAbsSemEtud +from sco_abs import getAbsSemEtud import sco_formsemestre import sco_formsemestre_edit diff --git a/sco_groups_view.py b/sco_groups_view.py index 1ae5387d68..152ceb98ee 100644 --- a/sco_groups_view.py +++ b/sco_groups_view.py @@ -35,7 +35,7 @@ from sco_utils import * import html_sco_header from gen_tables import GenTable import scolars -import ZAbsences +import sco_abs import sco_excel import sco_formsemestre import sco_groups @@ -870,7 +870,7 @@ def form_choix_jour_saisie_hebdo( if not authuser.has_permission(ScoAbsChange, context): return "" sem = groups_infos.formsemestre - first_monday = ZAbsences.ddmmyyyy(sem["date_debut"]).prev_monday() + first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() today_idx = datetime.date.today().weekday() FA = [] # formulaire avec menu saisi absences @@ -923,7 +923,7 @@ def form_choix_saisie_semaine(context, groups_infos, REQUEST=None): ) # car ici utilisee dans un format string ! DateJour = time.strftime("%d/%m/%Y") - datelundi = ZAbsences.ddmmyyyy(DateJour).prev_monday() + datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() FA = [] # formulaire avec menu saisi hebdo des absences FA.append('') FA.append('' % datelundi) diff --git a/sco_moduleimpl_status.py b/sco_moduleimpl_status.py index 7a4bf1e782..08c32bfc4e 100644 --- a/sco_moduleimpl_status.py +++ b/sco_moduleimpl_status.py @@ -43,7 +43,7 @@ import sco_formsemestre import sco_formsemestre_status from sco_formsemestre_status import makeMenu import sco_compute_moy -import ZAbsences +import sco_abs # ported from old DTML code in oct 2009 @@ -241,7 +241,7 @@ def moduleimpl_status(context, moduleimpl_id=None, partition_id=None, REQUEST=No if authuser.has_permission( ScoAbsChange, context ) and sco_formsemestre.sem_est_courant(context, sem): - datelundi = ZAbsences.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday() + datelundi = sco_abs.ddmmyyyy(time.strftime("%d/%m/%Y")).prev_monday() H.append( 'Saisie Absences hebdo.' % (formsemestre_id, moduleimpl_id, datelundi) diff --git a/sco_poursuite_dut.py b/sco_poursuite_dut.py index 46334deeed..c845ea049a 100644 --- a/sco_poursuite_dut.py +++ b/sco_poursuite_dut.py @@ -36,7 +36,7 @@ from notes_log import log from gen_tables import GenTable import sco_formsemestre import sco_groups -import ZAbsences +import sco_abs from sco_codes_parcours import code_semestre_validant, code_semestre_attente @@ -90,7 +90,7 @@ def etud_get_poursuite_info(context, sem, etud): rangs.append(["rang_" + codeModule, rangModule]) # Absences - AbsSemEtud = ZAbsences.getAbsSemEtud(context, nt.sem, etudid) + AbsSemEtud = sco_abs.getAbsSemEtud(context, nt.sem, etudid) NbAbs = AbsSemEtud.CountAbs() NbAbsJust = AbsSemEtud.CountAbsJust() if ( diff --git a/sco_prepajury.py b/sco_prepajury.py index 0df0063d2e..11f33a546c 100644 --- a/sco_prepajury.py +++ b/sco_prepajury.py @@ -38,7 +38,7 @@ import sco_formsemestre import sco_parcours_dut import sco_codes_parcours from scolars import format_nom, format_prenom, format_sexe, format_lycee -from ZAbsences import getAbsSemEtud +from sco_abs import getAbsSemEtud def feuille_preparation_jury(context, formsemestre_id, REQUEST): diff --git a/sco_trombino_tours.py b/sco_trombino_tours.py index eece74eb09..1b5a44ed8a 100644 --- a/sco_trombino_tours.py +++ b/sco_trombino_tours.py @@ -40,7 +40,7 @@ import tempfile from notes_log import log from sco_utils import * -import ZAbsences +import sco_abs import scolars import sco_photos import sco_formsemestre @@ -301,9 +301,9 @@ def pdf_feuille_releve_absences( NB_CELL_PM = context.get_preference("feuille_releve_abs_PM") COLWIDTH = 0.85 * cm if context.get_preference("feuille_releve_abs_samedi"): - days = ZAbsences.DAYNAMES[:6] # Lundi, ..., Samedi + days = sco_abs.DAYNAMES[:6] # Lundi, ..., Samedi else: - days = ZAbsences.DAYNAMES[:5] # Lundi, ..., Vendredi + days = sco_abs.DAYNAMES[:5] # Lundi, ..., Vendredi nb_days = len(days) # Informations sur les groupes à afficher: