Merge pull request 'export/import notes ; gentables' (#94) from jmplace/ScoDoc-Lille:repair into ScoDoc8

Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/94
This commit is contained in:
Emmanuel Viennet 2021-08-11 11:54:38 +02:00
commit 09d131a85d
4 changed files with 791 additions and 686 deletions

View File

@ -279,7 +279,7 @@ class GenTable(object):
""" """
if format == "html": if format == "html":
return self.html() return self.html()
elif format == "xls": elif format == "xls" or format == "xlsx":
return self.excel() return self.excel()
elif format == "text" or format == "csv": elif format == "text" or format == "csv":
return self.text() return self.text()
@ -465,22 +465,24 @@ class GenTable(object):
return "\n".join(H) return "\n".join(H)
def excel(self, wb=None): def excel(self, wb=None):
"Simple Excel representation of the table" """Simple Excel representation of the table"""
L = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name) ses = sco_excel.ScoExcelSheet(sheet_name=self.xls_sheet_name, wb=wb)
style_bold = sco_excel.Excel_MakeStyle(bold=True) ses.rows += self.xls_before_table
style_bold = sco_excel.excel_make_style(bold=True)
L.cells += self.xls_before_table style_base = sco_excel.excel_make_style()
L.set_style(style_bold, li=len(L.cells)) ses.append_row(ses.make_row(self.get_titles_list(), style_bold))
L.append(self.get_titles_list()) for line in self.get_data_list():
L.cells += [[x for x in line] for line in self.get_data_list()] ses.append_row(ses.make_row(line, style_base))
if self.caption: if self.caption:
L.append([]) # empty line ses.append_blank_row() # empty line
L.append([self.caption]) ses.append_single_cell_row(self.caption, style_base)
if self.origin: if self.origin:
L.append([]) # empty line ses.append_blank_row() # empty line
L.append([self.origin]) ses.append_single_cell_row(self.origin, style_base)
if wb is None:
return L.gen_workbook(wb=wb) return ses.generate_standalone()
else:
ses.generate_embeded()
def text(self): def text(self):
"raw text representation of the table" "raw text representation of the table"
@ -648,10 +650,10 @@ class GenTable(object):
return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") return scu.sendPDFFile(REQUEST, doc, filename + ".pdf")
else: else:
return doc return doc
elif format == "xls": elif format == "xls" or format == "xlsx":
xls = self.excel() xls = self.excel()
if publish: if publish:
return sco_excel.sendExcelFile(REQUEST, xls, filename + ".xls") return sco_excel.send_excel_file(REQUEST, xls, filename + ".xls")
else: else:
return xls return xls
elif format == "text": elif format == "text":

View File

