From 48ad4f38777ce01192f554279361a999d3fa2917 Mon Sep 17 00:00:00 2001 From: Jean-Marie Place Date: Wed, 11 Aug 2021 11:40:28 +0200 Subject: [PATCH] export/import notes ; gentables --- app/scodoc/gen_tables.py | 36 +- app/scodoc/sco_excel.py | 1426 +++++++++++++++++--------------- app/scodoc/sco_saisie_notes.py | 14 +- app/views/notes.py | 1 + 4 files changed, 791 insertions(+), 686 deletions(-) diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index 3791c81429..19d7f6dea2 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -279,7 +279,7 @@ class GenTable(object): """ if format == "html": return self.html() - elif format == "xls": + elif format == "xls" or format == "xlsx": return self.excel() elif format == "text" or format == "csv": return self.text() @@ -465,22 +465,24 @@ class GenTable(object): return "\n".join(H) def excel(self, wb=None): - "Simple Excel representation of the table" - L = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name) - style_bold = sco_excel.Excel_MakeStyle(bold=True) - - L.cells += self.xls_before_table - L.set_style(style_bold, li=len(L.cells)) - L.append(self.get_titles_list()) - L.cells += [[x for x in line] for line in self.get_data_list()] + """Simple Excel representation of the table""" + ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb) + ses.rows += self.xls_before_table + style_bold = sco_excel.excel_make_style(bold=True) + style_base = sco_excel.excel_make_style() + ses.append_row(ses.make_row(self.get_titles_list(), style_bold)) + for line in self.get_data_list(): + ses.append_row(ses.make_row(line, style_base)) if self.caption: - L.append([]) # empty line - L.append([self.caption]) + ses.append_blank_row() # empty line + ses.append_single_cell_row(self.caption, style_base) if self.origin: - L.append([]) # empty line - L.append([self.origin]) - - return L.gen_workbook(wb=wb) + ses.append_blank_row() # empty line + ses.append_single_cell_row(self.origin, style_base) + if wb is None: + return ses.generate_standalone() + else: + ses.generate_embeded() def text(self): "raw text representation of the table" @@ -648,10 +650,10 @@ class GenTable(object): return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") else: return doc - elif format == "xls": + elif format == "xls" or format == "xlsx": xls = self.excel() if publish: - return sco_excel.sendExcelFile(REQUEST, xls, filename + ".xls") + return sco_excel.send_excel_file(REQUEST, xls, filename + ".xls") else: return xls elif format == "text": diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 7ac59ba8fc..b7111a3285 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -1,662 +1,764 @@ -# -*- 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 - - -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, "Feuille saisie note (à enregistrer au format excel)", style_titres - ) - li += 1 - ws0.write(li, 0, "Saisir les notes dans la colonne E (cases jaunes)", style_expl) - li += 1 - ws0.write(li, 0, "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, "Evaluation du %s (coef. %g)" % (E["jour"], E["coefficient"]), style - ) - li += 1 - li += 1 # ligne blanche - # code et titres colonnes - ws0.write(li, 0, "!%s" % E["evaluation_id"], style_ro) - ws0.write(li, 1, "Nom", style_titres) - ws0.write(li, 2, "Prénom", style_titres) - ws0.write(li, 3, "Groupe", style_titres) - ws0.write(li, 4, "Note sur %g" % E["note_max"], style_titres) - ws0.write(li, 5, "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, "Code notes", style_titres) - ws0.write(li + 1, 1, "ABS", style_expl) - ws0.write(li + 1, 2, "absent (0)", style_expl) - ws0.write(li + 2, 1, "EXC", style_expl) - ws0.write(li + 2, 2, "pas prise en compte", style_expl) - ws0.write(li + 3, 1, "ATT", style_expl) - ws0.write(li + 3, 2, "en attente", style_expl) - ws0.write(li + 4, 1, "SUPR", style_expl) - ws0.write(li + 4, 2, "pour supprimer note déjà entrée", style_expl) - ws0.write(li + 5, 1, "", style_expl) - ws0.write(li + 5, 2, "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 = "", 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, "Discipline :", style2) - ws.append([None, cell_2]) - - # ligne 3 - cell_2 = _make_cell(ws, "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, "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, "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, "etudid", style3)) - cells.append(_make_cell(ws, "code_nip", style3)) - cells.append(_make_cell(ws, "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"], ""), style2t3) - ) - if with_codes: - cells.append(_make_cell(ws, t["etudid"], style2t3)) - code_nip = t.get("code_nip", "") - cells.append(_make_cell(ws, code_nip, style2t3)) - code_ine = t.get("code_ine", "") - 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() +# -*- 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 datetime +import io +import time +from enum import Enum +from tempfile import NamedTemporaryFile + +from openpyxl import Workbook, load_workbook +from openpyxl.cell import WriteOnlyCell +from openpyxl.styles import Font, Border, Side, Alignment, PatternFill + +import app.scodoc.sco_utils as scu +from app.scodoc import notesdb +from app.scodoc import sco_preferences +from app.scodoc.notes_log import log +from app.scodoc.sco_exceptions import ScoValueError + + +class COLORS(Enum): + BLACK = "FF000000" + WHITE = "FFFFFFFF" + RED = "FFFF0000" + BROWN = "FF993300" + PURPLE = "FF993366" + BLUE = "FF0000FF" + ORANGE = "FFFF3300" + LIGHT_YELLOW = "FFFFFF99" + + +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 + + +# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante: +# font, border, number_format, fill, .. (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) + + +# (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) + + +class ScoExcelBook: + """Permet la génération d'un classeur xlsx composé de plusieurs feuilles. + usage: + wb = ScoExcelBook() + ws0 = wb.create_sheet('sheet name 0') + ws1 = wb.create_sheet('sheet name 1') + ... + steam = wb.generate() + """ + def __init__(self): + self.sheets = [] # list of sheets + + def create_sheet(self, sheet_name="feuille", default_style=None): + sheet = ScoExcelSheet(sheet_name, default_style) + self.sheets.append(sheet) + + def generate(self): + """ génération d'un stream binaire représentant la totalité du classeur. + retourne le flux + """ + wb = Workbook(write_only=True) + for sheet in self.sheets: + sheet.generate(self) + # 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() + + +def excel_make_style( + bold=False, + italic=False, + color: COLORS = COLORS.BLACK, + bgcolor: COLORS = None, + halign=None, + valign=None, + format_number=None, +): + """Contruit un style. + Les couleurs peuvent être spécfiées soit par une valeur de COLORS, + soit par une chaine argb (exple "FF00FF00" pour le vert) + color -- La couleur du texte + bgcolor -- la couleur de fond + halign -- alignement horizontal ("left", "right", "center") + valign -- alignement vertical ("top", "bottom", "center") + format_number -- formattage du contenu ("general", "@", ...) + """ + style = {} + font = Font(name="Arial", bold=bold, italic=italic, color=color.value) + style["font"] = font + if bgcolor: + style["fill"] = PatternFill(fill_type="solid", bgColor=bgcolor.value) + if halign or valign: + al = Alignment() + if halign: + al.horz = { + "left": "left", + "right": "right", + "center": "center", + }[halign] + if valign: + al.vert = { + "top": "top", + "bottom": "bottom", + "center": "center", + }[valign] + style["alignment"] = al + if format_number is None: + style["format_number"] = "general" + else: + style["format_number"] = format_number + return style + + +class ScoExcelSheet: + """Représente une feuille qui peut être indépendante ou intégrée dans un SCoExcelBook. + En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations + est imposé: + * instructions globales (largeur/maquage des colonnes et ligne, ...) + * construction et ajout des cellules et ligne selon le sens de lecture (occidental) + ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..) + * pour finit appel de la méthode de génération + """ + def __init__(self, sheet_name="feuille", default_style=None, wb=None): + """Création de la feuille. + sheet_name -- le nom de la feuille + default_style -- le style par défaut des cellules + wb -- le WorkBook dans laquelle se trouve la feuille. Si wb est None (cas d'un classeur mono-feuille), + un workbook est crée et associé à cette feuille. + """ + self.sheet_name = sheet_name + self.rows = [] # list of list of cells + # self.cells_styles_lico = {} # { (li,co) : style } + # self.cells_styles_li = {} # { li : style } + # self.cells_styles_co = {} # { co : style } + if default_style is None: + default_style = excel_make_style() + self.default_style = default_style + self.wb = wb or Workbook(write_only=True) # Création de workbook si nécessaire + self.ws = self.wb.create_sheet(title=self.sheet_name) + self.column_dimensions = {} + + def set_column_dimension_width(self, cle, value): + """Détermine la largeur d'une colonne. + cle -- identifie la colonne ("A"n "B", ...) + value -- la dimension (unité : 7 pixels comme affiché dans Excel) + """ + self.ws.column_dimensions[cle].width = value + + def set_column_dimension_hidden(self, cle, value): + """Masque ou affiche une colonne. + cle -- identifie la colonne ("A"n "B", ...) + value -- boolean (vrai = colonne cachée) + """ + self.ws.column_dimensions[cle].hidden = value + + def make_cell(self, value: any = None, style=None): + """Construit une cellule. + value -- contenu de la cellule (texte ou numérique) + style -- style par défaut de la feuille si non spécifié + """ + cell = WriteOnlyCell(self.ws, value or "") + if style is None: + style = self.default_style + if "font" in style: + cell.font = style["font"] + if "border" in style: + cell.border = style["border"] + if "number_format" in style: + cell.number_format = style["number_format"] + if "fill" in style: + cell.fill = style["fill"] + if "alignment" in style: + cell.alignment = style["alignment"] + return cell + + def make_row(self, values: list, style): + return [self.make_cell(value, style) for value in values] + + def append_single_cell_row(self, value: any, style=None): + """construit une ligne composée d'une seule cellule et l'ajoute à la feuille. + mêmes paramètres que make_cell: + value -- contenu de la cellule (texte ou numérique) + style -- style par défaut de la feuille si non spécifié + """ + self.append_row([self.make_cell(value, style)]) + + def append_blank_row(self): + """construit une ligne vide et l'ajoute à la feuille.""" + self.append_row([None]) + + def append_row(self, row): + """ajoute une ligne déjà construite à la feuille.""" + self.rows.append(row) + + # def set_style(self, style=None, li=None, co=None): + # if li is not None and co is not None: + # self.cells_styles_lico[(li, co)] = style + # elif li is None: + # self.cells_styles_li[li] = style + # elif co is None: + # self.cells_styles_co[co] = style + # + # 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 _generate_ws(self): + """génére un flux décrivant la feuille. + Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille) + ou pour la génération d'un classeur multi-feuilles + """ + for col in self.column_dimensions.keys(): + self.ws.column_dimensions[col] = self.column_dimensions[col] + for row in self.rows: + self.ws.append(row) + + def generate_standalone(self): + """génération d'un classeur mono-feuille""" + self._generate_ws() + # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) + with NamedTemporaryFile() as tmp: + self.wb.save(tmp.name) + tmp.seek(0) + return tmp.read() + + def generate_embeded(self): + """generation d'une feuille include dans un classeur multi-feuilles""" + self._generate_ws() + + def gen_workbook(self, wb=None): + """TODO: à remplacer""" + """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 is None: + wb = Workbook() # Création du fichier + sauvegarde = True + else: + sauvegarde = False + ws0 = wb.add_sheet(self.sheet_name) + li = 0 + for row in self.rows: + co = 0 + for c in row: + # 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: + return wb.savetostr() + else: + return None + + +def excel_simple_table( + titles=None, lines=None, sheet_name=b"feuille", titles_styles=None +): + """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel""" + ws = ScoExcelSheet(sheet_name) + if titles is None: + titles = [] + if lines is None: + lines = [[]] + if titles_styles is None: + style = excel_make_style(bold=True) + titles_styles = [style] * len(titles) + # ligne de titres + ws.append_row( + [ws.make_cell(it, style) for (it, style) in zip(titles, titles_styles)] + ) + default_style = excel_make_style() + text_style = excel_make_style(format_number="@") + for line in lines: + cells = [] + for it in line: + # safety net: allow only str, int and float + # TODO Plus de type Long en Python 3 ? + # if isinstance(it, long): # XXX + # it = int(it) # assume all ScoDoc longs fits in int ! + cell_style = default_style + if type(it) not in (int, float): # XXX A REVOIR + cell_style = text_style + cells.append(ws.make_cell(it, cell_style)) + ws.append_row(cells) + return ws.generate_standalone() + + +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) + """ + sheet_name = "Saisie notes" + ws = ScoExcelSheet(sheet_name) + + # ajuste largeurs colonnes (unite inconnue, empirique) + ws.set_column_dimension_width("A", 11.0 / 7) # codes + # ws.set_column_dimension_hidden("A", True) # codes + ws.set_column_dimension_width("B", 164.00 / 7) # noms + ws.set_column_dimension_width("C", 109.0 / 7) # prenoms + ws.set_column_dimension_width("D", 164.0 / 7) # groupes + ws.set_column_dimension_width("E", 115.0 / 7) # notes + ws.set_column_dimension_width("F", 355.0 / 7) # remarques + + # fontes + font_base = Font(name="Arial", size=12) + font_bold = Font(name="Arial", bold=True) + font_italic = Font(name="Arial", size=12, italic=True, color=COLORS.RED.value) + font_titre = Font(name="Arial", bold=True, size=14) + font_purple = Font(name="Arial", color=COLORS.PURPLE.value) + font_brown = Font(name="Arial", color=COLORS.BROWN.value) + font_blue = Font(name="Arial", size=9, color=COLORS.BLUE.value) + + # bordures + side_thin = Side(border_style="thin", color=COLORS.BLACK.value) + border_top = Border(top=side_thin) + border_right = Border(right=side_thin) + + # fonds + fill_light_yellow = PatternFill( + patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value + ) + + # styles + style = {"font": font_base} + style_titres = {"font": font_titre} + style_expl = {"font": font_italic} + + style_ro = { # cells read-only + "font": font_purple, + "border": border_right, + } + style_dem = { + "font": font_brown, + "border": border_top, + } + style_nom = { # style pour nom, prenom, groupe + "font": font_base, + "border": border_top, + } + style_notes = { + "font": font_bold, + "number_format": "general", + "fill": fill_light_yellow, + "border": border_top, + } + style_comment = { + "font": font_blue, + "border": border_top, + } + + # ligne de titres + ws.append_single_cell_row( + "Feuille saisie note (à enregistrer au format excel)", style_titres + ) + # lignes d'instructions + ws.append_single_cell_row( + "Saisir les notes dans la colonne E (cases jaunes)", style_expl + ) + ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl) + # Nom du semestre + ws.append_single_cell_row(scu.unescape_html(titreannee), style_titres) + # description evaluation + ws.append_single_cell_row(scu.unescape_html(description), style_titres) + ws.append_single_cell_row( + "Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style + ) + # ligne blanche + ws.append_blank_row() + # code et titres colonnes + ws.append_row( + [ + ws.make_cell("!%s" % e["evaluation_id"], style_ro), + ws.make_cell("Nom", style_titres), + ws.make_cell("Prénom", style_titres), + ws.make_cell("Groupe", style_titres), + ws.make_cell("Note sur %g" % e["note_max"], style_titres), + ws.make_cell("Remarque", style_titres), + ] + ) + + # etudiants + for line in lines: + st = style_nom + 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/... + try: + val = float(line[5]) + except ValueError: + val = line[5] + ws.append_row( + [ + ws.make_cell("!" + line[0], style_ro), # code + ws.make_cell(line[1], st), + ws.make_cell(line[2], st), + ws.make_cell(s, st), + ws.make_cell(val, style_notes), # note + ws.make_cell(line[6], style_comment), # comment + ] + ) + + # explication en bas + ws.append_row([None, ws.make_cell("Code notes", style_titres)]) + ws.append_row( + [ + None, + ws.make_cell("ABS", style_expl), + ws.make_cell("absent (0)", style_expl), + ] + ) + ws.append_row( + [ + None, + ws.make_cell("EXC", style_expl), + ws.make_cell("pas prise en compte", style_expl), + ] + ) + ws.append_row( + [ + None, + ws.make_cell("ATT", style_expl), + ws.make_cell("en attente", style_expl), + ] + ) + ws.append_row( + [ + None, + ws.make_cell("SUPR", style_expl), + ws.make_cell("pour supprimer note déjà entrée", style_expl), + ] + ) + ws.append_row( + [ + None, + ws.make_cell("", style_expl), + ws.make_cell("cellule vide -> note non modifiée", style_expl), + ] + ) + return ws.generate_standalone() + + +def excel_bytes_to_list(bytes_content): + filelike = io.BytesIO(bytes_content) + return _excel_to_list(filelike) + + +def excel_file_to_list(filename): + return _excel_to_list(filename) + + +def _excel_to_list(filelike): # 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: + wb = load_workbook(filename=filelike, read_only=True, data_only=True) + except: + log("Excel_to_list: failure to import document") + open("/tmp/last_scodoc_import_failure.xls", "w").write(filelike) + 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(wb.get_sheet_names()) < 1: + diag.append("Aucune feuille trouvée dans le classeur !") + return diag, None + if len(wb.get_sheet_names()) > 1: + diag.append("Attention: n'utilise que la première feuille du classeur !") + # fill matrix + sheet_name = wb.get_sheet_names()[0] + ws = wb.get_sheet_by_name(sheet_name) + sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") + values = {} + for row in ws.iter_rows(): + for cell in row: + if cell.value is not None: + values[(cell.row - 1, cell.column - 1)] = str(cell.value) + if not values: + diag.append( + "Aucune valeur trouvée dans la feuille %s !" + % sheet_name.decode(scu.SCO_ENCODING) + ) + 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.decode(scu.SCO_ENCODING), len(m)) + ) + # diag.append(str(M)) + # + return diag, m + + +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 + + ws = ScoExcelSheet(sheet_name) + ws.set_column_dimension_width("A", 3) + ws.set_column_dimension_width("B", 35) + ws.set_column_dimension_width("C", 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=COLORS.BLACK.value) + + 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"], + ) + + ws.append_row([None, ws.make_cell(title, style2)]) + + # ligne 2 + ws.append_row([None, ws.make_cell("Discipline :", style2)]) + + # ligne 3 + cell_2 = ws.make_cell("Enseignant :", style2) + cell_6 = ws.make_cell(("Groupe %s" % groupname), style3) + ws.append_row([None, cell_2, None, None, None, None, cell_6]) + + # ligne 4: Avertissement pour ne pas confondre avec listes notes + cell_2 = ws.make_cell( + "Ne pas utiliser cette feuille pour saisir les notes !", style1i + ) + ws.append_row([None, None, cell_2]) + + ws.append_blank_row() + ws.append_blank_row() + + # ligne 7: Entête (contruction dans une liste cells) + cell_2 = ws.make_cell("Nom", style3) + cells = [None, cell_2] + for partition in partitions: + cells.append(ws.make_cell(partition["partition_name"], style3)) + if with_codes: + cells.append(ws.make_cell("etudid", style3)) + cells.append(ws.make_cell("code_nip", style3)) + cells.append(ws.make_cell("code_ine", style3)) + for i in range(nb_weeks): + cells.append(ws.make_cell("", style2b)) + ws.append_row(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 = ws.make_cell(n, style1b) + cell_2 = ws.make_cell(nomprenom, style_nom) + cells = [cell_1, cell_2] + + for partition in partitions: + if partition["partition_name"]: + cells.append( + ws.make_cell(t.get(partition["partition_id"], ""), style2t3) + ) + if with_codes: + cells.append(ws.make_cell(t["etudid"], style2t3)) + code_nip = t.get("code_nip", "") + cells.append(ws.make_cell(code_nip, style2t3)) + code_ine = t.get("code_ine", "") + cells.append(ws.make_cell(code_ine, style2t3)) + cells.append(ws.make_cell(t.get("etath", ""), style2b)) + for i in range(1, nb_weeks): + cells.append(ws.make_cell(style=style2t3)) + ws.append_row(cells) + + ws.append_blank_row() + + # bas de page (date, serveur) + dt = time.strftime("%d/%m/%Y à %Hh%M") + if server_name: + dt += " sur " + server_name + cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i) + ws.append_row([None, cell_2]) + + return ws.generate_standalone() diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index b6cb13208a..0c6b8a5f6b 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -29,6 +29,7 @@ Formulaire revu en juillet 2016 """ +import sys import time import datetime import psycopg2 @@ -178,8 +179,7 @@ def do_evaluation_upload_xls(context, REQUEST): # XXX imaginer un redirect + msg erreur raise AccessDenied("Modification des notes impossible pour %s" % authuser) # - data = REQUEST.form["notefile"].read() - diag, lines = sco_excel.Excel_to_list(data) + diag, lines = sco_excel.excel_file_to_list(REQUEST.form["notefile"]) try: if not lines: raise InvalidNoteValue() @@ -224,7 +224,7 @@ def do_evaluation_upload_xls(context, REQUEST): ni += 1 except: diag.append( - 'Erreur: feuille invalide ! (erreur ligne %d)
"%s"' + 'Erreur: Ligne invalide ! (erreur ligne %d)
"%s"' % (ni, str(lines[ni])) ) raise InvalidNoteValue() @@ -629,7 +629,7 @@ def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None): H.append( """
- Etape 2 : chargement d'un fichier de notes""" #' + Etape 2 : chargement d'un fichier de notes""" # ' ) nf = TrivialFormulator( @@ -702,7 +702,7 @@ def saisie_notes_tableur(context, evaluation_id, group_ids=[], REQUEST=None):
  • Effacer toutes les notes de cette évaluation (ceci permet ensuite de supprimer l'évaluation si besoin)
  • """ % (evaluation_id, evaluation_id) - ) #' + ) # ' H.append( """
  • Revenir au module
  • Revenir au formulaire de saisie
  • @@ -805,8 +805,8 @@ def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None): ) filename = "notes_%s_%s.xls" % (evalname, gr_title_filename) - xls = sco_excel.Excel_feuille_saisie(E, sem["titreannee"], description, lines=L) - return sco_excel.sendExcelFile(REQUEST, xls, filename) + xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) + return sco_excel.send_excel_file(REQUEST, xls, filename) def has_existing_decision(context, M, E, etudid): diff --git a/app/views/notes.py b/app/views/notes.py index 8f42c6ba05..4ed6d82f5a 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1551,6 +1551,7 @@ sco_publish( "/saisie_notes_tableur", sco_saisie_notes.saisie_notes_tableur, Permission.ScoEnsView, + methods=["GET", "POST"], ) sco_publish( "/feuille_saisie_notes",