From 845aeaba5f564224229aea25ee6d5afd322a005f Mon Sep 17 00:00:00 2001 From: Jean-Marie Place Date: Mon, 2 Aug 2021 08:25:48 +0200 Subject: [PATCH] test openpyxl sur liste appel --- app/scodoc/sco_excel.py | 1329 +++++++++++----------- app/scodoc/sco_groups_view.py | 1998 ++++++++++++++++----------------- app/scodoc/sco_utils.py | 1749 +++++++++++++++-------------- requirements-3.7.txt | 127 +-- 4 files changed, 2589 insertions(+), 2614 deletions(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 1b0ea311..46679b11 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -1,675 +1,654 @@ -# -*- 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 -# -############################################################################## - - -""" Excel file handling -""" -import time, datetime - -# #sco8 #py3 XXX TODO A revoir utiliser d'autres modules -# from pyExcelerator import * - -import app.scodoc.sco_utils as scu -from app.scodoc import notesdb -from app.scodoc.notes_log import log -from app.scodoc.scolog import logdb -from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc import sco_preferences -import six - - -# colors, voir exemple format.py -COLOR_CODES = { - "black": 0, - "red": 0x0A, - "mauve": 0x19, - "marron": 0x3C, - "blue": 0x4, - "orange": 0x34, - "lightyellow": 0x2B, -} - - -def sendExcelFile(REQUEST, data, filename): - """publication fichier. - (on ne doit rien avoir émis avant, car ici sont générés les entetes) - """ - filename = ( - scu.unescape_html(scu.suppress_accents(filename)) - .replace("&", "") - .replace(" ", "_") - ) - REQUEST.RESPONSE.setHeader("content-type", scu.XLS_MIMETYPE) - REQUEST.RESPONSE.setHeader( - "content-disposition", 'attachment; filename="%s"' % filename - ) - return data - - -## (stolen from xlrd) -# Convert an Excel number (presumed to represent a date, a datetime or a time) into -# a Python datetime.datetime -# @param xldate The Excel number -# @param datemode 0: 1900-based, 1: 1904-based. -# @return a datetime.datetime object, to the nearest_second. -#
Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time; -# a datetime.time object will be returned. -#
Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number" -# is zero. - -_XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) # This is equivalent to 10000-01-01 - - -def xldate_as_datetime(xldate, datemode=0): - if datemode not in (0, 1): - raise ValueError("invalid mode %s" % datemode) - if xldate == 0.00: - return datetime.time(0, 0, 0) - if xldate < 0.00: - raise ValueError("invalid date code %s" % xldate) - xldays = int(xldate) - frac = xldate - xldays - seconds = int(round(frac * 86400.0)) - assert 0 <= seconds <= 86400 - if seconds == 86400: - seconds = 0 - xldays += 1 - if xldays >= _XLDAYS_TOO_LARGE[datemode]: - raise ValueError("date too large %s" % xldate) - - if xldays == 0: - # second = seconds % 60; minutes = seconds // 60 - minutes, second = divmod(seconds, 60) - # minute = minutes % 60; hour = minutes // 60 - hour, minute = divmod(minutes, 60) - return datetime.time(hour, minute, second) - - if xldays < 61 and datemode == 0: - raise ValueError("ambiguous date %s" % xldate) - - return datetime.datetime.fromordinal( - xldays + 693594 + 1462 * datemode - ) + datetime.timedelta(seconds=seconds) - - -# Sous-classes pour ajouter methode savetostr() -# (generation de fichiers en memoire) -# XXX ne marche pas car accès a methodes privees (__xxx) -# -> on utilise version modifiee par nous meme de pyExcelerator -# -# class XlsDocWithSave(CompoundDoc.XlsDoc): -# def savetostr(self, stream): -# #added by Emmanuel: save method, but returns a string -# # 1. Align stream on 0x1000 boundary (and therefore on sector boundary) -# padding = '\x00' * (0x1000 - (len(stream) % 0x1000)) -# self.book_stream_len = len(stream) + len(padding) - -# self.__build_directory() -# self.__build_sat() -# self.__build_header() - -# return self.header+self.packed_MSAT_1st+stream+padding+self.packed_MSAT_2nd+self.packed_SAT+self.dir_stream - -# class WorkbookWithSave(Workbook): -# def savetostr(self): -# doc = XlsDocWithSave() -# return doc.savetostr(self.get_biff_data()) - - -def Excel_MakeStyle( - bold=False, italic=False, color="black", bgcolor=None, halign=None, valign=None -): - style = XFStyle() - font = Font() - if bold: - font.bold = bold - if italic: - font.italic = italic - font.name = "Arial" - colour_index = COLOR_CODES.get(color, None) - if colour_index: - font.colour_index = colour_index - if bgcolor: - style.pattern = Pattern() - style.pattern.pattern = Pattern.SOLID_PATTERN - style.pattern.pattern_fore_colour = COLOR_CODES.get(bgcolor, None) - al = None - if halign: - al = Alignment() - al.horz = { - "left": Alignment.HORZ_LEFT, - "right": Alignment.HORZ_RIGHT, - "center": Alignment.HORZ_CENTER, - }[halign] - if valign: - if not al: - al = Alignment() - al.vert = { - "top": Alignment.VERT_TOP, - "bottom": VERT_BOTTOM, - "center": VERT_CENTER, - }[valign] - if al: - style.alignment = al - style.font = font - return style - - -class ScoExcelSheet(object): - def __init__(self, sheet_name="feuille", default_style=None): - self.sheet_name = sheet_name - self.cells = [] # list of list - self.cells_styles_lico = {} # { (li,co) : style } - self.cells_styles_li = {} # { li : style } - self.cells_styles_co = {} # { co : style } - if not default_style: - default_style = Excel_MakeStyle() - self.default_style = default_style - - def set_style(self, style=None, li=None, co=None): - if li != None and co != None: - self.cells_styles_lico[(li, co)] = style - elif li != None: - self.cells_styles_li[li] = style - elif co != None: - self.cells_styles_co[co] = style - - def append(self, l): - """Append a line of cells""" - self.cells.append(l) - - def get_cell_style(self, li, co): - """Get style for specified cell""" - return ( - self.cells_styles_lico.get((li, co), None) - or self.cells_styles_li.get(li, None) - or self.cells_styles_co.get(co, None) - or self.default_style - ) - - def gen_workbook(self, wb=None): - """Generates and returns a workbook from stored data. - If wb, add a sheet (tab) to the existing workbook (in this case, returns None). - """ - if wb == None: - wb = Workbook() # Création du fichier - sauvegarde = True - else: - sauvegarde = False - ws0 = wb.add_sheet(self.sheet_name.decode(scu.SCO_ENCODING)) - li = 0 - for l in self.cells: - co = 0 - for c in l: - # safety net: allow only str, int and float - # #py3 #sco8 A revoir lors de la ré-écriture de ce module - # XXX if type(c) not in (IntType, FloatType): - # c = str(c).decode(scu.SCO_ENCODING) - - ws0.write(li, co, c, self.get_cell_style(li, co)) - co += 1 - li += 1 - if sauvegarde == True: - return wb.savetostr() - else: - return None - - -def Excel_SimpleTable(titles=[], lines=[[]], SheetName="feuille", titlesStyles=[]): - """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel""" - # XXX devrait maintenant utiliser ScoExcelSheet - wb = Workbook() - ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING)) - if not titlesStyles: - style = Excel_MakeStyle(bold=True) - titlesStyles = [style] * len(titles) - # ligne de titres - col = 0 - for it in titles: - ws0.write(0, col, it.decode(scu.SCO_ENCODING), titlesStyles[col]) - col += 1 - # suite - default_style = Excel_MakeStyle() - text_style = Excel_MakeStyle() - text_style.num_format_str = "@" - li = 1 - for l in lines: - col = 0 - for it in l: - cell_style = default_style - # safety net: allow only str, int and float - if isinstance(it, LongType): # XXX - it = int(it) # assume all ScoDoc longs fits in int ! - elif type(it) not in (IntType, FloatType): # XXX A REVOIR - it = str(it).decode(scu.SCO_ENCODING) - cell_style = text_style - ws0.write(li, col, it, cell_style) - col += 1 - li += 1 - # - return wb.savetostr() - - -def Excel_feuille_saisie(E, titreannee, description, lines): - """Genere feuille excel pour saisie des notes. - E: evaluation (dict) - lines: liste de tuples - (etudid, nom, prenom, etat, groupe, val, explanation) - """ - SheetName = "Saisie notes" - wb = Workbook() - ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING)) - # ajuste largeurs colonnes (unite inconnue, empirique) - ws0.col(0).width = 400 # codes - ws0.col(1).width = 6000 # noms - ws0.col(2).width = 4000 # prenoms - ws0.col(3).width = 6000 # groupes - ws0.col(4).width = 3000 # notes - ws0.col(5).width = 13000 # remarques - # styles - style_titres = XFStyle() - font0 = Font() - font0.bold = True - font0.name = "Arial" - font0.bold = True - font0.height = 14 * 0x14 - style_titres.font = font0 - - style_expl = XFStyle() - font_expl = Font() - font_expl.name = "Arial" - font_expl.italic = True - font0.height = 12 * 0x14 - font_expl.colour_index = 0x0A # rouge, voir exemple format.py - style_expl.font = font_expl - - topborders = Borders() - topborders.top = 1 - topleftborders = Borders() - topleftborders.top = 1 - topleftborders.left = 1 - rightborder = Borders() - rightborder.right = 1 - - style_ro = XFStyle() # cells read-only - font_ro = Font() - font_ro.name = "Arial" - font_ro.colour_index = COLOR_CODES["mauve"] - style_ro.font = font_ro - style_ro.borders = rightborder - - style_dem = XFStyle() # cells read-only - font_dem = Font() - font_dem.name = "Arial" - font_dem.colour_index = COLOR_CODES["marron"] - style_dem.font = font_dem - style_dem.borders = topborders - - style = XFStyle() - font1 = Font() - font1.name = "Arial" - font1.height = 12 * 0x14 - style.font = font1 - - style_nom = XFStyle() # style pour nom, prenom, groupe - style_nom.font = font1 - style_nom.borders = topborders - - style_notes = XFStyle() - font2 = Font() - font2.name = "Arial" - font2.bold = True - style_notes.font = font2 - style_notes.num_format_str = "general" - style_notes.pattern = Pattern() # fond jaune - style_notes.pattern.pattern = Pattern.SOLID_PATTERN - style_notes.pattern.pattern_fore_colour = COLOR_CODES["lightyellow"] - style_notes.borders = topborders - - style_comment = XFStyle() - font_comment = Font() - font_comment.name = "Arial" - font_comment.height = 9 * 0x14 - font_comment.colour_index = COLOR_CODES["blue"] - style_comment.font = font_comment - style_comment.borders = topborders - - # ligne de titres - li = 0 - ws0.write( - li, 0, u"Feuille saisie note (à enregistrer au format excel)", style_titres - ) - li += 1 - ws0.write(li, 0, u"Saisir les notes dans la colonne E (cases jaunes)", style_expl) - li += 1 - ws0.write(li, 0, u"Ne pas modifier les cases en mauve !", style_expl) - li += 1 - # Nom du semestre - ws0.write( - li, 0, scu.unescape_html(titreannee).decode(scu.SCO_ENCODING), style_titres - ) - li += 1 - # description evaluation - ws0.write( - li, 0, scu.unescape_html(description).decode(scu.SCO_ENCODING), style_titres - ) - li += 1 - ws0.write( - li, 0, u"Evaluation du %s (coef. %g)" % (E["jour"], E["coefficient"]), style - ) - li += 1 - li += 1 # ligne blanche - # code et titres colonnes - ws0.write(li, 0, u"!%s" % E["evaluation_id"], style_ro) - ws0.write(li, 1, u"Nom", style_titres) - ws0.write(li, 2, u"Prénom", style_titres) - ws0.write(li, 3, u"Groupe", style_titres) - ws0.write(li, 4, u"Note sur %g" % E["note_max"], style_titres) - ws0.write(li, 5, u"Remarque", style_titres) - # etudiants - for line in lines: - li += 1 - st = style_nom - ws0.write(li, 0, ("!" + line[0]).decode(scu.SCO_ENCODING), style_ro) # code - if line[3] != "I": - st = style_dem - if line[3] == "D": # demissionnaire - s = "DEM" - else: - s = line[3] # etat autre - else: - s = line[4] # groupes TD/TP/... - ws0.write(li, 1, line[1].decode(scu.SCO_ENCODING), st) - ws0.write(li, 2, line[2].decode(scu.SCO_ENCODING), st) - ws0.write(li, 3, s.decode(scu.SCO_ENCODING), st) - try: - val = float(line[5]) - except: - val = line[5].decode(scu.SCO_ENCODING) - ws0.write(li, 4, val, style_notes) # note - ws0.write(li, 5, line[6].decode(scu.SCO_ENCODING), style_comment) # comment - # explication en bas - li += 2 - ws0.write(li, 1, u"Code notes", style_titres) - ws0.write(li + 1, 1, u"ABS", style_expl) - ws0.write(li + 1, 2, u"absent (0)", style_expl) - ws0.write(li + 2, 1, u"EXC", style_expl) - ws0.write(li + 2, 2, u"pas prise en compte", style_expl) - ws0.write(li + 3, 1, u"ATT", style_expl) - ws0.write(li + 3, 2, u"en attente", style_expl) - ws0.write(li + 4, 1, u"SUPR", style_expl) - ws0.write(li + 4, 2, u"pour supprimer note déjà entrée", style_expl) - ws0.write(li + 5, 1, u"", style_expl) - ws0.write(li + 5, 2, u"cellule vide -> note non modifiée", style_expl) - return wb.savetostr() - - -def Excel_to_list(data, convert_to_string=str): # we may need 'encoding' argument ? - """returns list of list - convert_to_string is a conversion function applied to all non-string values (ie numbers) - """ - try: - P = parse_xls("", scu.SCO_ENCODING, doc=data) - except: - log("Excel_to_list: failure to import document") - open("/tmp/last_scodoc_import_failure.xls", "w").write(data) - raise ScoValueError( - "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" - ) - - diag = [] # liste de chaines pour former message d'erreur - # n'utilise que la première feuille - if len(P) < 1: - diag.append("Aucune feuille trouvée dans le classeur !") - return diag, None - if len(P) > 1: - diag.append("Attention: n'utilise que la première feuille du classeur !") - # fill matrix - sheet_name, values = P[0] - sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") - if not values: - diag.append("Aucune valeur trouvée dans le classeur !") - return diag, None - indexes = list(values.keys()) - # search numbers of rows and cols - rows = [x[0] for x in indexes] - cols = [x[1] for x in indexes] - nbcols = max(cols) + 1 - nbrows = max(rows) + 1 - M = [] - for _ in range(nbrows): - M.append([""] * nbcols) - - for row_idx, col_idx in indexes: - v = values[(row_idx, col_idx)] - if isinstance(v, six.text_type): - v = v.encode(scu.SCO_ENCODING, "backslashreplace") - elif convert_to_string: - v = convert_to_string(v) - M[row_idx][col_idx] = v - diag.append('Feuille "%s", %d lignes' % (sheet_name, len(M))) - # diag.append(str(M)) - # - return diag, M - - -# -def Excel_feuille_listeappel( - context, - sem, - groupname, - lines, - partitions=[], # partitions a montrer (colonnes) - with_codes=False, # indique codes etuds - with_paiement=False, # indique si etudiant a paye inscription - server_name=None, -): - "generation feuille appel" - formsemestre_id = sem["formsemestre_id"] - SheetName = "Liste " + groupname - wb = Workbook() - ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING)) - - font1 = Font() - font1.name = "Arial" - font1.height = 10 * 0x14 - - font1i = Font() - font1i.name = "Arial" - font1i.height = 10 * 0x14 - font1i.italic = True - - style1i = XFStyle() - style1i.font = font1i - - style1b = XFStyle() - style1b.font = font1 - borders = Borders() - borders.left = 1 - borders.top = 1 - borders.bottom = 1 - style1b.borders = borders - - style2 = XFStyle() - font2 = Font() - font2.name = "Arial" - font2.height = 14 * 0x14 - style2.font = font2 - - style2b = XFStyle() - style2b.font = font1i - borders = Borders() - borders.left = 1 - borders.top = 1 - borders.bottom = 1 - borders.right = 1 - style2b.borders = borders - - style2tb = XFStyle() - borders = Borders() - borders.top = 1 - borders.bottom = 1 - style2tb.borders = borders - style2tb.font = Font() - style2tb.font.height = 16 * 0x14 # -> ligne hautes - - style2t3 = XFStyle() - borders = Borders() - borders.top = 1 - borders.bottom = 1 - borders.left = 1 - style2t3.borders = borders - - style2t3bold = XFStyle() - borders = Borders() - borders.top = 1 - borders.bottom = 1 - borders.left = 1 - style2t3bold.borders = borders - fontb = Font() - fontb.bold = True - style2t3bold.font = fontb - - style3 = XFStyle() - font3 = Font() - font3.name = "Arial" - font3.bold = True - font3.height = 14 * 0x14 - style3.font = font3 - - NbWeeks = 4 # nombre de colonnes pour remplir absences - - # ligne 1 - li = 0 - ws0.write( - li, - 1, - ( - "%s %s (%s - %s)" - % ( - sco_preferences.get_preference("DeptName", formsemestre_id), - notesdb.unquote(sem["titre_num"]), - sem["date_debut"], - sem["date_fin"], - ) - ).decode(scu.SCO_ENCODING), - style2, - ) - # ligne 2 - li += 1 - ws0.write(li, 1, u"Discipline :", style2) - # ligne 3 - li += 1 - ws0.write(li, 1, u"Enseignant :", style2) - ws0.write(li, 5, ("Groupe %s" % groupname).decode(scu.SCO_ENCODING), style3) - # Avertissement pour ne pas confondre avec listes notes - ws0.write( - li + 1, 2, u"Ne pas utiliser cette feuille pour saisir les notes !", style1i - ) - # - li += 2 - li += 1 - ws0.write(li, 1, u"Nom", style3) - co = 2 - for partition in partitions: - if partition["partition_name"]: - ws0.write( - li, co, partition["partition_name"].decode(scu.SCO_ENCODING), style3 - ) - co += 1 - if with_codes: - coc = co - ws0.write(li, coc, u"etudid", style3) - ws0.write(li, coc + 1, u"code_nip", style3) - ws0.write(li, coc + 2, u"code_ine", style3) - co += 3 - - for i in range(NbWeeks): - ws0.write(li, co + i, "", style2b) - n = 0 - for t in lines: - n += 1 - li += 1 - ws0.write(li, 0, n, style1b) - nomprenom = ( - t["civilite_str"] - + " " - + t["nom"] - + " " - + scu.strcapitalize(scu.strlower(t["prenom"])) - ) - style_nom = style2t3 - if with_paiement: - paie = t.get("paiementinscription", None) - if paie is None: - nomprenom += " (inscription ?)" - style_nom = style2t3bold - elif not paie: - nomprenom += " (non paiement)" - style_nom = style2t3bold - ws0.write(li, 1, nomprenom.decode(scu.SCO_ENCODING), style_nom) - co = 2 - for partition in partitions: - if partition["partition_name"]: - ws0.write( - li, - co, - t.get(partition["partition_id"], "").decode(scu.SCO_ENCODING), - style2t3, - ) - co += 1 - if with_codes: - ws0.write(li, coc, t["etudid"].decode(scu.SCO_ENCODING), style2t3) - if t["code_nip"]: - code_nip = t["code_nip"].decode(scu.SCO_ENCODING) - else: - code_nip = u"" - ws0.write(li, coc + 1, code_nip, style2t3) - if t["code_ine"]: - code_ine = t["code_ine"].decode(scu.SCO_ENCODING) - else: - code_ine = u"" - ws0.write(li, coc + 2, code_ine, style2t3) - if t["etath"]: - etath = t["etath"].decode(scu.SCO_ENCODING) - else: - etath = u"" - ws0.write(li, co, etath, style2b) # etat - for i in range(1, NbWeeks): - ws0.write(li, co + i, u"", style2b) # cellules vides - ws0.row(li).height = 850 # sans effet ? - # - li += 2 - dt = time.strftime("%d/%m/%Y à %Hh%M") - if server_name: - dt += " sur " + server_name - ws0.write(li, 1, ("Liste éditée le " + dt).decode(scu.SCO_ENCODING), style1i) - # - ws0.col(0).width = 850 - ws0.col(1).width = 9000 - - return wb.savetostr() +# -*- 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 +# +############################################################################## + + +""" Excel file handling +""" +import time, datetime + +# #sco8 #py3 XXX TODO A revoir utiliser d'autres modules +# from pyExcelerator import * + +import app.scodoc.sco_utils as scu +from app.scodoc import notesdb +from app.scodoc.notes_log import log +from app.scodoc.scolog import logdb +from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc import sco_preferences +import six +from openpyxl import Workbook +from openpyxl.styles import Font, PatternFill, Border, Side, Alignment, Protection +from openpyxl.cell import WriteOnlyCell +from tempfile import NamedTemporaryFile + +# colors, voir exemple format.py +COLOR_CODES = { + "black": 0, + "red": 0x0A, + "mauve": 0x19, + "marron": 0x3C, + "blue": 0x4, + "orange": 0x34, + "lightyellow": 0x2B, +} + + +def send_excel_file(REQUEST, data, filename): + """publication fichier. + (on ne doit rien avoir émis avant, car ici sont générés les entetes) + """ + filename = ( + scu.unescape_html(scu.suppress_accents(filename)) + .replace("&", "") + .replace(" ", "_") + ) + REQUEST.RESPONSE.setHeader("content-type", scu.XLSX_MIMETYPE) + REQUEST.RESPONSE.setHeader( + "content-disposition", 'attachment; filename="%s"' % filename + ) + return data + + +## (stolen from xlrd) +# Convert an Excel number (presumed to represent a date, a datetime or a time) into +# a Python datetime.datetime +# @param xldate The Excel number +# @param datemode 0: 1900-based, 1: 1904-based. +# @return a datetime.datetime object, to the nearest_second. +#
Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time; +# a datetime.time object will be returned. +#
Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number" +# is zero. + +_XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) # This is equivalent to 10000-01-01 + + +def xldate_as_datetime(xldate, datemode=0): + if datemode not in (0, 1): + raise ValueError("invalid mode %s" % datemode) + if xldate == 0.00: + return datetime.time(0, 0, 0) + if xldate < 0.00: + raise ValueError("invalid date code %s" % xldate) + xldays = int(xldate) + frac = xldate - xldays + seconds = int(round(frac * 86400.0)) + assert 0 <= seconds <= 86400 + if seconds == 86400: + seconds = 0 + xldays += 1 + if xldays >= _XLDAYS_TOO_LARGE[datemode]: + raise ValueError("date too large %s" % xldate) + + if xldays == 0: + # second = seconds % 60; minutes = seconds // 60 + minutes, second = divmod(seconds, 60) + # minute = minutes % 60; hour = minutes // 60 + hour, minute = divmod(minutes, 60) + return datetime.time(hour, minute, second) + + if xldays < 61 and datemode == 0: + raise ValueError("ambiguous date %s" % xldate) + + return datetime.datetime.fromordinal( + xldays + 693594 + 1462 * datemode + ) + datetime.timedelta(seconds=seconds) + + +# Sous-classes pour ajouter methode savetostr() +# (generation de fichiers en memoire) +# XXX ne marche pas car accès a methodes privees (__xxx) +# -> on utilise version modifiee par nous meme de pyExcelerator +# +# class XlsDocWithSave(CompoundDoc.XlsDoc): +# def savetostr(self, stream): +# #added by Emmanuel: save method, but returns a string +# # 1. Align stream on 0x1000 boundary (and therefore on sector boundary) +# padding = '\x00' * (0x1000 - (len(stream) % 0x1000)) +# self.book_stream_len = len(stream) + len(padding) + +# self.__build_directory() +# self.__build_sat() +# self.__build_header() + +# return self.header+self.packed_MSAT_1st+stream+padding+self.packed_MSAT_2nd+self.packed_SAT+self.dir_stream + +# class WorkbookWithSave(Workbook): +# def savetostr(self): +# doc = XlsDocWithSave() +# return doc.savetostr(self.get_biff_data()) + + +def Excel_MakeStyle( + bold=False, italic=False, color="black", bgcolor=None, halign=None, valign=None +): + style = XFStyle() + font = Font() + if bold: + font.bold = bold + if italic: + font.italic = italic + font.name = "Arial" + colour_index = COLOR_CODES.get(color, None) + if colour_index: + font.colour_index = colour_index + if bgcolor: + style.pattern = Pattern() + style.pattern.pattern = Pattern.SOLID_PATTERN + style.pattern.pattern_fore_colour = COLOR_CODES.get(bgcolor, None) + al = None + if halign: + al = Alignment() + al.horz = { + "left": Alignment.HORZ_LEFT, + "right": Alignment.HORZ_RIGHT, + "center": Alignment.HORZ_CENTER, + }[halign] + if valign: + if not al: + al = Alignment() + al.vert = { + "top": Alignment.VERT_TOP, + "bottom": VERT_BOTTOM, + "center": VERT_CENTER, + }[valign] + if al: + style.alignment = al + style.font = font + return style + + +class ScoExcelSheet(object): + def __init__(self, sheet_name="feuille", default_style=None): + self.sheet_name = sheet_name + self.cells = [] # list of list + self.cells_styles_lico = {} # { (li,co) : style } + self.cells_styles_li = {} # { li : style } + self.cells_styles_co = {} # { co : style } + if not default_style: + default_style = Excel_MakeStyle() + self.default_style = default_style + + def set_style(self, style=None, li=None, co=None): + if li != None and co != None: + self.cells_styles_lico[(li, co)] = style + elif li != None: + self.cells_styles_li[li] = style + elif co != None: + self.cells_styles_co[co] = style + + def append(self, l): + """Append a line of cells""" + self.cells.append(l) + + def get_cell_style(self, li, co): + """Get style for specified cell""" + return ( + self.cells_styles_lico.get((li, co), None) + or self.cells_styles_li.get(li, None) + or self.cells_styles_co.get(co, None) + or self.default_style + ) + + def gen_workbook(self, wb=None): + """Generates and returns a workbook from stored data. + If wb, add a sheet (tab) to the existing workbook (in this case, returns None). + """ + if wb == None: + wb = Workbook() # Création du fichier + sauvegarde = True + else: + sauvegarde = False + ws0 = wb.add_sheet(self.sheet_name) + li = 0 + for l in self.cells: + co = 0 + for c in l: + # safety net: allow only str, int and float + # #py3 #sco8 A revoir lors de la ré-écriture de ce module + # XXX if type(c) not in (IntType, FloatType): + # c = str(c).decode(scu.SCO_ENCODING) + + ws0.write(li, co, c, self.get_cell_style(li, co)) + co += 1 + li += 1 + if sauvegarde == True: + return wb.savetostr() + else: + return None + + +def Excel_SimpleTable(titles=[], lines=[[]], SheetName="feuille", titlesStyles=[]): + """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel""" + # XXX devrait maintenant utiliser ScoExcelSheet + wb = Workbook() + ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING)) + if not titlesStyles: + style = Excel_MakeStyle(bold=True) + titlesStyles = [style] * len(titles) + # ligne de titres + col = 0 + for it in titles: + ws0.write(0, col, it.decode(scu.SCO_ENCODING), titlesStyles[col]) + col += 1 + # suite + default_style = Excel_MakeStyle() + text_style = Excel_MakeStyle() + text_style.num_format_str = "@" + li = 1 + for l in lines: + col = 0 + for it in l: + cell_style = default_style + # safety net: allow only str, int and float + if isinstance(it, LongType): # XXX + it = int(it) # assume all ScoDoc longs fits in int ! + elif type(it) not in (IntType, FloatType): # XXX A REVOIR + it = str(it).decode(scu.SCO_ENCODING) + cell_style = text_style + ws0.write(li, col, it, cell_style) + col += 1 + li += 1 + # + return wb.savetostr() + + +def Excel_feuille_saisie(E, titreannee, description, lines): + """Genere feuille excel pour saisie des notes. + E: evaluation (dict) + lines: liste de tuples + (etudid, nom, prenom, etat, groupe, val, explanation) + """ + SheetName = "Saisie notes" + wb = Workbook() + ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING)) + # ajuste largeurs colonnes (unite inconnue, empirique) + ws0.col(0).width = 400 # codes + ws0.col(1).width = 6000 # noms + ws0.col(2).width = 4000 # prenoms + ws0.col(3).width = 6000 # groupes + ws0.col(4).width = 3000 # notes + ws0.col(5).width = 13000 # remarques + # styles + style_titres = XFStyle() + font0 = Font() + font0.bold = True + font0.name = "Arial" + font0.bold = True + font0.height = 14 * 0x14 + style_titres.font = font0 + + style_expl = XFStyle() + font_expl = Font() + font_expl.name = "Arial" + font_expl.italic = True + font0.height = 12 * 0x14 + font_expl.colour_index = 0x0A # rouge, voir exemple format.py + style_expl.font = font_expl + + topborders = Borders() + topborders.top = 1 + topleftborders = Borders() + topleftborders.top = 1 + topleftborders.left = 1 + rightborder = Borders() + rightborder.right = 1 + + style_ro = XFStyle() # cells read-only + font_ro = Font() + font_ro.name = "Arial" + font_ro.colour_index = COLOR_CODES["mauve"] + style_ro.font = font_ro + style_ro.borders = rightborder + + style_dem = XFStyle() # cells read-only + font_dem = Font() + font_dem.name = "Arial" + font_dem.colour_index = COLOR_CODES["marron"] + style_dem.font = font_dem + style_dem.borders = topborders + + style = XFStyle() + font1 = Font() + font1.name = "Arial" + font1.height = 12 * 0x14 + style.font = font1 + + style_nom = XFStyle() # style pour nom, prenom, groupe + style_nom.font = font1 + style_nom.borders = topborders + + style_notes = XFStyle() + font2 = Font() + font2.name = "Arial" + font2.bold = True + style_notes.font = font2 + style_notes.num_format_str = "general" + style_notes.pattern = Pattern() # fond jaune + style_notes.pattern.pattern = Pattern.SOLID_PATTERN + style_notes.pattern.pattern_fore_colour = COLOR_CODES["lightyellow"] + style_notes.borders = topborders + + style_comment = XFStyle() + font_comment = Font() + font_comment.name = "Arial" + font_comment.height = 9 * 0x14 + font_comment.colour_index = COLOR_CODES["blue"] + style_comment.font = font_comment + style_comment.borders = topborders + + # ligne de titres + li = 0 + ws0.write( + li, 0, u"Feuille saisie note (à enregistrer au format excel)", style_titres + ) + li += 1 + ws0.write(li, 0, u"Saisir les notes dans la colonne E (cases jaunes)", style_expl) + li += 1 + ws0.write(li, 0, u"Ne pas modifier les cases en mauve !", style_expl) + li += 1 + # Nom du semestre + ws0.write( + li, 0, scu.unescape_html(titreannee).decode(scu.SCO_ENCODING), style_titres + ) + li += 1 + # description evaluation + ws0.write( + li, 0, scu.unescape_html(description).decode(scu.SCO_ENCODING), style_titres + ) + li += 1 + ws0.write( + li, 0, u"Evaluation du %s (coef. %g)" % (E["jour"], E["coefficient"]), style + ) + li += 1 + li += 1 # ligne blanche + # code et titres colonnes + ws0.write(li, 0, u"!%s" % E["evaluation_id"], style_ro) + ws0.write(li, 1, u"Nom", style_titres) + ws0.write(li, 2, u"Prénom", style_titres) + ws0.write(li, 3, u"Groupe", style_titres) + ws0.write(li, 4, u"Note sur %g" % E["note_max"], style_titres) + ws0.write(li, 5, u"Remarque", style_titres) + # etudiants + for line in lines: + li += 1 + st = style_nom + ws0.write(li, 0, ("!" + line[0]).decode(scu.SCO_ENCODING), style_ro) # code + if line[3] != "I": + st = style_dem + if line[3] == "D": # demissionnaire + s = "DEM" + else: + s = line[3] # etat autre + else: + s = line[4] # groupes TD/TP/... + ws0.write(li, 1, line[1].decode(scu.SCO_ENCODING), st) + ws0.write(li, 2, line[2].decode(scu.SCO_ENCODING), st) + ws0.write(li, 3, s.decode(scu.SCO_ENCODING), st) + try: + val = float(line[5]) + except: + val = line[5].decode(scu.SCO_ENCODING) + ws0.write(li, 4, val, style_notes) # note + ws0.write(li, 5, line[6].decode(scu.SCO_ENCODING), style_comment) # comment + # explication en bas + li += 2 + ws0.write(li, 1, u"Code notes", style_titres) + ws0.write(li + 1, 1, u"ABS", style_expl) + ws0.write(li + 1, 2, u"absent (0)", style_expl) + ws0.write(li + 2, 1, u"EXC", style_expl) + ws0.write(li + 2, 2, u"pas prise en compte", style_expl) + ws0.write(li + 3, 1, u"ATT", style_expl) + ws0.write(li + 3, 2, u"en attente", style_expl) + ws0.write(li + 4, 1, u"SUPR", style_expl) + ws0.write(li + 4, 2, u"pour supprimer note déjà entrée", style_expl) + ws0.write(li + 5, 1, u"", style_expl) + ws0.write(li + 5, 2, u"cellule vide -> note non modifiée", style_expl) + return wb.savetostr() + + +def Excel_to_list(data, convert_to_string=str): # we may need 'encoding' argument ? + """returns list of list + convert_to_string is a conversion function applied to all non-string values (ie numbers) + """ + try: + P = parse_xls("", scu.SCO_ENCODING, doc=data) + except: + log("Excel_to_list: failure to import document") + open("/tmp/last_scodoc_import_failure.xls", "w").write(data) + raise ScoValueError( + "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" + ) + + diag = [] # liste de chaines pour former message d'erreur + # n'utilise que la première feuille + if len(P) < 1: + diag.append("Aucune feuille trouvée dans le classeur !") + return diag, None + if len(P) > 1: + diag.append("Attention: n'utilise que la première feuille du classeur !") + # fill matrix + sheet_name, values = P[0] + sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") + if not values: + diag.append("Aucune valeur trouvée dans le classeur !") + return diag, None + indexes = list(values.keys()) + # search numbers of rows and cols + rows = [x[0] for x in indexes] + cols = [x[1] for x in indexes] + nbcols = max(cols) + 1 + nbrows = max(rows) + 1 + M = [] + for _ in range(nbrows): + M.append([""] * nbcols) + + for row_idx, col_idx in indexes: + v = values[(row_idx, col_idx)] + if isinstance(v, six.text_type): + v = v.encode(scu.SCO_ENCODING, "backslashreplace") + elif convert_to_string: + v = convert_to_string(v) + M[row_idx][col_idx] = v + diag.append('Feuille "%s", %d lignes' % (sheet_name, len(M))) + # diag.append(str(M)) + # + return diag, M + +# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante: +# font, border, .. (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) + + +def __make_cell(ws, value: any = u"", style=None): + """Contruit/retourne une cellule en spécifiant contenu et style. + + ws -- La feuille où sera intégrée la cellule + value -- le contenu de la cellule (texte) + style -- le style de la cellule + """ + cell = WriteOnlyCell(ws, value) + if "font" in style: + cell.font = style["font"] + if "border" in style: + cell.border = style["border"] + return cell + + +def excel_feuille_listeappel(sem, groupname, lines, partitions=None, with_codes=False, with_paiement=False, + server_name=None): + """generation feuille appel""" + if partitions is None: + partitions = [] + formsemestre_id = sem["formsemestre_id"] + sheet_name = "Liste " + groupname + wb = Workbook(write_only=True) + ws = wb.create_sheet(title=sheet_name) + ws.column_dimensions["A"].width = 3 + ws.column_dimensions["B"].width = 35 + ws.column_dimensions["C"].width = 12 + + font1 = Font(name="Arial", size=11) + font1i = Font(name="Arial", size=10, italic=True) + font1b = Font(name="Arial", size=11, bold=True) + + side_thin = Side(border_style="thin", color="FF000000") + + border_tbl = Border(top=side_thin, bottom=side_thin, left=side_thin) + border_tblr = Border( + top=side_thin, bottom=side_thin, left=side_thin, right=side_thin + ) + + style1i = { + "font": font1i, + } + + style1b = { + "font": font1, + "border": border_tbl, + } + + style2 = { + "font": Font(name="Arial", size=14), + } + + style2b = { + "font": font1i, + "border": border_tblr, + } + + style2t3 = { + "border": border_tblr, + } + + style2t3bold = { + "font": font1b, + "border": border_tblr, + } + + style3 = { + "font": Font(name="Arial", bold=True, size=14), + } + + nb_weeks = 4 # nombre de colonnes pour remplir absences + + # ligne 1 + title = "%s %s (%s - %s)" % ( + sco_preferences.get_preference("DeptName", formsemestre_id), + notesdb.unquote(sem["titre_num"]), + sem["date_debut"], + sem["date_fin"], + ) + + cell_2 = __make_cell(ws, title, style2) + ws.append([None, cell_2]) + + # ligne 2 + cell_2 = __make_cell(ws, u"Discipline :", style2) + ws.append([None, cell_2]) + + # ligne 3 + cell_2 = __make_cell(ws, u"Enseignant :", style2) + cell_6 = __make_cell(ws, ("Groupe %s" % groupname), style3) + ws.append([None, cell_2, None, None, None, None, cell_6]) + + # ligne 4: Avertissement pour ne pas confondre avec listes notes + cell_2 = __make_cell(ws, u"Ne pas utiliser cette feuille pour saisir les notes !", style1i) + ws.append([None, None, cell_2]) + + ws.append([None]) + ws.append([None]) + + # ligne 7: Entête (contruction dans une liste cells) + cells = [None] # passe la première colonne + cell_2 = __make_cell(ws, u"Nom", style3) + cells.append(cell_2) + for partition in partitions: + cells.append(__make_cell(ws, partition["partition_name"], style3)) + if with_codes: + cells.append(__make_cell(ws, u"etudid", style3)) + cells.append(__make_cell(ws, u"code_nip", style3)) + cells.append(__make_cell(ws, u"code_ine", style3)) + for i in range(nb_weeks): + cells.append(__make_cell(ws, "", style2b)) + ws.append(cells) + + n = 0 + # pour chaque étudiant + for t in lines: + n += 1 + nomprenom = ( + t["civilite_str"] + + " " + + t["nom"] + + " " + + scu.strcapitalize(scu.strlower(t["prenom"])) + ) + style_nom = style2t3 + if with_paiement: + paie = t.get("paiementinscription", None) + if paie is None: + nomprenom += " (inscription ?)" + style_nom = style2t3bold + elif not paie: + nomprenom += " (non paiement)" + style_nom = style2t3bold + cell_1 = __make_cell(ws, n, style1b) + cell_2 = __make_cell(ws, nomprenom, style_nom) + cells = [cell_1, cell_2] + + for partition in partitions: + if partition["partition_name"]: + cells.append( + __make_cell(ws, t.get(partition["partition_id"], u""), style2t3) + ) + if with_codes: + cells.append(__make_cell(ws, t["etudid"], style2t3)) + code_nip = t.get("code_nip", u"") + cells.append(__make_cell(ws, code_nip, style2t3)) + code_ine = t.get("code_ine", u"") + cells.append(__make_cell(ws, code_ine, style2t3)) + cells.append(__make_cell(ws, t.get("etath", ""), style2b)) + for i in range(1, nb_weeks): + cells.append(__make_cell(ws, style=style2t3)) + # ws0.row(li).height = 850 # sans effet ? + # (openpyxl: en mode optimisé, les hauteurs de lignes doivent être spécifiées avant toutes les cellules) + ws.append(cells) + + ws.append([None]) + + # bas de page (date, serveur) + dt = time.strftime("%d/%m/%Y à %Hh%M") + if server_name: + dt += " sur " + server_name + cell_2 = __make_cell(ws, ("Liste éditée le " + dt), style1i) + ws.append([None, cell_2]) + + # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) + with NamedTemporaryFile() as tmp: + wb.save(tmp.name) + tmp.seek(0) + return tmp.read() diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 033cb0e4..708038b4 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -1,1002 +1,996 @@ -# -*- 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 -# -############################################################################## - -"""Affichage étudiants d'un ou plusieurs groupes - sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf) -""" - -# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) -import datetime -import cgi -import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error -import time -import collections -import operator - -from flask import url_for, g - -import app.scodoc.sco_utils as scu -from app.scodoc import html_sco_header -from app.scodoc import sco_abs -from app.scodoc import sco_excel -from app.scodoc import sco_formsemestre -from app.scodoc import sco_groups -from app.scodoc import sco_moduleimpl -from app.scodoc import sco_parcours_dut -from app.scodoc import sco_portal_apogee -from app.scodoc import sco_preferences -from app.scodoc import sco_etud -from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_exceptions import ScoValueError -from app.scodoc.sco_permissions import Permission -from six.moves import range - -JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ - "js/etud_info.js", - "js/groups_view.js", -] - -CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS - - -def groups_view( - context, - group_ids=[], - format="html", - REQUEST=None, - # Options pour listes: - with_codes=0, - etat=None, - with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) - with_archives=0, # ajoute colonne avec noms fichiers archivés - with_annotations=0, - formsemestre_id=None, # utilise si aucun groupe selectionné -): - """Affichage des étudiants des groupes indiqués - group_ids: liste de group_id - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf - """ - # Informations sur les groupes à afficher: - groups_infos = DisplayedGroupsInfos( - context, - group_ids, - formsemestre_id=formsemestre_id, - etat=etat, - REQUEST=REQUEST, - select_all_when_unspecified=True, - ) - # Formats spéciaux: download direct - if format != "html": - return groups_table( - context=context, - groups_infos=groups_infos, - format=format, - REQUEST=REQUEST, - with_codes=with_codes, - etat=etat, - with_paiement=with_paiement, - with_archives=with_archives, - with_annotations=with_annotations, - ) - - H = [ - html_sco_header.sco_header( - javascripts=JAVASCRIPTS, - cssstyles=CSSSTYLES, - init_qtip=True, - ) - ] - # Menu choix groupe - H.append("""
""") - H.append(form_groups_choice(context, groups_infos, submit_on_change=True)) - # Note: le formulaire est soumis a chaque modif des groupes - # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela: - # - charger tous les etudiants au debut, quels que soient les groupes selectionnés - # - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change - - # Tabs - # H.extend( ("""toto""",) ) - H.extend( - ( - """ -
- -
-
- """, - groups_table( - context=context, - groups_infos=groups_infos, - format=format, - REQUEST=REQUEST, - with_codes=with_codes, - etat=etat, - with_paiement=with_paiement, - with_archives=with_archives, - with_annotations=with_annotations, - ), - "
", - """
""", - tab_photos_html(context, groups_infos, etat=etat, REQUEST=REQUEST), - #'

hello

', - "
", - '
', - tab_absences_html(context, groups_infos, etat=etat, REQUEST=REQUEST), - "
", - ) - ) - - H.append(html_sco_header.sco_footer()) - return "\n".join(H) - - -def form_groups_choice( - context, groups_infos, with_selectall_butt=False, submit_on_change=False -): - """form pour selection groupes - group_ids est la liste des groupes actuellement sélectionnés - et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. - (utilisé pour retrouver le semestre et proposer la liste des autres groupes) - - Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS - """ - default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - - H = [ - """
- - - Groupes: - """ - % (groups_infos.formsemestre_id, default_group_id) - ] - - H.append( - menu_groups_choice(context, groups_infos, submit_on_change=submit_on_change) - ) - - if with_selectall_butt: - H.append( - """""" - ) - H.append("
") - - return "\n".join(H) - - -def menu_groups_choice(context, groups_infos, submit_on_change=False): - """menu pour selection groupes - group_ids est la liste des groupes actuellement sélectionnés - et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. - (utilisé pour retrouver le semestre et proposer la liste des autres groupes) - """ - default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - - if submit_on_change: - klass = "submit_on_change" - else: - klass = "" - H = [ - """ ") - return "\n".join(H) - - -def menu_group_choice(context, group_id=None, formsemestre_id=None): - """Un bête menu pour choisir un seul groupe - group_id est le groupe actuellement sélectionné. - Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes. - """ - if group_id: - group = sco_groups.get_group(context, group_id) - formsemestre_id = group["formsemestre_id"] - elif not formsemestre_id: - raise ValueError("missing formsemestre_id") - H = [ - """ - - - """ - ) - return "\n".join(H) - - -class DisplayedGroupsInfos(object): - """Container with attributes describing groups to display in the page - .groups_query_args : 'group_ids=xxx&group_ids=yyy' - .base_url : url de la requete, avec les groupes, sans les autres paramètres - .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste) - .members - .groups_titles - """ - - def __init__( - self, - context, - group_ids=[], # groupes specifies dans l'URL - formsemestre_id=None, - etat=None, - select_all_when_unspecified=False, - moduleimpl_id=None, # used to find formsemestre when unspecified - REQUEST=None, - ): - # log('DisplayedGroupsInfos %s' % group_ids) - if isinstance(group_ids, str): - if group_ids: - group_ids = [group_ids] # cas ou un seul parametre, pas de liste - else: - group_ids = [] - if not formsemestre_id and moduleimpl_id: - mods = sco_moduleimpl.do_moduleimpl_list( - context, moduleimpl_id=moduleimpl_id - ) - if len(mods) != 1: - raise ValueError("invalid moduleimpl_id") - formsemestre_id = mods[0]["formsemestre_id"] - - if not group_ids: # appel sans groupe (eg page accueil) - if not formsemestre_id: - raise Exception("missing parameter formsemestre_id or group_ids") - if select_all_when_unspecified: - group_ids = [sco_groups.get_default_group(formsemestre_id)] - else: - # selectionne le premier groupe trouvé, s'il y en a un - partition = sco_groups.get_partitions_list( - context, formsemestre_id, with_default=True - )[0] - groups = sco_groups.get_partition_groups(context, partition) - if groups: - group_ids = [groups[0]["group_id"]] - else: - group_ids = [sco_groups.get_default_group(formsemestre_id)] - - gq = [] - for group_id in group_ids: - gq.append("group_ids=" + group_id) - self.groups_query_args = "&".join(gq) - self.base_url = REQUEST.URL0 + "?" + self.groups_query_args - self.group_ids = group_ids - self.groups = [] - groups_titles = [] - self.members = [] - self.tous_les_etuds_du_sem = ( - False # affiche tous les etuds du semestre ? (si un seul semestre) - ) - self.sems = collections.OrderedDict() # formsemestre_id : sem - self.formsemestre = None - self.formsemestre_id = formsemestre_id - self.nbdem = 0 # nombre d'étudiants démissionnaires en tout - sem = None - selected_partitions = set() - for group_id in group_ids: - group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos( - context, group_id, etat=etat - ) - self.groups.append(group) - self.nbdem += nbdem - self.sems[sem["formsemestre_id"]] = sem - if not self.formsemestre_id: - self.formsemestre_id = sem["formsemestre_id"] - self.formsemestre = sem - self.members.extend(group_members) - groups_titles.append(group_tit) - if group["group_name"] == None: - self.tous_les_etuds_du_sem = True - else: - # liste les partitions explicitement sélectionnés (= des groupes de group_ids) - selected_partitions.add((group["numero"], group["partition_id"])) - - self.selected_partitions = [ - x[1] for x in sorted(list(selected_partitions)) - ] # -> [ partition_id ] - - if not self.formsemestre: # aucun groupe selectionne - self.formsemestre = sco_formsemestre.get_formsemestre( - context, formsemestre_id - ) - - self.sortuniq() - - if len(self.sems) > 1: - self.tous_les_etuds_du_sem = False # plusieurs semestres - if self.tous_les_etuds_du_sem: - if sem and sem["semestre_id"] >= 0: - self.groups_titles = "S%d" % sem["semestre_id"] - else: - self.groups_titles = "tous" - self.groups_filename = self.groups_titles - else: - self.groups_titles = ", ".join(groups_titles) - self.groups_filename = "_".join(groups_titles).replace(" ", "_") - # Sanitize filename: - self.groups_filename = scu.make_filename(self.groups_filename) - - # colonnes pour affichages nom des groupes: - # gère le cas où les étudiants appartiennent à des semestres différents - self.partitions = [] # les partitions, sans celle par defaut - for formsemestre_id in self.sems: - for partition in sco_groups.get_partitions_list(context, formsemestre_id): - if partition["partition_name"]: - self.partitions.append(partition) - - def sortuniq(self): - "Trie les étudiants (de plusieurs groupes) et elimine les doublons" - if (len(self.group_ids) <= 1) or len(self.members) <= 1: - return # on suppose que les etudiants d'un groupe sont deja triés - self.members.sort( - key=operator.itemgetter("nom_disp", "prenom") - ) # tri selon nom_usuel ou nom - to_remove = [] - T = self.members - for i in range(len(T) - 1, 0, -1): - if T[i - 1]["etudid"] == T[i]["etudid"]: - to_remove.append(i) - for i in to_remove: - del T[i] - - def get_form_elem(self): - """html hidden input with groups""" - H = [] - for group_id in self.group_ids: - H.append('' % group_id) - return "\n".join(H) - - -# Ancien ZScolar.group_list renommé ici en group_table -def groups_table( - context=None, - REQUEST=None, - groups_infos=None, # instance of DisplayedGroupsInfos - with_codes=0, - etat=None, - format="html", - with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) - with_archives=0, # ajoute colonne avec noms fichiers archivés - with_annotations=0, -): - """liste etudiants inscrits dans ce semestre - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf - Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape - """ - from app.scodoc import sco_report - - # log( - # "enter groups_table %s: %s" - # % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-")) - # ) - with_codes = int(with_codes) - with_paiement = int(with_paiement) - with_archives = int(with_archives) - with_annotations = int(with_annotations) - - base_url_np = groups_infos.base_url + "&with_codes=%s" % with_codes - base_url = ( - base_url_np - + "&with_paiement=%s&with_archives=%s&with_annotations=%s" - % (with_paiement, with_archives, with_annotations) - ) - # - columns_ids = ["civilite_str", "nom_disp", "prenom"] # colonnes a inclure - titles = { - "civilite_str": "Civ.", - "nom_disp": "Nom", - "prenom": "Prénom", - "email": "Mail", - "emailperso": "Personnel", - "etat": "Etat", - "etudid": "etudid", - "code_nip": "code_nip", - "code_ine": "code_ine", - "datefinalisationinscription_str": "Finalisation inscr.", - "paiementinscription_str": "Paiement", - "etudarchive": "Fichiers", - "annotations_str": "Annotations", - "etape": "Etape", - "semestre_groupe": "Semestre-Groupe", # pour Moodle - } - - # ajoute colonnes pour groupes - columns_ids.extend([p["partition_id"] for p in groups_infos.partitions]) - titles.update( - dict( - [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] - ) - ) - - if format != "html": # ne mentionne l'état que en Excel (style en html) - columns_ids.append("etat") - columns_ids.append("email") - columns_ids.append("emailperso") - - if format == "moodlecsv": - columns_ids = ["email", "semestre_groupe"] - - if with_codes: - columns_ids += ["etape", "etudid", "code_nip", "code_ine"] - if with_paiement: - columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] - if with_paiement or with_codes: - sco_portal_apogee.check_paiement_etuds(context, groups_infos.members) - if with_archives: - from app.scodoc import sco_archives_etud - - sco_archives_etud.add_archives_info_to_etud_list(context, groups_infos.members) - columns_ids += ["etudarchive"] - if with_annotations: - sco_etud.add_annotations_to_etud_list(context, groups_infos.members) - columns_ids += ["annotations_str"] - - if groups_infos.formsemestre["semestre_id"] >= 0: - moodle_sem_name = "S%d" % groups_infos.formsemestre["semestre_id"] - else: - moodle_sem_name = "A" # pas de semestre spécifié, que faire ? - moodle_groupenames = set() - # ajoute liens - for etud in groups_infos.members: - if etud["email"]: - etud["_email_target"] = "mailto:" + etud["email"] - else: - etud["_email_target"] = "" - if etud["emailperso"]: - etud["_emailperso_target"] = "mailto:" + etud["emailperso"] - else: - etud["_emailperso_target"] = "" - fiche_url = url_for( - "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] - ) - etud["_nom_disp_target"] = fiche_url - etud["_prenom_target"] = fiche_url - - etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) - - if etud["etat"] == "D": - etud["_css_row_class"] = "etuddem" - # et groupes: - for partition_id in etud["partitions"]: - etud[partition_id] = etud["partitions"][partition_id]["group_name"] - # Ajoute colonne pour moodle: semestre_groupe, de la forme S1-NomgroupeXXX - moodle_groupename = [] - if groups_infos.selected_partitions: - # il y a des groupes selectionnes, utilise leurs partitions - for partition_id in groups_infos.selected_partitions: - if partition_id in etud["partitions"]: - moodle_groupename.append( - etud["partitions"][partition_id]["group_name"] - ) - else: - # pas de groupes sélectionnés: prend le premier s'il y en a un - if etud["partitions"]: - for p in etud["partitions"].items(): # partitions is an OrderedDict - break - moodle_groupename = [p[1]["group_name"]] - else: - moodle_groupename = ["tous"] - moodle_groupenames |= set(moodle_groupename) - etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) - - if groups_infos.nbdem > 1: - s = "s" - else: - s = "" - - if format == "moodlecsv": - # de la forme S1-[FI][FA]-groupe.csv - if not moodle_groupenames: - moodle_groupenames = {"tous"} - filename = ( - moodle_sem_name - + "-" - + groups_infos.formsemestre["modalite"] - + "-" - + "+".join(sorted(moodle_groupenames)) - ) - else: - filename = "etudiants_%s" % groups_infos.groups_filename - - prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) - tab = GenTable( - rows=groups_infos.members, - columns_ids=columns_ids, - titles=titles, - caption="soit %d étudiants inscrits et %d démissionaire%s." - % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s), - base_url=base_url, - filename=filename, - pdf_link=False, # pas d'export pdf - html_sortable=True, - html_class="table_leftalign table_listegroupe", - xml_outer_tag="group_list", - xml_row_tag="etud", - text_fields_separator=prefs["moodle_csv_separator"], - text_with_titles=prefs["moodle_csv_with_headerline"], - preferences=prefs, - ) - # - if format == "html": - amail_inst = [ - x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" - ] - amail_perso = [ - x["emailperso"] - for x in groups_infos.members - if x["emailperso"] and x["etat"] != "D" - ] - - if len(groups_infos.members): - if groups_infos.tous_les_etuds_du_sem: - htitle = "Les %d étudiants inscrits" % len(groups_infos.members) - else: - htitle = "Groupe %s (%d étudiants)" % ( - groups_infos.groups_titles, - len(groups_infos.members), - ) - else: - htitle = "Aucun étudiant !" - H = [ - '
' '

', - htitle, - "", - ] - if groups_infos.members: - Of = [] - options = { - "with_paiement": "Paiement inscription", - "with_archives": "Fichiers archivés", - "with_annotations": "Annotations", - "with_codes": "Codes", - } - for option in options: - if locals().get(option, False): - selected = "selected" - else: - selected = "" - Of.append( - """""" - % (option, selected, options[option]) - ) - - H.extend( - [ - """ - - """, - ] - ) - H.append("

") - if groups_infos.members: - H.extend( - [ - tab.html(), - "") - - return "".join(H) + "
" - - elif ( - format == "pdf" - or format == "xml" - or format == "json" - or format == "xls" - or format == "moodlecsv" - ): - if format == "moodlecsv": - format = "csv" - return tab.make_page(context, format=format, REQUEST=REQUEST) - - elif format == "xlsappel": - xls = sco_excel.Excel_feuille_listeappel( - context, - groups_infos.formsemestre, # le 1er semestre, serait à modifier si plusieurs - groups_infos.groups_titles, - groups_infos.members, - partitions=groups_infos.partitions, - with_codes=with_codes, - with_paiement=with_paiement, - server_name=REQUEST.BASE0, - ) - filename = "liste_%s" % groups_infos.groups_filename + ".xls" - return sco_excel.sendExcelFile(REQUEST, xls, filename) - elif format == "allxls": - # feuille Excel avec toutes les infos etudiants - if not groups_infos.members: - return "" - keys = [ - "etudid", - "code_nip", - "etat", - "civilite_str", - "nom", - "nom_usuel", - "prenom", - "inscriptionstr", - ] - if with_paiement: - keys.append("paiementinscription") - keys += [ - "email", - "emailperso", - "domicile", - "villedomicile", - "codepostaldomicile", - "paysdomicile", - "telephone", - "telephonemobile", - "fax", - "date_naissance", - "lieu_naissance", - "bac", - "specialite", - "annee_bac", - "nomlycee", - "villelycee", - "codepostallycee", - "codelycee", - "type_admission", - "boursier_prec", - "debouche", - "parcours", - "codeparcours", - ] - titles = keys[:] - other_partitions = sco_groups.get_group_other_partitions( - context, groups_infos.groups[0] - ) - keys += [p["partition_id"] for p in other_partitions] - titles += [p["partition_name"] for p in other_partitions] - # remplis infos lycee si on a que le code lycée - # et ajoute infos inscription - for m in groups_infos.members: - etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] - m.update(etud) - sco_etud.etud_add_lycee_infos(etud) - # et ajoute le parcours - Se = sco_parcours_dut.SituationEtudParcours( - context, etud, groups_infos.formsemestre_id - ) - m["parcours"] = Se.get_parcours_descr() - m["codeparcours"], _ = sco_report.get_codeparcoursetud(context, etud) - - def dicttakestr(d, keys): - r = [] - for k in keys: - r.append(str(d.get(k, ""))) - return r - - L = [dicttakestr(m, keys) for m in groups_infos.members] - title = "etudiants_%s" % groups_infos.groups_filename - xls = sco_excel.Excel_SimpleTable(titles=titles, lines=L, SheetName=title) - filename = title + ".xls" - return sco_excel.sendExcelFile(REQUEST, xls, filename) - else: - raise ValueError("unsupported format") - - -def tab_absences_html(context, groups_infos, etat=None, REQUEST=None): - """contenu du tab "absences et feuilles diverses" """ - authuser = REQUEST.AUTHENTICATED_USER - H = ['
'] - if not groups_infos.members: - return "".join(H) + "

Aucun étudiant !

" - H.extend( - [ - "

Absences

", - '", - "

Feuilles

", - '", - ] - ) - - H.append('

Opérations diverses

") - return "".join(H) - - -def tab_photos_html(context, groups_infos, etat=None, REQUEST=None): - """contenu du tab "photos" """ - from app.scodoc import sco_trombino - - if not groups_infos.members: - return '

Aucun étudiant !

' - - return sco_trombino.trombino_html(context, groups_infos, REQUEST=REQUEST) - - -def form_choix_jour_saisie_hebdo( - context, groups_infos, moduleimpl_id=None, REQUEST=None -): - """Formulaire choix jour semaine pour saisie.""" - authuser = REQUEST.AUTHENTICATED_USER - if not authuser.has_permission(Permission.ScoAbsChange): - return "" - sem = groups_infos.formsemestre - first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() - today_idx = datetime.date.today().weekday() - - FA = [] # formulaire avec menu saisi absences - FA.append( - '
' - ) - FA.append('' % sem) - FA.append(groups_infos.get_form_elem()) - if moduleimpl_id: - FA.append( - '' % moduleimpl_id - ) - FA.append('') - - FA.append( - """""" - ) - FA.append("""") - FA.append("
") - return "\n".join(FA) - - -# Ajout Le Havre -# Formulaire saisie absences semaine -def form_choix_saisie_semaine(context, groups_infos, REQUEST=None): - authuser = REQUEST.AUTHENTICATED_USER - if not authuser.has_permission(Permission.ScoAbsChange): - return "" - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) - query_args = cgi.parse_qs(REQUEST.QUERY_STRING) - moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] - if "head_message" in query_args: - del query_args["head_message"] - destination = "%s?%s" % ( - REQUEST.URL, - six.moves.urllib.parse.urlencode(query_args, True), - ) - destination = destination.replace( - "%", "%%" - ) # car ici utilisee dans un format string ! - - DateJour = time.strftime("%d/%m/%Y") - datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() - FA = [] # formulaire avec menu saisie hebdo des absences - FA.append('
') - FA.append('' % datelundi) - FA.append('' % moduleimpl_id) - FA.append('' % destination) - FA.append(groups_infos.get_form_elem()) - FA.append('') - FA.append("
") - return "\n".join(FA) - - -def export_groups_as_moodle_csv(context, formsemestre_id=None, REQUEST=None): - """Export all students/groups, in a CSV format suitable for Moodle - Each (student,group) will be listed on a separate line - jo@univ.fr,S3-A - jo@univ.fr,S3-B1 - if jo belongs to group A in a partition, and B1 in another one. - Caution: if groups in different partitions share the same name, there will be - duplicates... should we prefix the group names with the partition's name ? - """ - if not formsemestre_id: - raise ScoValueError("missing parameter: formsemestre_id") - _, partitions_etud_groups = sco_groups.get_formsemestre_groups( - context, formsemestre_id, with_default=True - ) - sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) - moodle_sem_name = sem["session_id"] - - columns_ids = ("email", "semestre_groupe") - T = [] - for partition_id in partitions_etud_groups: - partition = sco_groups.get_partition(context, partition_id) - members = partitions_etud_groups[partition_id] - for etudid in members: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - group_name = members[etudid]["group_name"] - elts = [moodle_sem_name] - if partition["partition_name"]: - elts.append(partition["partition_name"]) - if group_name: - elts.append(group_name) - T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) - # Make table - prefs = sco_preferences.SemPreferences(formsemestre_id) - tab = GenTable( - rows=T, - columns_ids=("email", "semestre_groupe"), - filename=moodle_sem_name + "-moodle", - titles={x: x for x in columns_ids}, - text_fields_separator=prefs["moodle_csv_separator"], - text_with_titles=prefs["moodle_csv_with_headerline"], - preferences=prefs, - ) - return tab.make_page(context, format="csv", REQUEST=REQUEST) +# -*- 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 +# +############################################################################## + +"""Affichage étudiants d'un ou plusieurs groupes + sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf) +""" + +# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) +import datetime +import cgi +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import time +import collections +import operator + +from flask import url_for, g + +import app.scodoc.sco_utils as scu +from app.scodoc import html_sco_header +from app.scodoc import sco_abs +from app.scodoc import sco_excel +from app.scodoc import sco_formsemestre +from app.scodoc import sco_groups +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_parcours_dut +from app.scodoc import sco_portal_apogee +from app.scodoc import sco_preferences +from app.scodoc import sco_etud +from app.scodoc.gen_tables import GenTable +from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_permissions import Permission +from six.moves import range + +JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ + "js/etud_info.js", + "js/groups_view.js", +] + +CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS + + +def groups_view( + context, + group_ids=[], + format="html", + REQUEST=None, + # Options pour listes: + with_codes=0, + etat=None, + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, + formsemestre_id=None, # utilise si aucun groupe selectionné +): + """Affichage des étudiants des groupes indiqués + group_ids: liste de group_id + format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + """ + # Informations sur les groupes à afficher: + groups_infos = DisplayedGroupsInfos( + context, + group_ids, + formsemestre_id=formsemestre_id, + etat=etat, + REQUEST=REQUEST, + select_all_when_unspecified=True, + ) + # Formats spéciaux: download direct + if format != "html": + return groups_table( + context=context, + groups_infos=groups_infos, + format=format, + REQUEST=REQUEST, + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, + with_archives=with_archives, + with_annotations=with_annotations, + ) + + H = [ + html_sco_header.sco_header( + javascripts=JAVASCRIPTS, + cssstyles=CSSSTYLES, + init_qtip=True, + ) + ] + # Menu choix groupe + H.append("""
""") + H.append(form_groups_choice(context, groups_infos, submit_on_change=True)) + # Note: le formulaire est soumis a chaque modif des groupes + # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela: + # - charger tous les etudiants au debut, quels que soient les groupes selectionnés + # - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change + + # Tabs + # H.extend( ("""toto""",) ) + H.extend( + ( + """ +
+ +
+
+ """, + groups_table( + context=context, + groups_infos=groups_infos, + format=format, + REQUEST=REQUEST, + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, + with_archives=with_archives, + with_annotations=with_annotations, + ), + "
", + """
""", + tab_photos_html(context, groups_infos, etat=etat, REQUEST=REQUEST), + #'

hello

', + "
", + '
', + tab_absences_html(context, groups_infos, etat=etat, REQUEST=REQUEST), + "
", + ) + ) + + H.append(html_sco_header.sco_footer()) + return "\n".join(H) + + +def form_groups_choice( + context, groups_infos, with_selectall_butt=False, submit_on_change=False +): + """form pour selection groupes + group_ids est la liste des groupes actuellement sélectionnés + et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. + (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + + Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS + """ + default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + + H = [ + """
+ + + Groupes: + """ + % (groups_infos.formsemestre_id, default_group_id) + ] + + H.append( + menu_groups_choice(context, groups_infos, submit_on_change=submit_on_change) + ) + + if with_selectall_butt: + H.append( + """""" + ) + H.append("
") + + return "\n".join(H) + + +def menu_groups_choice(context, groups_infos, submit_on_change=False): + """menu pour selection groupes + group_ids est la liste des groupes actuellement sélectionnés + et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. + (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + """ + default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + + if submit_on_change: + klass = "submit_on_change" + else: + klass = "" + H = [ + """ ") + return "\n".join(H) + + +def menu_group_choice(context, group_id=None, formsemestre_id=None): + """Un bête menu pour choisir un seul groupe + group_id est le groupe actuellement sélectionné. + Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes. + """ + if group_id: + group = sco_groups.get_group(context, group_id) + formsemestre_id = group["formsemestre_id"] + elif not formsemestre_id: + raise ValueError("missing formsemestre_id") + H = [ + """ + + + """ + ) + return "\n".join(H) + + +class DisplayedGroupsInfos(object): + """Container with attributes describing groups to display in the page + .groups_query_args : 'group_ids=xxx&group_ids=yyy' + .base_url : url de la requete, avec les groupes, sans les autres paramètres + .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste) + .members + .groups_titles + """ + + def __init__( + self, + context, + group_ids=[], # groupes specifies dans l'URL + formsemestre_id=None, + etat=None, + select_all_when_unspecified=False, + moduleimpl_id=None, # used to find formsemestre when unspecified + REQUEST=None, + ): + # log('DisplayedGroupsInfos %s' % group_ids) + if isinstance(group_ids, str): + if group_ids: + group_ids = [group_ids] # cas ou un seul parametre, pas de liste + else: + group_ids = [] + if not formsemestre_id and moduleimpl_id: + mods = sco_moduleimpl.do_moduleimpl_list( + context, moduleimpl_id=moduleimpl_id + ) + if len(mods) != 1: + raise ValueError("invalid moduleimpl_id") + formsemestre_id = mods[0]["formsemestre_id"] + + if not group_ids: # appel sans groupe (eg page accueil) + if not formsemestre_id: + raise Exception("missing parameter formsemestre_id or group_ids") + if select_all_when_unspecified: + group_ids = [sco_groups.get_default_group(formsemestre_id)] + else: + # selectionne le premier groupe trouvé, s'il y en a un + partition = sco_groups.get_partitions_list( + context, formsemestre_id, with_default=True + )[0] + groups = sco_groups.get_partition_groups(context, partition) + if groups: + group_ids = [groups[0]["group_id"]] + else: + group_ids = [sco_groups.get_default_group(formsemestre_id)] + + gq = [] + for group_id in group_ids: + gq.append("group_ids=" + group_id) + self.groups_query_args = "&".join(gq) + self.base_url = REQUEST.URL0 + "?" + self.groups_query_args + self.group_ids = group_ids + self.groups = [] + groups_titles = [] + self.members = [] + self.tous_les_etuds_du_sem = ( + False # affiche tous les etuds du semestre ? (si un seul semestre) + ) + self.sems = collections.OrderedDict() # formsemestre_id : sem + self.formsemestre = None + self.formsemestre_id = formsemestre_id + self.nbdem = 0 # nombre d'étudiants démissionnaires en tout + sem = None + selected_partitions = set() + for group_id in group_ids: + group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos( + context, group_id, etat=etat + ) + self.groups.append(group) + self.nbdem += nbdem + self.sems[sem["formsemestre_id"]] = sem + if not self.formsemestre_id: + self.formsemestre_id = sem["formsemestre_id"] + self.formsemestre = sem + self.members.extend(group_members) + groups_titles.append(group_tit) + if group["group_name"] == None: + self.tous_les_etuds_du_sem = True + else: + # liste les partitions explicitement sélectionnés (= des groupes de group_ids) + selected_partitions.add((group["numero"], group["partition_id"])) + + self.selected_partitions = [ + x[1] for x in sorted(list(selected_partitions)) + ] # -> [ partition_id ] + + if not self.formsemestre: # aucun groupe selectionne + self.formsemestre = sco_formsemestre.get_formsemestre( + context, formsemestre_id + ) + + self.sortuniq() + + if len(self.sems) > 1: + self.tous_les_etuds_du_sem = False # plusieurs semestres + if self.tous_les_etuds_du_sem: + if sem and sem["semestre_id"] >= 0: + self.groups_titles = "S%d" % sem["semestre_id"] + else: + self.groups_titles = "tous" + self.groups_filename = self.groups_titles + else: + self.groups_titles = ", ".join(groups_titles) + self.groups_filename = "_".join(groups_titles).replace(" ", "_") + # Sanitize filename: + self.groups_filename = scu.make_filename(self.groups_filename) + + # colonnes pour affichages nom des groupes: + # gère le cas où les étudiants appartiennent à des semestres différents + self.partitions = [] # les partitions, sans celle par defaut + for formsemestre_id in self.sems: + for partition in sco_groups.get_partitions_list(context, formsemestre_id): + if partition["partition_name"]: + self.partitions.append(partition) + + def sortuniq(self): + "Trie les étudiants (de plusieurs groupes) et elimine les doublons" + if (len(self.group_ids) <= 1) or len(self.members) <= 1: + return # on suppose que les etudiants d'un groupe sont deja triés + self.members.sort( + key=operator.itemgetter("nom_disp", "prenom") + ) # tri selon nom_usuel ou nom + to_remove = [] + T = self.members + for i in range(len(T) - 1, 0, -1): + if T[i - 1]["etudid"] == T[i]["etudid"]: + to_remove.append(i) + for i in to_remove: + del T[i] + + def get_form_elem(self): + """html hidden input with groups""" + H = [] + for group_id in self.group_ids: + H.append('' % group_id) + return "\n".join(H) + + +# Ancien ZScolar.group_list renommé ici en group_table +def groups_table( + context=None, + REQUEST=None, + groups_infos=None, # instance of DisplayedGroupsInfos + with_codes=0, + etat=None, + format="html", + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, +): + """liste etudiants inscrits dans ce semestre + format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape + """ + from app.scodoc import sco_report + + # log( + # "enter groups_table %s: %s" + # % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-")) + # ) + with_codes = int(with_codes) + with_paiement = int(with_paiement) + with_archives = int(with_archives) + with_annotations = int(with_annotations) + + base_url_np = groups_infos.base_url + "&with_codes=%s" % with_codes + base_url = ( + base_url_np + + "&with_paiement=%s&with_archives=%s&with_annotations=%s" + % (with_paiement, with_archives, with_annotations) + ) + # + columns_ids = ["civilite_str", "nom_disp", "prenom"] # colonnes a inclure + titles = { + "civilite_str": "Civ.", + "nom_disp": "Nom", + "prenom": "Prénom", + "email": "Mail", + "emailperso": "Personnel", + "etat": "Etat", + "etudid": "etudid", + "code_nip": "code_nip", + "code_ine": "code_ine", + "datefinalisationinscription_str": "Finalisation inscr.", + "paiementinscription_str": "Paiement", + "etudarchive": "Fichiers", + "annotations_str": "Annotations", + "etape": "Etape", + "semestre_groupe": "Semestre-Groupe", # pour Moodle + } + + # ajoute colonnes pour groupes + columns_ids.extend([p["partition_id"] for p in groups_infos.partitions]) + titles.update( + dict( + [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] + ) + ) + + if format != "html": # ne mentionne l'état que en Excel (style en html) + columns_ids.append("etat") + columns_ids.append("email") + columns_ids.append("emailperso") + + if format == "moodlecsv": + columns_ids = ["email", "semestre_groupe"] + + if with_codes: + columns_ids += ["etape", "etudid", "code_nip", "code_ine"] + if with_paiement: + columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] + if with_paiement or with_codes: + sco_portal_apogee.check_paiement_etuds(context, groups_infos.members) + if with_archives: + from app.scodoc import sco_archives_etud + + sco_archives_etud.add_archives_info_to_etud_list(context, groups_infos.members) + columns_ids += ["etudarchive"] + if with_annotations: + sco_etud.add_annotations_to_etud_list(context, groups_infos.members) + columns_ids += ["annotations_str"] + + if groups_infos.formsemestre["semestre_id"] >= 0: + moodle_sem_name = "S%d" % groups_infos.formsemestre["semestre_id"] + else: + moodle_sem_name = "A" # pas de semestre spécifié, que faire ? + moodle_groupenames = set() + # ajoute liens + for etud in groups_infos.members: + if etud["email"]: + etud["_email_target"] = "mailto:" + etud["email"] + else: + etud["_email_target"] = "" + if etud["emailperso"]: + etud["_emailperso_target"] = "mailto:" + etud["emailperso"] + else: + etud["_emailperso_target"] = "" + fiche_url = url_for( + "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] + ) + etud["_nom_disp_target"] = fiche_url + etud["_prenom_target"] = fiche_url + + etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) + + if etud["etat"] == "D": + etud["_css_row_class"] = "etuddem" + # et groupes: + for partition_id in etud["partitions"]: + etud[partition_id] = etud["partitions"][partition_id]["group_name"] + # Ajoute colonne pour moodle: semestre_groupe, de la forme S1-NomgroupeXXX + moodle_groupename = [] + if groups_infos.selected_partitions: + # il y a des groupes selectionnes, utilise leurs partitions + for partition_id in groups_infos.selected_partitions: + if partition_id in etud["partitions"]: + moodle_groupename.append( + etud["partitions"][partition_id]["group_name"] + ) + else: + # pas de groupes sélectionnés: prend le premier s'il y en a un + if etud["partitions"]: + for p in etud["partitions"].items(): # partitions is an OrderedDict + break + moodle_groupename = [p[1]["group_name"]] + else: + moodle_groupename = ["tous"] + moodle_groupenames |= set(moodle_groupename) + etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) + + if groups_infos.nbdem > 1: + s = "s" + else: + s = "" + + if format == "moodlecsv": + # de la forme S1-[FI][FA]-groupe.csv + if not moodle_groupenames: + moodle_groupenames = {"tous"} + filename = ( + moodle_sem_name + + "-" + + groups_infos.formsemestre["modalite"] + + "-" + + "+".join(sorted(moodle_groupenames)) + ) + else: + filename = "etudiants_%s" % groups_infos.groups_filename + + prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) + tab = GenTable( + rows=groups_infos.members, + columns_ids=columns_ids, + titles=titles, + caption="soit %d étudiants inscrits et %d démissionaire%s." + % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s), + base_url=base_url, + filename=filename, + pdf_link=False, # pas d'export pdf + html_sortable=True, + html_class="table_leftalign table_listegroupe", + xml_outer_tag="group_list", + xml_row_tag="etud", + text_fields_separator=prefs["moodle_csv_separator"], + text_with_titles=prefs["moodle_csv_with_headerline"], + preferences=prefs, + ) + # + if format == "html": + amail_inst = [ + x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" + ] + amail_perso = [ + x["emailperso"] + for x in groups_infos.members + if x["emailperso"] and x["etat"] != "D" + ] + + if len(groups_infos.members): + if groups_infos.tous_les_etuds_du_sem: + htitle = "Les %d étudiants inscrits" % len(groups_infos.members) + else: + htitle = "Groupe %s (%d étudiants)" % ( + groups_infos.groups_titles, + len(groups_infos.members), + ) + else: + htitle = "Aucun étudiant !" + H = [ + '
' '

', + htitle, + "", + ] + if groups_infos.members: + Of = [] + options = { + "with_paiement": "Paiement inscription", + "with_archives": "Fichiers archivés", + "with_annotations": "Annotations", + "with_codes": "Codes", + } + for option in options: + if locals().get(option, False): + selected = "selected" + else: + selected = "" + Of.append( + """""" + % (option, selected, options[option]) + ) + + H.extend( + [ + """ + + """, + ] + ) + H.append("

") + if groups_infos.members: + H.extend( + [ + tab.html(), + "") + + return "".join(H) + "
" + + elif ( + format == "pdf" + or format == "xml" + or format == "json" + or format == "xls" + or format == "moodlecsv" + ): + if format == "moodlecsv": + format = "csv" + return tab.make_page(context, format=format, REQUEST=REQUEST) + + elif format == "xlsappel": + xls = sco_excel.excel_feuille_listeappel(groups_infos.formsemestre, groups_infos.groups_titles, + groups_infos.members, partitions=groups_infos.partitions, + with_codes=with_codes, with_paiement=with_paiement, + server_name=REQUEST.BASE0) + filename = "liste_%s" % groups_infos.groups_filename + ".xlsx" + return sco_excel.send_excel_file(REQUEST, xls, filename) + elif format == "allxls": + # feuille Excel avec toutes les infos etudiants + if not groups_infos.members: + return "" + keys = [ + "etudid", + "code_nip", + "etat", + "civilite_str", + "nom", + "nom_usuel", + "prenom", + "inscriptionstr", + ] + if with_paiement: + keys.append("paiementinscription") + keys += [ + "email", + "emailperso", + "domicile", + "villedomicile", + "codepostaldomicile", + "paysdomicile", + "telephone", + "telephonemobile", + "fax", + "date_naissance", + "lieu_naissance", + "bac", + "specialite", + "annee_bac", + "nomlycee", + "villelycee", + "codepostallycee", + "codelycee", + "type_admission", + "boursier_prec", + "debouche", + "parcours", + "codeparcours", + ] + titles = keys[:] + other_partitions = sco_groups.get_group_other_partitions( + context, groups_infos.groups[0] + ) + keys += [p["partition_id"] for p in other_partitions] + titles += [p["partition_name"] for p in other_partitions] + # remplis infos lycee si on a que le code lycée + # et ajoute infos inscription + for m in groups_infos.members: + etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] + m.update(etud) + sco_etud.etud_add_lycee_infos(etud) + # et ajoute le parcours + Se = sco_parcours_dut.SituationEtudParcours( + context, etud, groups_infos.formsemestre_id + ) + m["parcours"] = Se.get_parcours_descr() + m["codeparcours"], _ = sco_report.get_codeparcoursetud(context, etud) + + def dicttakestr(d, keys): + r = [] + for k in keys: + r.append(str(d.get(k, ""))) + return r + + L = [dicttakestr(m, keys) for m in groups_infos.members] + title = "etudiants_%s" % groups_infos.groups_filename + xls = sco_excel.Excel_SimpleTable(titles=titles, lines=L, SheetName=title) + filename = title + ".xls" + return sco_excel.send_excel_file(REQUEST, xls, filename) + else: + raise ValueError("unsupported format") + + +def tab_absences_html(context, groups_infos, etat=None, REQUEST=None): + """contenu du tab "absences et feuilles diverses" """ + authuser = REQUEST.AUTHENTICATED_USER + H = ['
'] + if not groups_infos.members: + return "".join(H) + "

Aucun étudiant !

" + H.extend( + [ + "

Absences

", + '", + "

Feuilles

", + '", + ] + ) + + H.append('

Opérations diverses

") + return "".join(H) + + +def tab_photos_html(context, groups_infos, etat=None, REQUEST=None): + """contenu du tab "photos" """ + from app.scodoc import sco_trombino + + if not groups_infos.members: + return '

Aucun étudiant !

' + + return sco_trombino.trombino_html(context, groups_infos, REQUEST=REQUEST) + + +def form_choix_jour_saisie_hebdo( + context, groups_infos, moduleimpl_id=None, REQUEST=None +): + """Formulaire choix jour semaine pour saisie.""" + authuser = REQUEST.AUTHENTICATED_USER + if not authuser.has_permission(Permission.ScoAbsChange): + return "" + sem = groups_infos.formsemestre + first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() + today_idx = datetime.date.today().weekday() + + FA = [] # formulaire avec menu saisi absences + FA.append( + '
' + ) + FA.append('' % sem) + FA.append(groups_infos.get_form_elem()) + if moduleimpl_id: + FA.append( + '' % moduleimpl_id + ) + FA.append('') + + FA.append( + """""" + ) + FA.append("""") + FA.append("
") + return "\n".join(FA) + + +# Ajout Le Havre +# Formulaire saisie absences semaine +def form_choix_saisie_semaine(context, groups_infos, REQUEST=None): + authuser = REQUEST.AUTHENTICATED_USER + if not authuser.has_permission(Permission.ScoAbsChange): + return "" + # construit l'URL "destination" + # (a laquelle on revient apres saisie absences) + query_args = cgi.parse_qs(REQUEST.QUERY_STRING) + moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] + if "head_message" in query_args: + del query_args["head_message"] + destination = "%s?%s" % ( + REQUEST.URL, + six.moves.urllib.parse.urlencode(query_args, True), + ) + destination = destination.replace( + "%", "%%" + ) # car ici utilisee dans un format string ! + + DateJour = time.strftime("%d/%m/%Y") + datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() + FA = [] # formulaire avec menu saisie hebdo des absences + FA.append('
') + FA.append('' % datelundi) + FA.append('' % moduleimpl_id) + FA.append('' % destination) + FA.append(groups_infos.get_form_elem()) + FA.append('') + FA.append("
") + return "\n".join(FA) + + +def export_groups_as_moodle_csv(context, formsemestre_id=None, REQUEST=None): + """Export all students/groups, in a CSV format suitable for Moodle + Each (student,group) will be listed on a separate line + jo@univ.fr,S3-A + jo@univ.fr,S3-B1 + if jo belongs to group A in a partition, and B1 in another one. + Caution: if groups in different partitions share the same name, there will be + duplicates... should we prefix the group names with the partition's name ? + """ + if not formsemestre_id: + raise ScoValueError("missing parameter: formsemestre_id") + _, partitions_etud_groups = sco_groups.get_formsemestre_groups( + context, formsemestre_id, with_default=True + ) + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + moodle_sem_name = sem["session_id"] + + columns_ids = ("email", "semestre_groupe") + T = [] + for partition_id in partitions_etud_groups: + partition = sco_groups.get_partition(context, partition_id) + members = partitions_etud_groups[partition_id] + for etudid in members: + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + group_name = members[etudid]["group_name"] + elts = [moodle_sem_name] + if partition["partition_name"]: + elts.append(partition["partition_name"]) + if group_name: + elts.append(group_name) + T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) + # Make table + prefs = sco_preferences.SemPreferences(formsemestre_id) + tab = GenTable( + rows=T, + columns_ids=("email", "semestre_groupe"), + filename=moodle_sem_name + "-moodle", + titles={x: x for x in columns_ids}, + text_fields_separator=prefs["moodle_csv_separator"], + text_with_titles=prefs["moodle_csv_with_headerline"], + preferences=prefs, + ) + return tab.make_page(context, format="csv", REQUEST=REQUEST) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 7a520fc5..d3196a2d 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -1,874 +1,875 @@ -# -*- 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 -# -############################################################################## - - -""" Common definitions -""" -import base64 -import bisect -import copy -import datetime -import json -from hashlib import md5 -import numbers -import os -import pydot -import re -import six -import six.moves._thread -import sys -import time -import types -import unicodedata -import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error -import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse -from xml.etree import ElementTree - -STRING_TYPES = six.string_types - -from PIL import Image as PILImage - -from flask import g, url_for - -from scodoc_manager import sco_mgr - -from config import Config - -from app.scodoc.notes_log import log -from app.scodoc.sco_vdi import ApoEtapeVDI -from app.scodoc.sco_xml import quote_xml_attr -from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL -from app.scodoc import sco_exceptions -from app.scodoc import sco_xml -from app.scodoc import VERSION - - -# ----- CALCUL ET PRESENTATION DES NOTES -NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis -NOTES_MIN = 0.0 # valeur minimale admise pour une note (sauf malus, dans [-20, 20]) -NOTES_MAX = 1000.0 -NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes -NOTES_SUPPRESS = -1001.0 # note a supprimer -NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) - - -# Types de modules -MODULE_STANDARD = 0 -MODULE_MALUS = 1 - -MALUS_MAX = 20.0 -MALUS_MIN = -20.0 - -APO_MISSING_CODE_STR = "----" # shown in HTML pages in place of missing code Apogée -EDIT_NB_ETAPES = 6 # Nombre max de codes étapes / semestre presentés dans l'UI - -IT_SITUATION_MISSING_STR = ( - "____" # shown on ficheEtud (devenir) in place of empty situation -) - -RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente - -# borne supérieure de chaque mention -NOTES_MENTIONS_TH = ( - NOTES_TOLERANCE, - 7.0, - 10.0, - 12.0, - 14.0, - 16.0, - 18.0, - 20.0 + NOTES_TOLERANCE, -) -NOTES_MENTIONS_LABS = ( - "Nul", - "Faible", - "Insuffisant", - "Passable", - "Assez bien", - "Bien", - "Très bien", - "Excellent", -) - -EVALUATION_NORMALE = 0 -EVALUATION_RATTRAPAGE = 1 -EVALUATION_SESSION2 = 2 - - -def fmt_note(val, note_max=None, keep_numeric=False): - """conversion note en str pour affichage dans tables HTML ou PDF. - Si keep_numeric, laisse les valeur numeriques telles quelles (pour export Excel) - """ - if val is None: - return "ABS" - if val == NOTES_NEUTRALISE: - return "EXC" # excuse, note neutralise - if val == NOTES_ATTENTE: - return "ATT" # attente, note neutralisee - if isinstance(val, float) or isinstance(val, int): - if note_max != None and note_max > 0: - val = val * 20.0 / note_max - if keep_numeric: - return val - else: - s = "%2.2f" % round(float(val), 2) # 2 chiffres apres la virgule - s = "0" * (5 - len(s)) + s # padding: 0 à gauche pour longueur 5: "12.34" - return s - else: - return val.replace("NA0", "-") # notes sans le NA0 - - -def fmt_coef(val): - """Conversion valeur coefficient (float) en chaine""" - if val < 0.01: - return "%g" % val # unusually small value - return "%g" % round(val, 2) - - -def fmt_abs(val): - """Conversion absences en chaine. val est une list [nb_abs_total, nb_abs_justifiees - => NbAbs / Nb_justifiees - """ - return "%s / %s" % (val[0], val[1]) - - -def isnumber(x): - "True if x is a number (int, float, etc.)" - return isinstance(x, numbers.Number) - - -def join_words(*words): - words = [str(w).strip() for w in words if w is not None] - return " ".join([w for w in words if w]) - - -def get_mention(moy): - """Texte "mention" en fonction de la moyenne générale""" - try: - moy = float(moy) - except: - return "" - return NOTES_MENTIONS_LABS[bisect.bisect_right(NOTES_MENTIONS_TH, moy)] - - -class DictDefault(dict): # obsolete, use collections.defaultdict - """A dictionnary with default value for all keys - Each time a non existent key is requested, it is added to the dict. - (used in python 2.4, can't use new __missing__ method) - """ - - defaultvalue = 0 - - def __init__(self, defaultvalue=0, kv_dict={}): - dict.__init__(self) - self.defaultvalue = defaultvalue - self.update(kv_dict) - - def __getitem__(self, k): - if k in self: - return self.get(k) - value = copy.copy(self.defaultvalue) - self[k] = value - return value - - -class WrapDict(object): - """Wrap a dict so that getitem returns '' when values are None""" - - def __init__(self, adict, NoneValue=""): - self.dict = adict - self.NoneValue = NoneValue - - def __getitem__(self, key): - value = self.dict[key] - if value is None: - return self.NoneValue - else: - return value - - -def group_by_key(d, key): - gr = DictDefault(defaultvalue=[]) - for e in d: - gr[e[key]].append(e) - return gr - - -# ----- Global lock for critical sections (except notes_tables caches) -GSL = six.moves._thread.allocate_lock() # Global ScoDoc Lock - -SCODOC_DIR = Config.SCODOC_DIR - -# ----- Repertoire "config" modifiable -# /opt/scodoc-data/config -SCODOC_CFG_DIR = os.path.join(Config.SCODOC_VAR_DIR, "config") -# ----- Version information -SCODOC_VERSION_DIR = os.path.join(SCODOC_CFG_DIR, "version") -# ----- Repertoire tmp : /opt/scodoc-data/tmp -SCO_TMP_DIR = os.path.join(Config.SCODOC_VAR_DIR, "tmp") -if not os.path.exists(SCO_TMP_DIR): - os.mkdir(SCO_TMP_DIR, 0o755) -# ----- Les logos: /opt/scodoc-data/config/logos -SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") - -# ----- Les outils distribués -SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools") - - -# ----- Lecture du fichier de configuration -from app.scodoc import sco_config -from app.scodoc import sco_config_load - -sco_config_load.load_local_configuration(SCODOC_CFG_DIR) -CONFIG = sco_config.CONFIG -if hasattr(CONFIG, "CODES_EXPL"): - CODES_EXPL.update( - CONFIG.CODES_EXPL - ) # permet de customiser les explications de codes - -if CONFIG.CUSTOM_HTML_HEADER: - CUSTOM_HTML_HEADER = open(CONFIG.CUSTOM_HTML_HEADER).read() -else: - CUSTOM_HTML_HEADER = "" - -if CONFIG.CUSTOM_HTML_HEADER_CNX: - CUSTOM_HTML_HEADER_CNX = open(CONFIG.CUSTOM_HTML_HEADER_CNX).read() -else: - CUSTOM_HTML_HEADER_CNX = "" - -if CONFIG.CUSTOM_HTML_FOOTER: - CUSTOM_HTML_FOOTER = open(CONFIG.CUSTOM_HTML_FOOTER).read() -else: - CUSTOM_HTML_FOOTER = "" - -if CONFIG.CUSTOM_HTML_FOOTER_CNX: - CUSTOM_HTML_FOOTER_CNX = open(CONFIG.CUSTOM_HTML_FOOTER_CNX).read() -else: - CUSTOM_HTML_FOOTER_CNX = "" - -SCO_ENCODING = "utf-8" # used by Excel, XML, PDF, ... - - -SCO_DEFAULT_SQL_USER = "scodoc" # should match Zope process UID -SCO_DEFAULT_SQL_PORT = "5432" -SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT - -# Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés: -SCO_WEBSITE = "https://scodoc.org" -SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur" -SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces" -SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr" -SCO_USERS_LIST = "notes@listes.univ-paris13.fr" - -# Mails avec exceptions (erreurs) anormales envoyés à cette adresse: -# mettre '' pour désactiver completement l'envois de mails d'erreurs. -# (ces mails sont précieux pour corriger les erreurs, ne les désactiver que si -# vous avez de bonnes raisons de le faire: vous pouvez me contacter avant) -SCO_EXC_MAIL = "scodoc-exception@viennet.net" - -# L'adresse du mainteneur (non utilisée automatiquement par ScoDoc: ne pas changer) -SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer - -# Adresse pour l'envoi des dumps (pour assistance technnique): -# ne pas changer (ou vous perdez le support) -SCO_DUMP_UP_URL = "https://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/upload-dump" - -CSV_FIELDSEP = ";" -CSV_LINESEP = "\n" -CSV_MIMETYPE = "text/comma-separated-values" -XLS_MIMETYPE = "application/vnd.ms-excel" -PDF_MIMETYPE = "application/pdf" -XML_MIMETYPE = "text/xml" -JSON_MIMETYPE = "application/json" - -LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png") # remind that PIL does not read pdf - -# Admissions des étudiants -# Différents types de voies d'admission: -# (stocké en texte libre dans la base, mais saisie par menus pour harmoniser) -TYPE_ADMISSION_DEFAULT = "Inconnue" -TYPES_ADMISSION = (TYPE_ADMISSION_DEFAULT, "APB", "APB-PC", "CEF", "Direct") - -BULLETINS_VERSIONS = ("short", "selectedevals", "long") - -# Support for ScoDoc7 compatibility -def get_dept_id(): - if g.scodoc_dept in sco_mgr.get_dept_ids(): - return g.scodoc_dept - raise sco_exceptions.ScoInvalidDept("département invalide: %s" % g.scodoc_dept) - - -def get_db_cnx_string(scodoc_dept=None): - return "dbname=SCO" + (scodoc_dept or g.scodoc_dept) - - -def ScoURL(): - """base URL for this sco instance. - e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite - = page accueil département - """ - return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] - - -def NotesURL(): - """URL of Notes - e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Notes - = url de base des méthodes de notes - (page accueil programmes). - """ - return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")] - - -def EntreprisesURL(): - """URL of Enterprises - e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Entreprises - = url de base des requêtes de ZEntreprises - et page accueil Entreprises - """ - return "NotImplemented" - # url_for("entreprises.index_html", scodoc_dept=g.scodoc_dept)[ - # : -len("/index_html") - # ] - - -def AbsencesURL(): - """URL of Absences""" - return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] - - -def UsersURL(): - """URL of Users - e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users - = url de base des requêtes ZScoUsers - et page accueil users - """ - return url_for("users.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")] - - -# ---- Simple python utilities - - -def simplesqlquote(s, maxlen=50): - """simple SQL quoting to avoid most SQL injection attacks. - Note: we use this function in the (rare) cases where we have to - construct SQL code manually""" - s = s[:maxlen] - s.replace("'", r"\'") - s.replace(";", r"\;") - for bad in ("select", "drop", ";", "--", "insert", "delete", "xp_"): - s = s.replace(bad, "") - return s - - -def unescape_html(s): - """un-escape html entities""" - s = s.strip().replace("&", "&") - s = s.replace("<", "<") - s = s.replace(">", ">") - return s - - -# test if obj is iterable (but not a string) -isiterable = lambda obj: getattr(obj, "__iter__", False) - - -def unescape_html_dict(d): - """un-escape all dict values, recursively""" - try: - indices = list(d.keys()) - except: - indices = list(range(len(d))) - for k in indices: - v = d[k] - if isinstance(v, bytes): - d[k] = unescape_html(v) - elif isiterable(v): - unescape_html_dict(v) - - -# Expressions used to check noms/prenoms -FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]") -ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE) - - -def is_valid_code_nip(s): - """True si s peut être un code NIP: au moins 6 chiffres décimaux""" - if not s: - return False - return re.match(r"^[0-9]{6,32}$", s) - - -def strnone(s): - "convert s to string, '' if s is false" - if s: - return str(s) - else: - return "" - - -def stripquotes(s): - "strip s from spaces and quotes" - s = s.strip() - if s and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")): - s = s[1:-1] - return s - - -def suppress_accents(s): - "remove accents and suppress non ascii characters from string s" - return ( - unicodedata.normalize("NFD", s).encode("ascii", "ignore").decode(SCO_ENCODING) - ) - - -def sanitize_string(s): - """s is an ordinary string, encoding given by SCO_ENCODING" - suppress accents and chars interpreted in XML - Irreversible (not a quote) - - For ids and some filenames - """ - # Table suppressing some chars: - trans = str.maketrans("", "", "'`\"<>!&\\ ") - return suppress_accents(s.translate(trans)).replace(" ", "_").replace("\t", "_") - - -_BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\") - - -def make_filename(name): - """Try to convert name to a reasonable filename - without spaces, (back)slashes, : and without accents - """ - return suppress_accents(name.translate(_BAD_FILENAME_CHARS)).replace(" ", "_") - - -VALID_CARS = ( - "-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.!" # no / ! -) -VALID_CARS_SET = set(VALID_CARS) -VALID_EXP = re.compile("^[" + VALID_CARS + "]+$") - - -def sanitize_filename(filename): - """Keep only valid chars - used for archives filenames - """ - sane = "".join([c for c in filename if c in VALID_CARS_SET]) - if len(sane) < 2: - sane = time.strftime("%Y-%m-%d-%H%M%S") + "-" + sane - return sane - - -def is_valid_filename(filename): - """True if filename is safe""" - return VALID_EXP.match(filename) - - -def sendCSVFile(REQUEST, data, filename): - """publication fichier. - (on ne doit rien avoir émis avant, car ici sont générés les entetes) - """ - filename = ( - unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_") - ) - REQUEST.RESPONSE.setHeader("content-type", CSV_MIMETYPE) - REQUEST.RESPONSE.setHeader( - "content-disposition", 'attachment; filename="%s"' % filename - ) - return data - - -def sendPDFFile(REQUEST, data, filename): - filename = ( - unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_") - ) - if REQUEST: - REQUEST.RESPONSE.setHeader("content-type", PDF_MIMETYPE) - REQUEST.RESPONSE.setHeader( - "content-disposition", 'attachment; filename="%s"' % filename - ) - return data - - -class ScoDocJSONEncoder(json.JSONEncoder): - def default(self, o): # pylint: disable=E0202 - if isinstance(o, (datetime.date, datetime.datetime)): - return o.isoformat() - elif isinstance(o, ApoEtapeVDI): - return str(o) - else: - return json.JSONEncoder.default(self, o) - - -def sendJSON(REQUEST, data): - js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) - if REQUEST: - REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE) - return js - - -def sendXML(REQUEST, data, tagname=None, force_outer_xml_tag=True): - if type(data) != list: - data = [data] # always list-of-dicts - if force_outer_xml_tag: - root_tagname = tagname + "_list" - data = [{root_tagname: data}] - doc = sco_xml.simple_dictlist2xml(data, tagname=tagname) - - if REQUEST: - REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE) - return doc - - -def sendResult(REQUEST, data, name=None, format=None, force_outer_xml_tag=True): - if (format is None) or (format == "html"): - return data - elif format == "xml": # name is outer tagname - return sendXML( - REQUEST, data, tagname=name, force_outer_xml_tag=force_outer_xml_tag - ) - elif format == "json": - return sendJSON(REQUEST, data) - else: - raise ValueError("invalid format: %s" % format) - - -def get_scodoc_version(): - "return a string identifying ScoDoc version" - return os.popen("cd %s; ./get_scodoc_version.sh -s" % SCO_TOOLS_DIR).read().strip() - - -def check_scodoc7_password(scodoc7_hash, password): - """Check a password vs scodoc7 hash - used only during old databases migrations""" - m = md5() - m.update(password.encode("utf-8")) - # encodestring à remplacer par encodebytes #py3 - h = base64.encodestring(m.digest()).decode("utf-8").strip() - return h == scodoc7_hash - - -# Simple string manipulations -# not necessary anymore in Python 3 ! TODO remove -def strupper(s): - return s.upper() - # return s.decode(SCO_ENCODING).upper().encode(SCO_ENCODING) - - -# XXX fonctions inutiles en Python3 ! -def strlower(s): - return s.lower() - - -def strcapitalize(s): - return s.capitalize() - - -def abbrev_prenom(prenom): - "Donne l'abreviation d'un prenom" - # un peu lent, mais espère traiter tous les cas - # Jean -> J. - # Charles -> Ch. - # Jean-Christophe -> J.-C. - # Marie Odile -> M. O. - prenom = prenom.replace(".", " ").strip() - if not prenom: - return "" - d = prenom[:3].upper() - if d == "CHA": - abrv = "Ch." # 'Charles' donne 'Ch.' - i = 3 - else: - abrv = prenom[0].upper() + "." - i = 1 - n = len(prenom) - while i < n: - c = prenom[i] - if c == " " or c == "-" and i < n - 1: - sep = c - i += 1 - # gobbe tous les separateurs - while i < n and (prenom[i] == " " or prenom[i] == "-"): - if prenom[i] == "-": - sep = "-" - i += 1 - if i < n: - abrv += sep + prenom[i].upper() + "." - i += 1 - return abrv - - -# -def timedate_human_repr(): - "representation du temps courant pour utilisateur: a localiser" - return time.strftime("%d/%m/%Y à %Hh%M") - - -def annee_scolaire_repr(year, month): - """representation de l'annee scolaire : '2009 - 2010' - à partir d'une date. - """ - if month > 7: # apres le 1er aout - return "%s - %s" % (year, year + 1) - else: - return "%s - %s" % (year - 1, year) - - -def annee_scolaire_debut(year, month): - """Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord...""" - if int(month) > 7: - return int(year) - else: - return int(year) - 1 - - -def sem_decale_str(sem): - """'D' si semestre decalé, ou ''""" - # considère "décalé" les semestre impairs commençant entre janvier et juin - # et les pairs entre juillet et decembre - if sem["semestre_id"] <= 0: - return "" - if (sem["semestre_id"] % 2 and sem["mois_debut_ord"] <= 6) or ( - not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6 - ): - return "D" - else: - return "" - - -def is_valid_mail(email): - """True if well-formed email address""" - return re.match(r"^.+@.+\..{2,3}$", email) - - -def graph_from_edges(edges, graph_name="mygraph"): - """Crée un graph pydot - à partir d'une liste d'arêtes [ (n1, n2), (n2, n3), ... ] - où n1, n2, ... sont des chaînes donnant l'id des nœuds. - - Fonction remplaçant celle de pydot qui est buggée. - """ - nodes = set([it for tup in edges for it in tup]) - graph = pydot.Dot(graph_name) - for n in nodes: - graph.add_node(pydot.Node(n)) - for e in edges: - graph.add_edge(pydot.Edge(src=e[0], dst=e[1])) - return graph - - -ICONSIZES = {} # name : (width, height) cache image sizes - - -def icontag(name, file_format="png", **attrs): - """tag HTML pour un icone. - (dans les versions anterieures on utilisait Zope) - Les icones sont des fichiers PNG dans .../static/icons - Si la taille (width et height) n'est pas spécifiée, lit l'image - pour la mesurer (et cache le résultat). - """ - if ("width" not in attrs) or ("height" not in attrs): - if name not in ICONSIZES: - img_file = os.path.join( - Config.SCODOC_DIR, - "app/static/icons/%s.%s" - % ( - name, - file_format, - ), - ) - im = PILImage.open(img_file) - width, height = im.size[0], im.size[1] - ICONSIZES[name] = (width, height) # cache - else: - width, height = ICONSIZES[name] - attrs["width"] = width - attrs["height"] = height - if "border" not in attrs: - attrs["border"] = 0 - if "alt" not in attrs: - attrs["alt"] = "logo %s" % name - s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs]) - return '' % ( - name, - s, - name, - file_format, - ) - - -ICON_PDF = icontag("pdficon16x20_img", title="Version PDF") -ICON_XLS = icontag("xlsicon_img", title="Version tableur") - - -def sort_dates(L, reverse=False): - """Return sorted list of dates, allowing None items (they are put at the beginning)""" - mindate = datetime.datetime(datetime.MINYEAR, 1, 1) - try: - return sorted(L, key=lambda x: x or mindate, reverse=reverse) - except: - # Helps debugging - log("sort_dates( %s )" % L) - raise - - -def query_portal(req, msg="Portail Apogee", timeout=3): - """Retreives external data using HTTP request - (used to connect to Apogee portal, or ScoDoc server) - returns a string, "" on error - """ - log("query_portal: %s" % req) - - try: - f = six.moves.urllib.request.urlopen(req, timeout=timeout) # seconds / request - except: - log("query_portal: can't connect to %s" % msg) - return "" - try: - data = f.read() - except: - log("query_portal: error reading from %s" % msg) - data = "" - - return data - - -def AnneeScolaire(REQUEST=None): # TODO remplacer REQUEST #sco8 - "annee de debut de l'annee scolaire courante" - if REQUEST and "sco_year" in REQUEST.form: - year = REQUEST.form["sco_year"] - try: - year = int(year) - if year > 1900 and year < 2999: - return year - except: - pass - t = time.localtime() - year, month = t[0], t[1] - if month < 8: # le "pivot" est le 1er aout - year = year - 1 - return year - - -def log_unknown_etud(REQUEST=None, format="html"): - """Log request: cas ou getEtudInfo n'a pas ramene de resultat""" - etudid = REQUEST.form.get("etudid", "?") - code_nip = REQUEST.form.get("code_nip", "?") - code_ine = REQUEST.form.get("code_ine", "?") - log( - "unknown student: etudid=%s code_nip=%s code_ine=%s" - % (etudid, code_nip, code_ine) - ) - return _sco_error_response("unknown student", format=format, REQUEST=REQUEST) - - -# XXX #sco8 à tester ou ré-écrire -def _sco_error_response(msg, format="html", REQUEST=None): - """Send an error message to the client, in html or xml format.""" - REQUEST.RESPONSE.setStatus(404, reason=msg) - if format == "html" or format == "pdf": - raise sco_exceptions.ScoValueError(msg) - elif format == "xml": - REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE) - doc = ElementTree.Element("error", msg=msg) - return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(SCO_ENCODING) - elif format == "json": - REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE) - return "undefined" # XXX voir quoi faire en cas d'erreur json - else: - raise ValueError("ScoErrorResponse: invalid format") - - -def return_text_if_published(val, REQUEST): - """Pour les méthodes publiées qui ramènent soit du texte (HTML) soit du JSON - sauf quand elles sont appellées depuis python. - La présence de l'argument REQUEST indique la publication. - """ - if REQUEST and not isinstance(val, STRING_TYPES): - return sendJSON(REQUEST, val) - return val - - -def confirm_dialog( - context, - message="

Confirmer ?

", - OK="OK", - Cancel="Annuler", - dest_url="", - cancel_url="", - target_variable="dialog_confirmed", - parameters={}, - add_headers=True, # complete page - REQUEST=None, # required - helpmsg=None, -): - from app.scodoc import html_sco_header - - # dialog de confirmation simple - parameters[target_variable] = 1 - # Attention: la page a pu etre servie en GET avec des parametres - # si on laisse l'url "action" vide, les parametres restent alors que l'on passe en POST... - if not dest_url: - dest_url = REQUEST.URL - # strip remaining parameters from destination url: - dest_url = six.moves.urllib.parse.splitquery(dest_url)[0] - H = [ - """
""" % dest_url, - message, - """""" % OK, - ] - if cancel_url: - H.append( - """""" - % (Cancel, cancel_url) - ) - for param in parameters.keys(): - if parameters[param] is None: - parameters[param] = "" - if type(parameters[param]) == type([]): - for e in parameters[param]: - H.append('' % (param, e)) - else: - H.append( - '' - % (param, parameters[param]) - ) - H.append("
") - if helpmsg: - H.append('

' + helpmsg + "

") - if add_headers and REQUEST: - return ( - html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() - ) - else: - return "\n".join(H) +# -*- 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 +# +############################################################################## + + +""" Common definitions +""" +import base64 +import bisect +import copy +import datetime +import json +from hashlib import md5 +import numbers +import os +import pydot +import re +import six +import six.moves._thread +import sys +import time +import types +import unicodedata +import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import six.moves.urllib.request, six.moves.urllib.error, six.moves.urllib.parse +from xml.etree import ElementTree + +STRING_TYPES = six.string_types + +from PIL import Image as PILImage + +from flask import g, url_for + +from scodoc_manager import sco_mgr + +from config import Config + +from app.scodoc.notes_log import log +from app.scodoc.sco_vdi import ApoEtapeVDI +from app.scodoc.sco_xml import quote_xml_attr +from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL +from app.scodoc import sco_exceptions +from app.scodoc import sco_xml +from app.scodoc import VERSION + + +# ----- CALCUL ET PRESENTATION DES NOTES +NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis +NOTES_MIN = 0.0 # valeur minimale admise pour une note (sauf malus, dans [-20, 20]) +NOTES_MAX = 1000.0 +NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes +NOTES_SUPPRESS = -1001.0 # note a supprimer +NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) + + +# Types de modules +MODULE_STANDARD = 0 +MODULE_MALUS = 1 + +MALUS_MAX = 20.0 +MALUS_MIN = -20.0 + +APO_MISSING_CODE_STR = "----" # shown in HTML pages in place of missing code Apogée +EDIT_NB_ETAPES = 6 # Nombre max de codes étapes / semestre presentés dans l'UI + +IT_SITUATION_MISSING_STR = ( + "____" # shown on ficheEtud (devenir) in place of empty situation +) + +RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente + +# borne supérieure de chaque mention +NOTES_MENTIONS_TH = ( + NOTES_TOLERANCE, + 7.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0 + NOTES_TOLERANCE, +) +NOTES_MENTIONS_LABS = ( + "Nul", + "Faible", + "Insuffisant", + "Passable", + "Assez bien", + "Bien", + "Très bien", + "Excellent", +) + +EVALUATION_NORMALE = 0 +EVALUATION_RATTRAPAGE = 1 +EVALUATION_SESSION2 = 2 + + +def fmt_note(val, note_max=None, keep_numeric=False): + """conversion note en str pour affichage dans tables HTML ou PDF. + Si keep_numeric, laisse les valeur numeriques telles quelles (pour export Excel) + """ + if val is None: + return "ABS" + if val == NOTES_NEUTRALISE: + return "EXC" # excuse, note neutralise + if val == NOTES_ATTENTE: + return "ATT" # attente, note neutralisee + if isinstance(val, float) or isinstance(val, int): + if note_max != None and note_max > 0: + val = val * 20.0 / note_max + if keep_numeric: + return val + else: + s = "%2.2f" % round(float(val), 2) # 2 chiffres apres la virgule + s = "0" * (5 - len(s)) + s # padding: 0 à gauche pour longueur 5: "12.34" + return s + else: + return val.replace("NA0", "-") # notes sans le NA0 + + +def fmt_coef(val): + """Conversion valeur coefficient (float) en chaine""" + if val < 0.01: + return "%g" % val # unusually small value + return "%g" % round(val, 2) + + +def fmt_abs(val): + """Conversion absences en chaine. val est une list [nb_abs_total, nb_abs_justifiees + => NbAbs / Nb_justifiees + """ + return "%s / %s" % (val[0], val[1]) + + +def isnumber(x): + "True if x is a number (int, float, etc.)" + return isinstance(x, numbers.Number) + + +def join_words(*words): + words = [str(w).strip() for w in words if w is not None] + return " ".join([w for w in words if w]) + + +def get_mention(moy): + """Texte "mention" en fonction de la moyenne générale""" + try: + moy = float(moy) + except: + return "" + return NOTES_MENTIONS_LABS[bisect.bisect_right(NOTES_MENTIONS_TH, moy)] + + +class DictDefault(dict): # obsolete, use collections.defaultdict + """A dictionnary with default value for all keys + Each time a non existent key is requested, it is added to the dict. + (used in python 2.4, can't use new __missing__ method) + """ + + defaultvalue = 0 + + def __init__(self, defaultvalue=0, kv_dict={}): + dict.__init__(self) + self.defaultvalue = defaultvalue + self.update(kv_dict) + + def __getitem__(self, k): + if k in self: + return self.get(k) + value = copy.copy(self.defaultvalue) + self[k] = value + return value + + +class WrapDict(object): + """Wrap a dict so that getitem returns '' when values are None""" + + def __init__(self, adict, NoneValue=""): + self.dict = adict + self.NoneValue = NoneValue + + def __getitem__(self, key): + value = self.dict[key] + if value is None: + return self.NoneValue + else: + return value + + +def group_by_key(d, key): + gr = DictDefault(defaultvalue=[]) + for e in d: + gr[e[key]].append(e) + return gr + + +# ----- Global lock for critical sections (except notes_tables caches) +GSL = six.moves._thread.allocate_lock() # Global ScoDoc Lock + +SCODOC_DIR = Config.SCODOC_DIR + +# ----- Repertoire "config" modifiable +# /opt/scodoc-data/config +SCODOC_CFG_DIR = os.path.join(Config.SCODOC_VAR_DIR, "config") +# ----- Version information +SCODOC_VERSION_DIR = os.path.join(SCODOC_CFG_DIR, "version") +# ----- Repertoire tmp : /opt/scodoc-data/tmp +SCO_TMP_DIR = os.path.join(Config.SCODOC_VAR_DIR, "tmp") +if not os.path.exists(SCO_TMP_DIR): + os.mkdir(SCO_TMP_DIR, 0o755) +# ----- Les logos: /opt/scodoc-data/config/logos +SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") + +# ----- Les outils distribués +SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools") + + +# ----- Lecture du fichier de configuration +from app.scodoc import sco_config +from app.scodoc import sco_config_load + +sco_config_load.load_local_configuration(SCODOC_CFG_DIR) +CONFIG = sco_config.CONFIG +if hasattr(CONFIG, "CODES_EXPL"): + CODES_EXPL.update( + CONFIG.CODES_EXPL + ) # permet de customiser les explications de codes + +if CONFIG.CUSTOM_HTML_HEADER: + CUSTOM_HTML_HEADER = open(CONFIG.CUSTOM_HTML_HEADER).read() +else: + CUSTOM_HTML_HEADER = "" + +if CONFIG.CUSTOM_HTML_HEADER_CNX: + CUSTOM_HTML_HEADER_CNX = open(CONFIG.CUSTOM_HTML_HEADER_CNX).read() +else: + CUSTOM_HTML_HEADER_CNX = "" + +if CONFIG.CUSTOM_HTML_FOOTER: + CUSTOM_HTML_FOOTER = open(CONFIG.CUSTOM_HTML_FOOTER).read() +else: + CUSTOM_HTML_FOOTER = "" + +if CONFIG.CUSTOM_HTML_FOOTER_CNX: + CUSTOM_HTML_FOOTER_CNX = open(CONFIG.CUSTOM_HTML_FOOTER_CNX).read() +else: + CUSTOM_HTML_FOOTER_CNX = "" + +SCO_ENCODING = "utf-8" # used by Excel, XML, PDF, ... + + +SCO_DEFAULT_SQL_USER = "scodoc" # should match Zope process UID +SCO_DEFAULT_SQL_PORT = "5432" +SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT + +# Valeurs utilisées pour affichage seulement, pas de requetes ni de mails envoyés: +SCO_WEBSITE = "https://scodoc.org" +SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur" +SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces" +SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr" +SCO_USERS_LIST = "notes@listes.univ-paris13.fr" + +# Mails avec exceptions (erreurs) anormales envoyés à cette adresse: +# mettre '' pour désactiver completement l'envois de mails d'erreurs. +# (ces mails sont précieux pour corriger les erreurs, ne les désactiver que si +# vous avez de bonnes raisons de le faire: vous pouvez me contacter avant) +SCO_EXC_MAIL = "scodoc-exception@viennet.net" + +# L'adresse du mainteneur (non utilisée automatiquement par ScoDoc: ne pas changer) +SCO_DEV_MAIL = "emmanuel.viennet@gmail.com" # SVP ne pas changer + +# Adresse pour l'envoi des dumps (pour assistance technnique): +# ne pas changer (ou vous perdez le support) +SCO_DUMP_UP_URL = "https://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/upload-dump" + +CSV_FIELDSEP = ";" +CSV_LINESEP = "\n" +CSV_MIMETYPE = "text/comma-separated-values" +XLS_MIMETYPE = "application/vnd.ms-excel" +XLSX_MIMETYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" +PDF_MIMETYPE = "application/pdf" +XML_MIMETYPE = "text/xml" +JSON_MIMETYPE = "application/json" + +LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "png") # remind that PIL does not read pdf + +# Admissions des étudiants +# Différents types de voies d'admission: +# (stocké en texte libre dans la base, mais saisie par menus pour harmoniser) +TYPE_ADMISSION_DEFAULT = "Inconnue" +TYPES_ADMISSION = (TYPE_ADMISSION_DEFAULT, "APB", "APB-PC", "CEF", "Direct") + +BULLETINS_VERSIONS = ("short", "selectedevals", "long") + +# Support for ScoDoc7 compatibility +def get_dept_id(): + if g.scodoc_dept in sco_mgr.get_dept_ids(): + return g.scodoc_dept + raise sco_exceptions.ScoInvalidDept("département invalide: %s" % g.scodoc_dept) + + +def get_db_cnx_string(scodoc_dept=None): + return "dbname=SCO" + (scodoc_dept or g.scodoc_dept) + + +def ScoURL(): + """base URL for this sco instance. + e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite + = page accueil département + """ + return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[ + : -len("/index_html") + ] + + +def NotesURL(): + """URL of Notes + e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Notes + = url de base des méthodes de notes + (page accueil programmes). + """ + return url_for("notes.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")] + + +def EntreprisesURL(): + """URL of Enterprises + e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Entreprises + = url de base des requêtes de ZEntreprises + et page accueil Entreprises + """ + return "NotImplemented" + # url_for("entreprises.index_html", scodoc_dept=g.scodoc_dept)[ + # : -len("/index_html") + # ] + + +def AbsencesURL(): + """URL of Absences""" + return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[ + : -len("/index_html") + ] + + +def UsersURL(): + """URL of Users + e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite/Users + = url de base des requêtes ZScoUsers + et page accueil users + """ + return url_for("users.index_html", scodoc_dept=g.scodoc_dept)[: -len("/index_html")] + + +# ---- Simple python utilities + + +def simplesqlquote(s, maxlen=50): + """simple SQL quoting to avoid most SQL injection attacks. + Note: we use this function in the (rare) cases where we have to + construct SQL code manually""" + s = s[:maxlen] + s.replace("'", r"\'") + s.replace(";", r"\;") + for bad in ("select", "drop", ";", "--", "insert", "delete", "xp_"): + s = s.replace(bad, "") + return s + + +def unescape_html(s): + """un-escape html entities""" + s = s.strip().replace("&", "&") + s = s.replace("<", "<") + s = s.replace(">", ">") + return s + + +# test if obj is iterable (but not a string) +isiterable = lambda obj: getattr(obj, "__iter__", False) + + +def unescape_html_dict(d): + """un-escape all dict values, recursively""" + try: + indices = list(d.keys()) + except: + indices = list(range(len(d))) + for k in indices: + v = d[k] + if isinstance(v, bytes): + d[k] = unescape_html(v) + elif isiterable(v): + unescape_html_dict(v) + + +# Expressions used to check noms/prenoms +FORBIDDEN_CHARS_EXP = re.compile(r"[*\|~\(\)\\]") +ALPHANUM_EXP = re.compile(r"^[\w-]+$", re.UNICODE) + + +def is_valid_code_nip(s): + """True si s peut être un code NIP: au moins 6 chiffres décimaux""" + if not s: + return False + return re.match(r"^[0-9]{6,32}$", s) + + +def strnone(s): + "convert s to string, '' if s is false" + if s: + return str(s) + else: + return "" + + +def stripquotes(s): + "strip s from spaces and quotes" + s = s.strip() + if s and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")): + s = s[1:-1] + return s + + +def suppress_accents(s): + "remove accents and suppress non ascii characters from string s" + return ( + unicodedata.normalize("NFD", s).encode("ascii", "ignore").decode(SCO_ENCODING) + ) + + +def sanitize_string(s): + """s is an ordinary string, encoding given by SCO_ENCODING" + suppress accents and chars interpreted in XML + Irreversible (not a quote) + + For ids and some filenames + """ + # Table suppressing some chars: + trans = str.maketrans("", "", "'`\"<>!&\\ ") + return suppress_accents(s.translate(trans)).replace(" ", "_").replace("\t", "_") + + +_BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\") + + +def make_filename(name): + """Try to convert name to a reasonable filename + without spaces, (back)slashes, : and without accents + """ + return suppress_accents(name.translate(_BAD_FILENAME_CHARS)).replace(" ", "_") + + +VALID_CARS = ( + "-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.!" # no / ! +) +VALID_CARS_SET = set(VALID_CARS) +VALID_EXP = re.compile("^[" + VALID_CARS + "]+$") + + +def sanitize_filename(filename): + """Keep only valid chars + used for archives filenames + """ + sane = "".join([c for c in filename if c in VALID_CARS_SET]) + if len(sane) < 2: + sane = time.strftime("%Y-%m-%d-%H%M%S") + "-" + sane + return sane + + +def is_valid_filename(filename): + """True if filename is safe""" + return VALID_EXP.match(filename) + + +def sendCSVFile(REQUEST, data, filename): + """publication fichier. + (on ne doit rien avoir émis avant, car ici sont générés les entetes) + """ + filename = ( + unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_") + ) + REQUEST.RESPONSE.setHeader("content-type", CSV_MIMETYPE) + REQUEST.RESPONSE.setHeader( + "content-disposition", 'attachment; filename="%s"' % filename + ) + return data + + +def sendPDFFile(REQUEST, data, filename): + filename = ( + unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_") + ) + if REQUEST: + REQUEST.RESPONSE.setHeader("content-type", PDF_MIMETYPE) + REQUEST.RESPONSE.setHeader( + "content-disposition", 'attachment; filename="%s"' % filename + ) + return data + + +class ScoDocJSONEncoder(json.JSONEncoder): + def default(self, o): # pylint: disable=E0202 + if isinstance(o, (datetime.date, datetime.datetime)): + return o.isoformat() + elif isinstance(o, ApoEtapeVDI): + return str(o) + else: + return json.JSONEncoder.default(self, o) + + +def sendJSON(REQUEST, data): + js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) + if REQUEST: + REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE) + return js + + +def sendXML(REQUEST, data, tagname=None, force_outer_xml_tag=True): + if type(data) != list: + data = [data] # always list-of-dicts + if force_outer_xml_tag: + root_tagname = tagname + "_list" + data = [{root_tagname: data}] + doc = sco_xml.simple_dictlist2xml(data, tagname=tagname) + + if REQUEST: + REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE) + return doc + + +def sendResult(REQUEST, data, name=None, format=None, force_outer_xml_tag=True): + if (format is None) or (format == "html"): + return data + elif format == "xml": # name is outer tagname + return sendXML( + REQUEST, data, tagname=name, force_outer_xml_tag=force_outer_xml_tag + ) + elif format == "json": + return sendJSON(REQUEST, data) + else: + raise ValueError("invalid format: %s" % format) + + +def get_scodoc_version(): + "return a string identifying ScoDoc version" + return os.popen("cd %s; ./get_scodoc_version.sh -s" % SCO_TOOLS_DIR).read().strip() + + +def check_scodoc7_password(scodoc7_hash, password): + """Check a password vs scodoc7 hash + used only during old databases migrations""" + m = md5() + m.update(password.encode("utf-8")) + # encodestring à remplacer par encodebytes #py3 + h = base64.encodestring(m.digest()).decode("utf-8").strip() + return h == scodoc7_hash + + +# Simple string manipulations +# not necessary anymore in Python 3 ! TODO remove +def strupper(s): + return s.upper() + # return s.decode(SCO_ENCODING).upper().encode(SCO_ENCODING) + + +# XXX fonctions inutiles en Python3 ! +def strlower(s): + return s.lower() + + +def strcapitalize(s): + return s.capitalize() + + +def abbrev_prenom(prenom): + "Donne l'abreviation d'un prenom" + # un peu lent, mais espère traiter tous les cas + # Jean -> J. + # Charles -> Ch. + # Jean-Christophe -> J.-C. + # Marie Odile -> M. O. + prenom = prenom.replace(".", " ").strip() + if not prenom: + return "" + d = prenom[:3].upper() + if d == "CHA": + abrv = "Ch." # 'Charles' donne 'Ch.' + i = 3 + else: + abrv = prenom[0].upper() + "." + i = 1 + n = len(prenom) + while i < n: + c = prenom[i] + if c == " " or c == "-" and i < n - 1: + sep = c + i += 1 + # gobbe tous les separateurs + while i < n and (prenom[i] == " " or prenom[i] == "-"): + if prenom[i] == "-": + sep = "-" + i += 1 + if i < n: + abrv += sep + prenom[i].upper() + "." + i += 1 + return abrv + + +# +def timedate_human_repr(): + "representation du temps courant pour utilisateur: a localiser" + return time.strftime("%d/%m/%Y à %Hh%M") + + +def annee_scolaire_repr(year, month): + """representation de l'annee scolaire : '2009 - 2010' + à partir d'une date. + """ + if month > 7: # apres le 1er aout + return "%s - %s" % (year, year + 1) + else: + return "%s - %s" % (year - 1, year) + + +def annee_scolaire_debut(year, month): + """Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord...""" + if int(month) > 7: + return int(year) + else: + return int(year) - 1 + + +def sem_decale_str(sem): + """'D' si semestre decalé, ou ''""" + # considère "décalé" les semestre impairs commençant entre janvier et juin + # et les pairs entre juillet et decembre + if sem["semestre_id"] <= 0: + return "" + if (sem["semestre_id"] % 2 and sem["mois_debut_ord"] <= 6) or ( + not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6 + ): + return "D" + else: + return "" + + +def is_valid_mail(email): + """True if well-formed email address""" + return re.match(r"^.+@.+\..{2,3}$", email) + + +def graph_from_edges(edges, graph_name="mygraph"): + """Crée un graph pydot + à partir d'une liste d'arêtes [ (n1, n2), (n2, n3), ... ] + où n1, n2, ... sont des chaînes donnant l'id des nœuds. + + Fonction remplaçant celle de pydot qui est buggée. + """ + nodes = set([it for tup in edges for it in tup]) + graph = pydot.Dot(graph_name) + for n in nodes: + graph.add_node(pydot.Node(n)) + for e in edges: + graph.add_edge(pydot.Edge(src=e[0], dst=e[1])) + return graph + + +ICONSIZES = {} # name : (width, height) cache image sizes + + +def icontag(name, file_format="png", **attrs): + """tag HTML pour un icone. + (dans les versions anterieures on utilisait Zope) + Les icones sont des fichiers PNG dans .../static/icons + Si la taille (width et height) n'est pas spécifiée, lit l'image + pour la mesurer (et cache le résultat). + """ + if ("width" not in attrs) or ("height" not in attrs): + if name not in ICONSIZES: + img_file = os.path.join( + Config.SCODOC_DIR, + "app/static/icons/%s.%s" + % ( + name, + file_format, + ), + ) + im = PILImage.open(img_file) + width, height = im.size[0], im.size[1] + ICONSIZES[name] = (width, height) # cache + else: + width, height = ICONSIZES[name] + attrs["width"] = width + attrs["height"] = height + if "border" not in attrs: + attrs["border"] = 0 + if "alt" not in attrs: + attrs["alt"] = "logo %s" % name + s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs]) + return '' % ( + name, + s, + name, + file_format, + ) + + +ICON_PDF = icontag("pdficon16x20_img", title="Version PDF") +ICON_XLS = icontag("xlsicon_img", title="Version tableur") + + +def sort_dates(L, reverse=False): + """Return sorted list of dates, allowing None items (they are put at the beginning)""" + mindate = datetime.datetime(datetime.MINYEAR, 1, 1) + try: + return sorted(L, key=lambda x: x or mindate, reverse=reverse) + except: + # Helps debugging + log("sort_dates( %s )" % L) + raise + + +def query_portal(req, msg="Portail Apogee", timeout=3): + """Retreives external data using HTTP request + (used to connect to Apogee portal, or ScoDoc server) + returns a string, "" on error + """ + log("query_portal: %s" % req) + + try: + f = six.moves.urllib.request.urlopen(req, timeout=timeout) # seconds / request + except: + log("query_portal: can't connect to %s" % msg) + return "" + try: + data = f.read() + except: + log("query_portal: error reading from %s" % msg) + data = "" + + return data + + +def AnneeScolaire(REQUEST=None): # TODO remplacer REQUEST #sco8 + "annee de debut de l'annee scolaire courante" + if REQUEST and "sco_year" in REQUEST.form: + year = REQUEST.form["sco_year"] + try: + year = int(year) + if year > 1900 and year < 2999: + return year + except: + pass + t = time.localtime() + year, month = t[0], t[1] + if month < 8: # le "pivot" est le 1er aout + year = year - 1 + return year + + +def log_unknown_etud(REQUEST=None, format="html"): + """Log request: cas ou getEtudInfo n'a pas ramene de resultat""" + etudid = REQUEST.form.get("etudid", "?") + code_nip = REQUEST.form.get("code_nip", "?") + code_ine = REQUEST.form.get("code_ine", "?") + log( + "unknown student: etudid=%s code_nip=%s code_ine=%s" + % (etudid, code_nip, code_ine) + ) + return _sco_error_response("unknown student", format=format, REQUEST=REQUEST) + + +# XXX #sco8 à tester ou ré-écrire +def _sco_error_response(msg, format="html", REQUEST=None): + """Send an error message to the client, in html or xml format.""" + REQUEST.RESPONSE.setStatus(404, reason=msg) + if format == "html" or format == "pdf": + raise sco_exceptions.ScoValueError(msg) + elif format == "xml": + REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE) + doc = ElementTree.Element("error", msg=msg) + return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(SCO_ENCODING) + elif format == "json": + REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE) + return "undefined" # XXX voir quoi faire en cas d'erreur json + else: + raise ValueError("ScoErrorResponse: invalid format") + + +def return_text_if_published(val, REQUEST): + """Pour les méthodes publiées qui ramènent soit du texte (HTML) soit du JSON + sauf quand elles sont appellées depuis python. + La présence de l'argument REQUEST indique la publication. + """ + if REQUEST and not isinstance(val, STRING_TYPES): + return sendJSON(REQUEST, val) + return val + + +def confirm_dialog( + context, + message="

Confirmer ?

", + OK="OK", + Cancel="Annuler", + dest_url="", + cancel_url="", + target_variable="dialog_confirmed", + parameters={}, + add_headers=True, # complete page + REQUEST=None, # required + helpmsg=None, +): + from app.scodoc import html_sco_header + + # dialog de confirmation simple + parameters[target_variable] = 1 + # Attention: la page a pu etre servie en GET avec des parametres + # si on laisse l'url "action" vide, les parametres restent alors que l'on passe en POST... + if not dest_url: + dest_url = REQUEST.URL + # strip remaining parameters from destination url: + dest_url = six.moves.urllib.parse.splitquery(dest_url)[0] + H = [ + """
""" % dest_url, + message, + """""" % OK, + ] + if cancel_url: + H.append( + """""" + % (Cancel, cancel_url) + ) + for param in parameters.keys(): + if parameters[param] is None: + parameters[param] = "" + if type(parameters[param]) == type([]): + for e in parameters[param]: + H.append('' % (param, e)) + else: + H.append( + '' + % (param, parameters[param]) + ) + H.append("
") + if helpmsg: + H.append('

' + helpmsg + "

") + if add_headers and REQUEST: + return ( + html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() + ) + else: + return "\n".join(H) diff --git a/requirements-3.7.txt b/requirements-3.7.txt index 275cd7af..bd98cf72 100755 --- a/requirements-3.7.txt +++ b/requirements-3.7.txt @@ -1,63 +1,64 @@ -alembic==1.6.5 -attrs==21.2.0 -Babel==2.9.1 -blinker==1.4 -certifi==2021.5.30 -cffi==1.14.6 -chardet==4.0.0 -charset-normalizer==2.0.3 -click==8.0.1 -cracklib==2.9.3 -cryptography==3.4.7 -dnspython==2.1.0 -dominate==2.6.0 -email-validator==1.1.3 -Flask==2.0.1 -Flask-Babel==2.0.0 -Flask-Bootstrap==3.3.7.1 -Flask-Caching==1.10.1 -Flask-Login==0.5.0 -Flask-Mail==0.9.1 -Flask-Migrate==3.0.1 -Flask-Moment==1.0.2 -Flask-SQLAlchemy==2.5.1 -Flask-WTF==0.15.1 -greenlet==1.1.0 -icalendar==4.0.7 -idna==3.2 -importlib-metadata==4.6.1 -iniconfig==1.1.1 -itsdangerous==2.0.1 -Jinja2==3.0.1 -jwt==1.2.0 -Mako==1.1.4 -MarkupSafe==2.0.1 -packaging==21.0 -Pillow==8.3.1 -pkg-resources==0.0.0 -pluggy==0.13.1 -psycopg2==2.9.1 -py==1.10.0 -pycparser==2.20 -pydot==1.4.2 -pyOpenSSL==20.0.1 -pyparsing==2.4.7 -PyRSS2Gen==1.1 -pytest==6.2.4 -python-dateutil==2.8.2 -python-dotenv==0.18.0 -python-editor==1.0.4 -pytz==2021.1 -redis==3.5.3 -reportlab==3.5.68 -requests==2.26.0 -rq==1.9.0 -six==1.16.0 -SQLAlchemy==1.4.22 -toml==0.10.2 -typing-extensions==3.10.0.0 -urllib3==1.26.6 -visitor==0.1.3 -Werkzeug==2.0.1 -WTForms==2.3.3 -zipp==3.5.0 +openpyxl +alembic==1.6.5 +attrs==21.2.0 +Babel==2.9.1 +blinker==1.4 +certifi==2021.5.30 +cffi==1.14.6 +chardet==4.0.0 +charset-normalizer==2.0.3 +click==8.0.1 +cracklib==2.9.3 +cryptography==3.4.7 +dnspython==2.1.0 +dominate==2.6.0 +email-validator==1.1.3 +Flask==2.0.1 +Flask-Babel==2.0.0 +Flask-Bootstrap==3.3.7.1 +Flask-Caching==1.10.1 +Flask-Login==0.5.0 +Flask-Mail==0.9.1 +Flask-Migrate==3.0.1 +Flask-Moment==1.0.2 +Flask-SQLAlchemy==2.5.1 +Flask-WTF==0.15.1 +greenlet==1.1.0 +icalendar==4.0.7 +idna==3.2 +importlib-metadata==4.6.1 +iniconfig==1.1.1 +itsdangerous==2.0.1 +Jinja2==3.0.1 +jwt==1.2.0 +Mako==1.1.4 +MarkupSafe==2.0.1 +packaging==21.0 +Pillow==8.3.1 +pkg-resources==0.0.0 +pluggy==0.13.1 +psycopg2==2.9.1 +py==1.10.0 +pycparser==2.20 +pydot==1.4.2 +pyOpenSSL==20.0.1 +pyparsing==2.4.7 +PyRSS2Gen==1.1 +pytest==6.2.4 +python-dateutil==2.8.2 +python-dotenv==0.18.0 +python-editor==1.0.4 +pytz==2021.1 +redis==3.5.3 +reportlab==3.5.68 +requests==2.26.0 +rq==1.9.0 +six==1.16.0 +SQLAlchemy==1.4.22 +toml==0.10.2 +typing-extensions==3.10.0.0 +urllib3==1.26.6 +visitor==0.1.3 +Werkzeug==2.0.1 +WTForms==2.3.3 +zipp==3.5.0