@ -28,34 +28,35 @@
""" Excel file handling """ Excel file handling
""" """
import time, datetime 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 import app.scodoc.sco_utils as scu
from app.scodoc import notesdb 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 from app.scodoc import sco_preferences
import six from app.scodoc.notes_log import log
from openpyxl import Workbook from app.scodoc.sco_exceptions import ScoValueError
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): 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. """publication fichier.
(on ne doit rien avoir émis avant, car ici sont générés les entetes) (on ne doit rien avoir émis avant, car ici sont générés les entetes)
""" """
@ -64,14 +65,18 @@ def send_excel_file(REQUEST, data, filename):
.replace("&", "") .replace("&", "")
.replace(" ", "_") .replace(" ", "_")
) )
REQUEST.RESPONSE.setHeader("content-type", scu.XLSX_MIMETYPE) request.RESPONSE.setHeader("content-type", scu.XLSX_MIMETYPE)
REQUEST.RESPONSE.setHeader( request.RESPONSE.setHeader(
"content-disposition", 'attachment; filename="%s"' % filename "content-disposition", 'attachment; filename="%s"' % filename
) )
return data return data
## (stolen from xlrd) # 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 # Convert an Excel number (presumed to represent a date, a datetime or a time) into
# a Python datetime.datetime # a Python datetime.datetime
# @param xldate The Excel number # @param xldate The Excel number
@ -117,285 +122,361 @@ def xldate_as_datetime(xldate, datemode=0):
) + datetime.timedelta(seconds=seconds) ) + datetime.timedelta(seconds=seconds)
# Sous-classes pour ajouter methode savetostr() class ScoExcelBook:
# (generation de fichiers en memoire) """Permet la génération d'un classeur xlsx composé de plusieurs feuilles.
# XXX ne marche pas car accès a methodes privees (__xxx) usage:
# -> on utilise version modifiee par nous meme de pyExcelerator wb = ScoExcelBook()
# ws0 = wb.create_sheet('sheet name 0')
# class XlsDocWithSave(CompoundDoc.XlsDoc): ws1 = wb.create_sheet('sheet name 1')
# def savetostr(self, stream): ...
# #added by Emmanuel: save method, but returns a string steam = wb.generate()
# # 1. Align stream on 0x1000 boundary (and therefore on sector boundary) """
# padding = '\x00' * (0x1000 - (len(stream) % 0x1000)) def __init__(self):
# self.book_stream_len = len(stream) + len(padding) self.sheets = [] # list of sheets
# self.__build_directory() def create_sheet(self, sheet_name="feuille", default_style=None):
# self.__build_sat() sheet = ScoExcelSheet(sheet_name, default_style)
# self.__build_header() self.sheets.append(sheet)
# return self.header+self.packed_MSAT_1st+stream+padding+self.packed_MSAT_2nd+self.packed_SAT+self.dir_stream def generate(self):
""" génération d'un stream binaire représentant la totalité du classeur.
# class WorkbookWithSave(Workbook): retourne le flux
# def savetostr(self): """
# doc = XlsDocWithSave() wb = Workbook(write_only=True)
# return doc.savetostr(self.get_biff_data()) 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_MakeStyle( def excel_make_style(
bold=False, italic=False, color="black", bgcolor=None, halign=None, valign=None bold=False,
italic=False,
color: COLORS = COLORS.BLACK,
bgcolor: COLORS = None,
halign=None,
valign=None,
format_number=None,
): ):
style = XFStyle() """Contruit un style.
font = Font() Les couleurs peuvent être spécfiées soit par une valeur de COLORS,
if bold: soit par une chaine argb (exple "FF00FF00" pour le vert)
font.bold = bold color -- La couleur du texte
if italic: bgcolor -- la couleur de fond
font.italic = italic halign -- alignement horizontal ("left", "right", "center")
font.name = "Arial" valign -- alignement vertical ("top", "bottom", "center")
colour_index = COLOR_CODES.get(color, None) format_number -- formattage du contenu ("general", "@", ...)
if colour_index: """
font.colour_index = colour_index style = {}
font = Font(name="Arial", bold=bold, italic=italic, color=color.value)
style["font"] = font
if bgcolor: if bgcolor:
style.pattern = Pattern() style["fill"] = PatternFill(fill_type="solid", bgColor=bgcolor.value)
style.pattern.pattern = Pattern.SOLID_PATTERN if halign or valign:
style.pattern.pattern_fore_colour = COLOR_CODES.get(bgcolor, None)
al = None
if halign:
al = Alignment() al = Alignment()
if halign:
al.horz = { al.horz = {
"left": Alignment.HORZ_LEFT, "left": "left",
"right": Alignment.HORZ_RIGHT, "right": "right",
"center": Alignment.HORZ_CENTER, "center": "center",
}[halign] }[halign]
if valign: if valign:
if not al:
al = Alignment()
al.vert = { al.vert = {
"top": Alignment.VERT_TOP, "top": "top",
"bottom": VERT_BOTTOM, "bottom": "bottom",
"center": VERT_CENTER, "center": "center",
}[valign] }[valign]
if al: style["alignment"] = al
style.alignment = al if format_number is None:
style.font = font style["format_number"] = "general"
else:
style["format_number"] = format_number
return style return style
class ScoExcelSheet(object): class ScoExcelSheet:
def __init__(self, sheet_name="feuille", default_style=None): """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.sheet_name = sheet_name
self.cells = [] # list of list self.rows = [] # list of list of cells
self.cells_styles_lico = {} # { (li,co) : style } # self.cells_styles_lico = {} # { (li,co) : style }
self.cells_styles_li = {} # { li : style } # self.cells_styles_li = {} # { li : style }
self.cells_styles_co = {} # { co : style } # self.cells_styles_co = {} # { co : style }
if not default_style: if default_style is None:
default_style = Excel_MakeStyle() default_style = excel_make_style()
self.default_style = default_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_style(self, style=None, li=None, co=None): def set_column_dimension_width(self, cle, value):
if li != None and co != None: """Détermine la largeur d'une colonne.
self.cells_styles_lico[(li, co)] = style cle -- identifie la colonne ("A"n "B", ...)
elif li != None: value -- la dimension (unité : 7 pixels comme affiché dans Excel)
self.cells_styles_li[li] = style """
elif co != None: self.ws.column_dimensions[cle].width = value
self.cells_styles_co[co] = style
def append(self, l): def set_column_dimension_hidden(self, cle, value):
"""Append a line of cells""" """Masque ou affiche une colonne.
self.cells.append(l) cle -- identifie la colonne ("A"n "B", ...)
value -- boolean (vrai = colonne cachée)
"""
self.ws.column_dimensions[cle].hidden = value
def get_cell_style(self, li, co): def make_cell(self, value: any = None, style=None):
"""Get style for specified cell""" """Construit une cellule.
return ( value -- contenu de la cellule (texte ou numérique)
self.cells_styles_lico.get((li, co), None) style -- style par défaut de la feuille si non spécifié
or self.cells_styles_li.get(li, None) """
or self.cells_styles_co.get(co, None) cell = WriteOnlyCell(self.ws, value or "")
or self.default_style 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): def gen_workbook(self, wb=None):
"""TODO: à remplacer"""
"""Generates and returns a workbook from stored data. """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, add a sheet (tab) to the existing workbook (in this case, returns None).
""" """
if wb == None: if wb is None:
wb = Workbook() # Création du fichier wb = Workbook() # Création du fichier
sauvegarde = True sauvegarde = True
else: else:
sauvegarde = False sauvegarde = False
ws0 = wb.add_sheet(self.sheet_name) ws0 = wb.add_sheet(self.sheet_name)
li = 0 li = 0
for l in self.cells: for row in self.rows:
co = 0 co = 0
for c in l: for c in row:
# safety net: allow only str, int and float # safety net: allow only str, int and float
# #py3 #sco8 A revoir lors de la ré-écriture de ce module # #py3 #sco8 A revoir lors de la ré-écriture de ce module
# XXX if type(c) not in (IntType, FloatType): # XXX if type(c) not in (IntType, FloatType):
# c = str(c).decode(scu.SCO_ENCODING) # c = str(c).decode(scu.SCO_ENCODING)
ws0.write(li, co, c, self.get_cell_style(li, co)) ws0.write(li, co, c, self.get_cell_style(li, co))
co += 1 co += 1
li += 1 li += 1
if sauvegarde == True: if sauvegarde:
return wb.savetostr() return wb.savetostr()
else: else:
return None return None
def Excel_SimpleTable(titles=[], lines=[[]], SheetName="feuille", titlesStyles=[]): 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""" """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel"""
# XXX devrait maintenant utiliser ScoExcelSheet ws = ScoExcelSheet(sheet_name)
wb = Workbook() if titles is None:
ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING)) titles = []
if not titlesStyles: if lines is None:
style = Excel_MakeStyle(bold=True) lines = [[]]
titlesStyles = [style] * len(titles) if titles_styles is None:
style = excel_make_style(bold=True)
titles_styles = [style] * len(titles)
# ligne de titres # ligne de titres
col = 0 ws.append_row(
for it in titles: [ws.make_cell(it, style) for (it, style) in zip(titles, titles_styles)]
ws0.write(0, col, it.decode(scu.SCO_ENCODING), titlesStyles[col]) )
col += 1 default_style = excel_make_style()
# suite text_style = excel_make_style(format_number="@")
default_style = Excel_MakeStyle() for line in lines:
text_style = Excel_MakeStyle() cells = []
text_style.num_format_str = "@" for it in line:
li = 1
for l in lines:
col = 0
for it in l:
cell_style = default_style
# safety net: allow only str, int and float # safety net: allow only str, int and float
if isinstance(it, LongType): # XXX # TODO Plus de type Long en Python 3 ?
it = int(it) # assume all ScoDoc longs fits in int ! # if isinstance(it, long): # XXX
elif type(it) not in (IntType, FloatType): # XXX A REVOIR # it = int(it) # assume all ScoDoc longs fits in int !
it = str(it).decode(scu.SCO_ENCODING) cell_style = default_style
if type(it) not in (int, float): # XXX A REVOIR
cell_style = text_style cell_style = text_style
ws0.write(li, col, it, cell_style) cells.append(ws.make_cell(it, cell_style))
col += 1 ws.append_row(cells)
li += 1 return ws.generate_standalone()
#
return wb.savetostr()
def Excel_feuille_saisie(E, titreannee, description, lines): def excel_feuille_saisie(e, titreannee, description, lines):
"""Genere feuille excel pour saisie des notes. """Genere feuille excel pour saisie des notes.
E: evaluation (dict) E: evaluation (dict)
lines: liste de tuples lines: liste de tuples
(etudid, nom, prenom, etat, groupe, val, explanation) (etudid, nom, prenom, etat, groupe, val, explanation)
""" """
SheetName = "Saisie notes" sheet_name = "Saisie notes"
wb = Workbook() ws = ScoExcelSheet(sheet_name)
ws0 = wb.add_sheet(SheetName.decode(scu.SCO_ENCODING))
# ajuste largeurs colonnes (unite inconnue, empirique) # ajuste largeurs colonnes (unite inconnue, empirique)
ws0.col(0).width = 400 # codes ws.set_column_dimension_width("A", 11.0 / 7) # codes
ws0.col(1).width = 6000 # noms # ws.set_column_dimension_hidden("A", True) # codes
ws0.col(2).width = 4000 # prenoms ws.set_column_dimension_width("B", 164.00 / 7) # noms
ws0.col(3).width = 6000 # groupes ws.set_column_dimension_width("C", 109.0 / 7) # prenoms
ws0.col(4).width = 3000 # notes ws.set_column_dimension_width("D", 164.0 / 7) # groupes
ws0.col(5).width = 13000 # remarques 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 # styles
style_titres = XFStyle() style = {"font": font_base}
font0 = Font() style_titres = {"font": font_titre}
font0.bold = True style_expl = {"font": font_italic}
font0.name = "Arial"
font0.bold = True
font0.height = 14 * 0x14
style_titres.font = font0
style_expl = XFStyle() style_ro = { # cells read-only
font_expl = Font() "font": font_purple,
font_expl.name = "Arial" "border": border_right,
font_expl.italic = True }
font0.height = 12 * 0x14 style_dem = {
font_expl.colour_index = 0x0A # rouge, voir exemple format.py "font": font_brown,
style_expl.font = font_expl "border": border_top,
}
topborders = Borders() style_nom = { # style pour nom, prenom, groupe
topborders.top = 1 "font": font_base,
topleftborders = Borders() "border": border_top,
topleftborders.top = 1 }
topleftborders.left = 1 style_notes = {
rightborder = Borders() "font": font_bold,
rightborder.right = 1 "number_format": "general",
"fill": fill_light_yellow,
style_ro = XFStyle() # cells read-only "border": border_top,
font_ro = Font() }
font_ro.name = "Arial" style_comment = {
font_ro.colour_index = COLOR_CODES["mauve"] "font": font_blue,
style_ro.font = font_ro "border": border_top,
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 # ligne de titres
li = 0 ws.append_single_cell_row(
ws0.write( "Feuille saisie note (à enregistrer au format excel)", style_titres
li, 0, "Feuille saisie note (à enregistrer au format excel)", style_titres
) )
li += 1 # lignes d'instructions
ws0.write(li, 0, "Saisir les notes dans la colonne E (cases jaunes)", style_expl) ws.append_single_cell_row(
li += 1 "Saisir les notes dans la colonne E (cases jaunes)", style_expl
ws0.write(li, 0, "Ne pas modifier les cases en mauve !", style_expl) )
li += 1 ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl)
# Nom du semestre # Nom du semestre
ws0.write( ws.append_single_cell_row(scu.unescape_html(titreannee), style_titres)
li, 0, scu.unescape_html(titreannee).decode(scu.SCO_ENCODING), style_titres
)
li += 1
# description evaluation # description evaluation
ws0.write( ws.append_single_cell_row(scu.unescape_html(description), style_titres)
li, 0, scu.unescape_html(description).decode(scu.SCO_ENCODING), style_titres ws.append_single_cell_row(
"Evaluation du %s (coef. %g)" % (e["jour"], e["coefficient"]), style
) )
li += 1 # ligne blanche
ws0.write( ws.append_blank_row()
li, 0, "Evaluation du %s (coef. %g)" % (E["jour"], E["coefficient"]), style
)
li += 1
li += 1 # ligne blanche
# code et titres colonnes # code et titres colonnes
ws0.write(li, 0, "!%s" % E["evaluation_id"], style_ro) ws.append_row(
ws0.write(li, 1, "Nom", style_titres) [
ws0.write(li, 2, "Prénom", style_titres) ws.make_cell("!%s" % e["evaluation_id"], style_ro),
ws0.write(li, 3, "Groupe", style_titres) ws.make_cell("Nom", style_titres),
ws0.write(li, 4, "Note sur %g" % E["note_max"], style_titres) ws.make_cell("Prénom", style_titres),
ws0.write(li, 5, "Remarque", 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 # etudiants
for line in lines: for line in lines:
li += 1
st = style_nom st = style_nom
ws0.write(li, 0, ("!" + line[0]).decode(scu.SCO_ENCODING), style_ro) # code
if line[3] != "I": if line[3] != "I":
st = style_dem st = style_dem
if line[3] == "D": # demissionnaire if line[3] == "D": # demissionnaire
@ -404,56 +485,103 @@ def Excel_feuille_saisie(E, titreannee, description, lines):
s = line[3] # etat autre s = line[3] # etat autre
else: else:
s = line[4] # groupes TD/TP/... 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: try:
val = float(line[5]) val = float(line[5])
except: except ValueError:
val = line[5].decode(scu.SCO_ENCODING) val = line[5]
ws0.write(li, 4, val, style_notes) # note ws.append_row(
ws0.write(li, 5, line[6].decode(scu.SCO_ENCODING), style_comment) # comment [
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 # explication en bas
li += 2 ws.append_row([None, ws.make_cell("Code notes", style_titres)])
ws0.write(li, 1, "Code notes", style_titres) ws.append_row(
ws0.write(li + 1, 1, "ABS", style_expl) [
ws0.write(li + 1, 2, "absent (0)", style_expl) None,
ws0.write(li + 2, 1, "EXC", style_expl) ws.make_cell("ABS", style_expl),
ws0.write(li + 2, 2, "pas prise en compte", style_expl) ws.make_cell("absent (0)", 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) ws.append_row(
ws0.write(li + 4, 2, "pour supprimer note déjà entrée", style_expl) [
ws0.write(li + 5, 1, "", style_expl) None,
ws0.write(li + 5, 2, "cellule vide -> note non modifiée", style_expl) ws.make_cell("EXC", style_expl),
return wb.savetostr() 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_to_list(data, convert_to_string=str): # we may need 'encoding' argument ? 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 """returns list of list
convert_to_string is a conversion function applied to all non-string values (ie numbers) convert_to_string is a conversion function applied to all non-string values (ie numbers)
""" """
try: try:
P = parse_xls("", scu.SCO_ENCODING, doc=data) wb = load_workbook(filename=filelike, read_only=True, data_only=True)
except: except:
log("Excel_to_list: failure to import document") log("Excel_to_list: failure to import document")
open("/tmp/last_scodoc_import_failure.xls", "w").write(data) open("/tmp/last_scodoc_import_failure.xls", "w").write(filelike)
raise ScoValueError( raise ScoValueError(
"Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !"
) )
diag = [] # liste de chaines pour former message d'erreur diag = [] # liste de chaines pour former message d'erreur
# n'utilise que la première feuille # n'utilise que la première feuille
if len(P) < 1: if len(wb.get_sheet_names()) < 1:
diag.append("Aucune feuille trouvée dans le classeur !") diag.append("Aucune feuille trouvée dans le classeur !")
return diag, None return diag, None
if len(P) > 1: if len(wb.get_sheet_names()) > 1:
diag.append("Attention: n'utilise que la première feuille du classeur !") diag.append("Attention: n'utilise que la première feuille du classeur !")
# fill matrix # fill matrix
sheet_name, values = P[0] sheet_name = wb.get_sheet_names()[0]
ws = wb.get_sheet_by_name(sheet_name)
sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") 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: if not values:
diag.append("Aucune valeur trouvée dans le classeur !") diag.append(
"Aucune valeur trouvée dans la feuille %s !"
% sheet_name.decode(scu.SCO_ENCODING)
)
return diag, None return diag, None
indexes = list(values.keys()) indexes = list(values.keys())
# search numbers of rows and cols # search numbers of rows and cols
@ -461,40 +589,23 @@ def Excel_to_list(data, convert_to_string=str): # we may need 'encoding' argume
cols = [x[1] for x in indexes] cols = [x[1] for x in indexes]
nbcols = max(cols) + 1 nbcols = max(cols) + 1
nbrows = max(rows) + 1 nbrows = max(rows) + 1
M = [] m = []
for _ in range(nbrows): for _ in range(nbrows):
M.append([""] * nbcols) m.append([""] * nbcols)
for row_idx, col_idx in indexes: for row_idx, col_idx in indexes:
v = values[(row_idx, col_idx)] v = values[(row_idx, col_idx)]
if isinstance(v, six.text_type): # if isinstance(v, six.text_type):
v = v.encode(scu.SCO_ENCODING, "backslashreplace") # v = v.encode(scu.SCO_ENCODING, "backslashreplace")
elif convert_to_string: # elif convert_to_string:
v = convert_to_string(v) # v = convert_to_string(v)
M[row_idx][col_idx] = v m[row_idx][col_idx] = v
diag.append('Feuille "%s", %d lignes' % (sheet_name, len(M))) diag.append(
'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m))
)
# diag.append(str(M)) # diag.append(str(M))
# #
return diag, 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 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( def excel_feuille_listeappel(
@ -511,17 +622,17 @@ def excel_feuille_listeappel(
partitions = [] partitions = []
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
sheet_name = "Liste " + groupname sheet_name = "Liste " + groupname
wb = Workbook(write_only=True)
ws = wb.create_sheet(title=sheet_name) ws = ScoExcelSheet(sheet_name)
ws.column_dimensions["A"].width = 3 ws.set_column_dimension_width("A", 3)
ws.column_dimensions["B"].width = 35 ws.set_column_dimension_width("B", 35)
ws.column_dimensions["C"].width = 12 ws.set_column_dimension_width("C", 12)
font1 = Font(name="Arial", size=11) font1 = Font(name="Arial", size=11)
font1i = Font(name="Arial", size=10, italic=True) font1i = Font(name="Arial", size=10, italic=True)
font1b = Font(name="Arial", size=11, bold=True) font1b = Font(name="Arial", size=11, bold=True)
side_thin = Side(border_style="thin", color="FF000000") side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
border_tbl = Border(top=side_thin, bottom=side_thin, left=side_thin) border_tbl = Border(top=side_thin, bottom=side_thin, left=side_thin)
border_tblr = Border( border_tblr = Border(
@ -569,40 +680,37 @@ def excel_feuille_listeappel(
sem["date_fin"], sem["date_fin"],
) )
cell_2 = _make_cell(ws, title, style2) ws.append_row([None, ws.make_cell(title, style2)])
ws.append([None, cell_2])
# ligne 2 # ligne 2
cell_2 = _make_cell(ws, "Discipline :", style2) ws.append_row([None, ws.make_cell("Discipline :", style2)])
ws.append([None, cell_2])
# ligne 3 # ligne 3
cell_2 = _make_cell(ws, "Enseignant :", style2) cell_2 = ws.make_cell("Enseignant :", style2)
cell_6 = _make_cell(ws, ("Groupe %s" % groupname), style3) cell_6 = ws.make_cell(("Groupe %s" % groupname), style3)
ws.append([None, cell_2, None, None, None, None, cell_6]) ws.append_row([None, cell_2, None, None, None, None, cell_6])
# ligne 4: Avertissement pour ne pas confondre avec listes notes # ligne 4: Avertissement pour ne pas confondre avec listes notes
cell_2 = _make_cell( cell_2 = ws.make_cell(
ws, "Ne pas utiliser cette feuille pour saisir les notes !", style1i "Ne pas utiliser cette feuille pour saisir les notes !", style1i
) )
ws.append([None, None, cell_2]) ws.append_row([None, None, cell_2])
ws.append([None]) ws.append_blank_row()
ws.append([None]) ws.append_blank_row()
# ligne 7: Entête (contruction dans une liste cells) # ligne 7: Entête (contruction dans une liste cells)
cells = [None] # passe la première colonne cell_2 = ws.make_cell("Nom", style3)
cell_2 = _make_cell(ws, "Nom", style3) cells = [None, cell_2]
cells.append(cell_2)
for partition in partitions: for partition in partitions:
cells.append(_make_cell(ws, partition["partition_name"], style3)) cells.append(ws.make_cell(partition["partition_name"], style3))
if with_codes: if with_codes:
cells.append(_make_cell(ws, "etudid", style3)) cells.append(ws.make_cell("etudid", style3))
cells.append(_make_cell(ws, "code_nip", style3)) cells.append(ws.make_cell("code_nip", style3))
cells.append(_make_cell(ws, "code_ine", style3)) cells.append(ws.make_cell("code_ine", style3))
for i in range(nb_weeks): for i in range(nb_weeks):
cells.append(_make_cell(ws, "", style2b)) cells.append(ws.make_cell("", style2b))
ws.append(cells) ws.append_row(cells)
n = 0 n = 0
# pour chaque étudiant # pour chaque étudiant
@ -624,39 +732,33 @@ def excel_feuille_listeappel(
elif not paie: elif not paie:
nomprenom += " (non paiement)" nomprenom += " (non paiement)"
style_nom = style2t3bold style_nom = style2t3bold
cell_1 = _make_cell(ws, n, style1b) cell_1 = ws.make_cell(n, style1b)
cell_2 = _make_cell(ws, nomprenom, style_nom) cell_2 = ws.make_cell(nomprenom, style_nom)
cells = [cell_1, cell_2] cells = [cell_1, cell_2]
for partition in partitions: for partition in partitions:
if partition["partition_name"]: if partition["partition_name"]:
cells.append( cells.append(
_make_cell(ws, t.get(partition["partition_id"], ""), style2t3) ws.make_cell(t.get(partition["partition_id"], ""), style2t3)
) )
if with_codes: if with_codes:
cells.append(_make_cell(ws, t["etudid"], style2t3)) cells.append(ws.make_cell(t["etudid"], style2t3))
code_nip = t.get("code_nip", "") code_nip = t.get("code_nip", "")
cells.append(_make_cell(ws, code_nip, style2t3)) cells.append(ws.make_cell(code_nip, style2t3))
code_ine = t.get("code_ine", "") code_ine = t.get("code_ine", "")
cells.append(_make_cell(ws, code_ine, style2t3)) cells.append(ws.make_cell(code_ine, style2t3))
cells.append(_make_cell(ws, t.get("etath", ""), style2b)) cells.append(ws.make_cell(t.get("etath", ""), style2b))
for i in range(1, nb_weeks): for i in range(1, nb_weeks):
cells.append(_make_cell(ws, style=style2t3)) cells.append(ws.make_cell(style=style2t3))
# ws0.row(li).height = 850 # sans effet ? ws.append_row(cells)
# (openpyxl: en mode optimisé, les hauteurs de lignes doivent être spécifiées avant toutes les cellules)
ws.append(cells)
ws.append([None]) ws.append_blank_row()
# bas de page (date, serveur) # bas de page (date, serveur)
dt = time.strftime("%d/%m/%Y à %Hh%M") dt = time.strftime("%d/%m/%Y à %Hh%M")
if server_name: if server_name:
dt += " sur " + server_name dt += " sur " + server_name
cell_2 = _make_cell(ws, ("Liste éditée le " + dt), style1i) cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)
ws.append([None, cell_2]) ws.append_row([None, cell_2])
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) return ws.generate_standalone()
with NamedTemporaryFile() as tmp:
wb.save(tmp.name)
tmp.seek(0)
return tmp.read()

View File

@ -29,6 +29,7 @@
Formulaire revu en juillet 2016 Formulaire revu en juillet 2016
""" """
import sys
import time import time
import datetime import datetime
import psycopg2 import psycopg2
@ -178,8 +179,7 @@ def do_evaluation_upload_xls(context, REQUEST):
# XXX imaginer un redirect + msg erreur # XXX imaginer un redirect + msg erreur
raise AccessDenied("Modification des notes impossible pour %s" % authuser) raise AccessDenied("Modification des notes impossible pour %s" % authuser)
# #
data = REQUEST.form["notefile"].read() diag, lines = sco_excel.excel_file_to_list(REQUEST.form["notefile"])
diag, lines = sco_excel.Excel_to_list(data)
try: try:
if not lines: if not lines:
raise InvalidNoteValue() raise InvalidNoteValue()
@ -224,7 +224,7 @@ def do_evaluation_upload_xls(context, REQUEST):
ni += 1 ni += 1
except: except:
diag.append( diag.append(
'Erreur: feuille invalide ! (erreur ligne %d)<br/>"%s"' 'Erreur: Ligne invalide ! (erreur ligne %d)<br/>"%s"'
% (ni, str(lines[ni])) % (ni, str(lines[ni]))
) )
raise InvalidNoteValue() raise InvalidNoteValue()
@ -805,8 +805,8 @@ def feuille_saisie_notes(context, evaluation_id, group_ids=[], REQUEST=None):
) )
filename = "notes_%s_%s.xls" % (evalname, gr_title_filename) filename = "notes_%s_%s.xls" % (evalname, gr_title_filename)
xls = sco_excel.Excel_feuille_saisie(E, sem["titreannee"], description, lines=L) xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L)
return sco_excel.sendExcelFile(REQUEST, xls, filename) return sco_excel.send_excel_file(REQUEST, xls, filename)
def has_existing_decision(context, M, E, etudid): def has_existing_decision(context, M, E, etudid):

View File

@ -1551,6 +1551,7 @@ sco_publish(
"/saisie_notes_tableur", "/saisie_notes_tableur",
sco_saisie_notes.saisie_notes_tableur, sco_saisie_notes.saisie_notes_tableur,
Permission.ScoEnsView, Permission.ScoEnsView,
methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/feuille_saisie_notes", "/feuille_saisie_notes",