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("""
hello
', - "hello
', + "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 = [ - """") - 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 = [ + """") + 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