diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index b7111a32..d04b57c4 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -1,764 +1,775 @@ -# -*- 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() +# -*- 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): + """Crée une nouvelle feuille dans ce classeur + sheet_name -- le nom de la feuille + default_style -- le style par défaut + """ + sheet = ScoExcelSheet(sheet_name, default_style) + self.sheets.append(sheet) + return 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, + font_name="Arial", + size=10, +): + """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", "@", ...) + font_name -- police + size -- taille de police + """ + style = {} + font = Font(name=font_name, bold=bold, italic=italic, color=color.value, size=size) + style["font"] = font + if bgcolor: + style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) + if halign or valign: + al = Alignment() + if halign: + al.horizontal = { + "left": "left", + "right": "right", + "center": "center", + }[halign] + if valign: + al.vertical = { + "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 (dictionnaire cf. excel_make_style) de la feuille si non spécifié + """ + cell = WriteOnlyCell(self.ws, value or "") + # if style is not None and "fill" in style: + # toto() + 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=None): + 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.xlsx", "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_prepajury.py b/app/scodoc/sco_prepajury.py index 057f8ea5..85f347ff 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -1,318 +1,322 @@ -# -*- 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 -# -############################################################################## - -"""Feuille excel pour preparation des jurys -""" -import time - -import app.scodoc.sco_utils as scu -from app.scodoc import sco_abs -from app.scodoc import sco_groups -from app.scodoc import sco_cache -from app.scodoc import sco_excel -from app.scodoc import sco_formsemestre -from app.scodoc import sco_parcours_dut -from app.scodoc import sco_codes_parcours -from app.scodoc import VERSION -from app.scodoc import sco_etud -from app.scodoc import sco_preferences - - -def feuille_preparation_jury(context, formsemestre_id, REQUEST): - "Feuille excel pour preparation des jurys" - nt = sco_cache.NotesTableCache.get( - formsemestre_id - ) # > get_etudids, get_etud_moy_gen, get_ues, get_etud_ue_status, get_etud_decision_sem, identdict, - etudids = nt.get_etudids(sorted=True) # tri par moy gen - sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) - - etud_groups = sco_groups.formsemestre_get_etud_groupnames(context, formsemestre_id) - main_partition_id = sco_groups.formsemestre_get_main_partition( - context, formsemestre_id - )["partition_id"] - - prev_moy_ue = scu.DictDefault(defaultvalue={}) # ue_code_s : { etudid : moy ue } - prev_ue_acro = {} # ue_code_s : acronyme (à afficher) - prev_moy = {} # moyennes gen sem prec - moy_ue = scu.DictDefault(defaultvalue={}) # ue_acro : moyennes { etudid : moy ue } - ue_acro = {} # ue_code_s : acronyme (à afficher) - moy = {} # moyennes gen - moy_inter = {} # moyenne gen. sur les 2 derniers semestres - code = {} # decision existantes s'il y en a - autorisations = {} - prev_code = {} # decisions sem prec - assidu = {} - parcours = {} # etudid : parcours, sous la forme S1, S2, S2, S3 - groupestd = {} # etudid : nom groupe principal - nbabs = {} - nbabsjust = {} - for etudid in etudids: - info = sco_etud.get_etud_info(etudid=etudid, filled=True) - if not info: - continue # should not occur... - etud = info[0] - Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id) - if Se.prev: - ntp = sco_cache.NotesTableCache.get( - Se.prev["formsemestre_id"] - ) # > get_ues, get_etud_ue_status, get_etud_moy_gen, get_etud_decision_sem - for ue in ntp.get_ues(filter_sport=True): - ue_status = ntp.get_etud_ue_status(etudid, ue["ue_id"]) - ue_code_s = ( - ue["ue_code"] + "_%s" % ntp.sem["semestre_id"] - ) # code indentifiant l'UE - prev_moy_ue[ue_code_s][etudid] = ue_status["moy"] - # prev_ue_acro[ue_code_s] = (ue['numero'], ue['acronyme']) - prev_ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) - prev_moy[etudid] = ntp.get_etud_moy_gen(etudid) - prev_decision = ntp.get_etud_decision_sem(etudid) - if prev_decision: - prev_code[etudid] = prev_decision["code"] - if prev_decision["compense_formsemestre_id"]: - prev_code[etudid] += "+" # indique qu'il a servi a compenser - - moy[etudid] = nt.get_etud_moy_gen(etudid) - for ue in nt.get_ues(filter_sport=True): - ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) - ue_code_s = ue["ue_code"] + "_%s" % nt.sem["semestre_id"] - moy_ue[ue_code_s][etudid] = ue_status["moy"] - # ue_acro[ue_code_s] = (ue['numero'], ue['acronyme']) - ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) - - if Se.prev: - try: - moy_inter[etudid] = (moy[etudid] + prev_moy[etudid]) / 2.0 - except: - pass - - decision = nt.get_etud_decision_sem(etudid) - if decision: - code[etudid] = decision["code"] - if decision["compense_formsemestre_id"]: - code[etudid] += "+" # indique qu'il a servi a compenser - assidu[etudid] = {0: "Non", 1: "Oui"}.get(decision["assidu"], "") - aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( - context, etudid, formsemestre_id - ) - autorisations[etudid] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list]) - # parcours: - parcours[etudid] = Se.get_parcours_descr() - # groupe principal (td) - groupestd[etudid] = "" - for s in etud["sems"]: - if s["formsemestre_id"] == formsemestre_id: - groupestd[etudid] = etud_groups.get(etudid, {}).get( - main_partition_id, "" - ) - # absences: - nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) - nbabs[etudid] = nbabs - nbabsjust[etudid] = nbabs - nbabsjust - - # Codes des UE "semestre précédent": - ue_prev_codes = list(prev_moy_ue.keys()) - ue_prev_codes.sort( - key=lambda x, prev_ue_acro=prev_ue_acro: prev_ue_acro[ # pylint: disable=undefined-variable - x - ] - ) - # Codes des UE "semestre courant": - ue_codes = list(moy_ue.keys()) - ue_codes.sort( - key=lambda x, ue_acro=ue_acro: ue_acro[x] # pylint: disable=undefined-variable - ) - - sid = sem["semestre_id"] - sn = sp = "" - if sid >= 0: - sn = "S%s" % sid - if prev_moy: # si qq chose dans precedent - sp = "S%s" % (sid - 1) - - L = sco_excel.ScoExcelSheet(sheet_name="Prepa Jury %s" % sn) - L.append(["Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"])]) - L.append([]) # empty line - - titles = ["Rang"] - if sco_preferences.get_preference("prepa_jury_nip"): - titles.append("NIP") - if sco_preferences.get_preference("prepa_jury_ine"): - titles.append("INE") - titles += [ - "etudid", - "Civ.", - "Nom", - "Prénom", - "Naissance", - "Bac", - "Spe", - "Rg Adm", - "Parcours", - "Groupe", - ] - - if prev_moy: # si qq chose dans precedent - titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [ - "Moy %s" % sp, - "Décision %s" % sp, - ] - titles += [ue_acro[x][1] for x in ue_codes] + ["Moy %s" % sn] - if moy_inter: - titles += ["Moy %s-%s" % (sp, sn)] - titles += ["Abs", "Abs Injust."] - if code: - titles.append("Proposit. %s" % sn) - if autorisations: - titles.append("Autorisations") - # titles.append('Assidu') - L.append(titles) - style_bold = sco_excel.Excel_MakeStyle(bold=True) - style_center = sco_excel.Excel_MakeStyle(halign="center") - style_boldcenter = sco_excel.Excel_MakeStyle(bold=True, halign="center") - style_moy = sco_excel.Excel_MakeStyle( - bold=True, halign="center", bgcolor="lightyellow" - ) - style_note = sco_excel.Excel_MakeStyle(halign="right") - style_note_bold = sco_excel.Excel_MakeStyle(halign="right", bold=True) - if prev_moy: - tit_prev_moy = "Moy " + sp - col_prev_moy = titles.index(tit_prev_moy) - tit_moy = "Moy " + sn - col_moy = titles.index(tit_moy) - col_abs = titles.index("Abs") - - L.set_style(style_bold, li=0) - L.set_style(style_boldcenter, li=2) - - def fmt(x): - "reduit les notes a deux chiffres" - x = scu.fmt_note(x, keep_numeric=False) - try: - return float(x) - except: - return x - - i = 1 # numero etudiant - for etudid in etudids: - etud = nt.identdict[etudid] - l = [str(i)] - if sco_preferences.get_preference("prepa_jury_nip"): - l.append(etud["code_nip"]) - if sco_preferences.get_preference("prepa_jury_ine"): - l.append(etud["code_ine"]) - l += [ - etudid, - etud["civilite_str"], - sco_etud.format_nom(etud["nom"]), - sco_etud.format_prenom(etud["prenom"]), - etud["date_naissance"], - etud["bac"], - etud["specialite"], - etud["classement"], - parcours[etudid], - groupestd[etudid], - ] - co = len(l) - if prev_moy: - for ue_acro in ue_prev_codes: - l.append(fmt(prev_moy_ue.get(ue_acro, {}).get(etudid, ""))) - L.set_style(style_note, li=i + 2, co=co) - co += 1 - l.append(fmt(prev_moy.get(etudid, ""))) - l.append(prev_code.get(etudid, "")) - # L.set_style(style_bold, li=i+2, co=col_prev_moy+1) # moy gen prev - # L.set_style(style_moy, li=i+2, co=col_prev_moy+2) # decision prev - L.set_style(style_bold, li=i + 2, co=col_prev_moy) # moy gen prev - L.set_style(style_moy, li=i + 2, co=col_prev_moy + 1) # decision prev - co += 2 - - for ue_acro in ue_codes: - l.append(fmt(moy_ue.get(ue_acro, {}).get(etudid, ""))) - L.set_style(style_note, li=i + 2, co=co) - co += 1 - l.append(fmt(moy.get(etudid, ""))) - # L.set_style(style_note_bold, li=i+2, co=col_moy+1) # moy gen - L.set_style(style_note_bold, li=i + 2, co=col_moy) # moy gen - co += 1 - if moy_inter: - l.append(fmt(moy_inter.get(etudid, ""))) - L.set_style(style_note, li=i + 2, co=co) - l.append(fmt(str(nbabs.get(etudid, "")))) - l.append(fmt(str(nbabsjust.get(etudid, "")))) - if code: - l.append(code.get(etudid, "")) - if autorisations: - l.append(autorisations.get(etudid, "")) - # l.append(assidu.get(etudid, '')) - L.append(l) - i += 1 - L.set_style(style_center, li=i + 1, co=col_abs) # absences - L.set_style(style_center, li=i + 1, co=col_abs + 1) # absences injustifiées - L.set_style(style_moy, li=i + 1, co=col_abs + 2) # décision semestre - L.set_style(style_center, li=i + 1, co=col_abs + 3) # Autorisations - # - L.append([""]) - # Explications des codes - codes = list(sco_codes_parcours.CODES_EXPL.keys()) - codes.sort() - L.append(["Explication des codes"]) - for code in codes: - L.append(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]]) - L.append( - [ - "", - "", - "", - "ADM+", - "indique que le semestre a déjà servi à en compenser un autre", - ] - ) - # UE : Correspondances acronyme et titre complet - L.append([""]) - L.append(["Titre des UE"]) - if prev_moy: - for ue in ntp.get_ues(filter_sport=True): - L.append(["", "", "", ue["acronyme"], ue["titre"]]) - for ue in nt.get_ues(filter_sport=True): - L.append(["", "", "", ue["acronyme"], ue["titre"]]) - # - L.append([""]) - L.append( - [ - "Préparé par %s le %s sur %s pour %s" - % ( - VERSION.SCONAME, - time.strftime("%d/%m/%Y"), - REQUEST.BASE0, - REQUEST.AUTHENTICATED_USER, - ) - ] - ) - - xls = L.gen_workbook() - - return sco_excel.sendExcelFile(REQUEST, xls, "PrepaJury%s.xls" % sn) +# -*- 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 +# +############################################################################## + +"""Feuille excel pour preparation des jurys +""" +import time + +import app.scodoc.sco_utils as scu +from app.scodoc import sco_abs +from app.scodoc import sco_groups +from app.scodoc import sco_cache +from app.scodoc import sco_excel +from app.scodoc import sco_formsemestre +from app.scodoc import sco_parcours_dut +from app.scodoc import sco_codes_parcours +from app.scodoc import VERSION +from app.scodoc import sco_etud +from app.scodoc import sco_preferences +from app.scodoc.sco_excel import ScoExcelSheet + + +def feuille_preparation_jury(context, formsemestre_id, REQUEST): + "Feuille excel pour preparation des jurys" + nt = sco_cache.NotesTableCache.get( + formsemestre_id + ) # > get_etudids, get_etud_moy_gen, get_ues, get_etud_ue_status, get_etud_decision_sem, identdict, + etudids = nt.get_etudids(sorted=True) # tri par moy gen + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + + etud_groups = sco_groups.formsemestre_get_etud_groupnames(context, formsemestre_id) + main_partition_id = sco_groups.formsemestre_get_main_partition( + context, formsemestre_id + )["partition_id"] + + prev_moy_ue = scu.DictDefault(defaultvalue={}) # ue_code_s : { etudid : moy ue } + prev_ue_acro = {} # ue_code_s : acronyme (à afficher) + prev_moy = {} # moyennes gen sem prec + moy_ue = scu.DictDefault(defaultvalue={}) # ue_acro : moyennes { etudid : moy ue } + ue_acro = {} # ue_code_s : acronyme (à afficher) + moy = {} # moyennes gen + moy_inter = {} # moyenne gen. sur les 2 derniers semestres + code = {} # decision existantes s'il y en a + autorisations = {} + prev_code = {} # decisions sem prec + assidu = {} + parcours = {} # etudid : parcours, sous la forme S1, S2, S2, S3 + groupestd = {} # etudid : nom groupe principal + nbabs = {} + nbabsjust = {} + for etudid in etudids: + info = sco_etud.get_etud_info(etudid=etudid, filled=True) + if not info: + continue # should not occur... + etud = info[0] + Se = sco_parcours_dut.SituationEtudParcours(context, etud, formsemestre_id) + if Se.prev: + ntp = sco_cache.NotesTableCache.get( + Se.prev["formsemestre_id"] + ) # > get_ues, get_etud_ue_status, get_etud_moy_gen, get_etud_decision_sem + for ue in ntp.get_ues(filter_sport=True): + ue_status = ntp.get_etud_ue_status(etudid, ue["ue_id"]) + ue_code_s = ( + ue["ue_code"] + "_%s" % ntp.sem["semestre_id"] + ) # code indentifiant l'UE + prev_moy_ue[ue_code_s][etudid] = ue_status["moy"] + # prev_ue_acro[ue_code_s] = (ue['numero'], ue['acronyme']) + prev_ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) + prev_moy[etudid] = ntp.get_etud_moy_gen(etudid) + prev_decision = ntp.get_etud_decision_sem(etudid) + if prev_decision: + prev_code[etudid] = prev_decision["code"] + if prev_decision["compense_formsemestre_id"]: + prev_code[etudid] += "+" # indique qu'il a servi a compenser + + moy[etudid] = nt.get_etud_moy_gen(etudid) + for ue in nt.get_ues(filter_sport=True): + ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"]) + ue_code_s = ue["ue_code"] + "_%s" % nt.sem["semestre_id"] + moy_ue[ue_code_s][etudid] = ue_status["moy"] + # ue_acro[ue_code_s] = (ue['numero'], ue['acronyme']) + ue_acro[ue_code_s] = (ue["numero"], ue["acronyme"], ue["titre"]) + + if Se.prev: + try: + moy_inter[etudid] = (moy[etudid] + prev_moy[etudid]) / 2.0 + except: + pass + + decision = nt.get_etud_decision_sem(etudid) + if decision: + code[etudid] = decision["code"] + if decision["compense_formsemestre_id"]: + code[etudid] += "+" # indique qu'il a servi a compenser + assidu[etudid] = {0: "Non", 1: "Oui"}.get(decision["assidu"], "") + aut_list = sco_parcours_dut.formsemestre_get_autorisation_inscription( + context, etudid, formsemestre_id + ) + autorisations[etudid] = ", ".join(["S%s" % x["semestre_id"] for x in aut_list]) + # parcours: + parcours[etudid] = Se.get_parcours_descr() + # groupe principal (td) + groupestd[etudid] = "" + for s in etud["sems"]: + if s["formsemestre_id"] == formsemestre_id: + groupestd[etudid] = etud_groups.get(etudid, {}).get( + main_partition_id, "" + ) + # absences: + e_nbabs, e_nbabsjust = sco_abs.get_abs_count(etudid, sem) + nbabs[etudid] = e_nbabs + nbabsjust[etudid] = e_nbabs - e_nbabsjust + + # Codes des UE "semestre précédent": + ue_prev_codes = list(prev_moy_ue.keys()) + ue_prev_codes.sort( + key=lambda x, prev_ue_acro=prev_ue_acro: prev_ue_acro[ # pylint: disable=undefined-variable + x + ] + ) + # Codes des UE "semestre courant": + ue_codes = list(moy_ue.keys()) + ue_codes.sort( + key=lambda x, ue_acro=ue_acro: ue_acro[x] # pylint: disable=undefined-variable + ) + + sid = sem["semestre_id"] + sn = sp = "" + if sid >= 0: + sn = "S%s" % sid + if prev_moy: # si qq chose dans precedent + sp = "S%s" % (sid - 1) + + ws = sco_excel.ScoExcelSheet(sheet_name="Prepa Jury %s" % sn) + # génération des styles + style_bold = sco_excel.excel_make_style(size=10, bold=True) + style_center = sco_excel.excel_make_style(halign="center") + style_boldcenter = sco_excel.excel_make_style(bold=True, halign="center") + style_moy = sco_excel.excel_make_style( + bold=True, halign="center", bgcolor=sco_excel.COLORS.LIGHT_YELLOW + ) + style_note = sco_excel.excel_make_style(halign="right") + style_note_bold = sco_excel.excel_make_style(halign="right", bold=True) + + # Première ligne + ws.append_single_cell_row( + "Feuille préparation Jury %s" % scu.unescape_html(sem["titreannee"]), style_bold + ) + ws.append_blank_row() + + # Ligne de titre + titles = ["Rang"] + if sco_preferences.get_preference("prepa_jury_nip"): + titles.append("NIP") + if sco_preferences.get_preference("prepa_jury_ine"): + titles.append("INE") + titles += [ + "etudid", + "Civ.", + "Nom", + "Prénom", + "Naissance", + "Bac", + "Spe", + "Rg Adm", + "Parcours", + "Groupe", + ] + if prev_moy: # si qq chose dans precedent + titles += [prev_ue_acro[x][1] for x in ue_prev_codes] + [ + "Moy %s" % sp, + "Décision %s" % sp, + ] + titles += [ue_acro[x][1] for x in ue_codes] + ["Moy %s" % sn] + if moy_inter: + titles += ["Moy %s-%s" % (sp, sn)] + titles += ["Abs", "Abs Injust."] + if code: + titles.append("Proposit. %s" % sn) + if autorisations: + titles.append("Autorisations") + # titles.append('Assidu') + ws.append_row(ws.make_row(titles, style_boldcenter)) + if prev_moy: + tit_prev_moy = "Moy " + sp + col_prev_moy = titles.index(tit_prev_moy) + tit_moy = "Moy " + sn + col_moy = titles.index(tit_moy) + col_abs = titles.index("Abs") + + def fmt(x): + "reduit les notes a deux chiffres" + x = scu.fmt_note(x, keep_numeric=False) + try: + return float(x) + except: + return x + + i = 1 # numero etudiant + for etudid in etudids: + cells = [] + etud = nt.identdict[etudid] + cells.append(ws.make_cell(str(i))) + if sco_preferences.get_preference("prepa_jury_nip"): + cells.append(ws.make_cell(etud["code_nip"])) + if sco_preferences.get_preference("prepa_jury_ine"): + cells.append(ws.make_cell(["code_ine"])) + cells += ws.make_row( + [ + etudid, + etud["civilite_str"], + sco_etud.format_nom(etud["nom"]), + sco_etud.format_prenom(etud["prenom"]), + etud["date_naissance"], + etud["bac"], + etud["specialite"], + etud["classement"], + parcours[etudid], + groupestd[etudid], + ] + ) + co = len(cells) + if prev_moy: + for ue_acro in ue_prev_codes: + cells.append( + ws.make_cell( + prev_moy_ue.get(ue_acro, {}).get(etudid, ""), style_note + ) + ) + co += 1 + cells.append( + ws.make_cell(fmt(prev_moy.get(etudid, "")), style_bold) + ) # moy gen prev + cells.append( + ws.make_cell(prev_code.get(etudid, ""), style_moy) + ) # decision prev + co += 2 + + for ue_acro in ue_codes: + cells.append( + ws.make_cell(moy_ue.get(ue_acro, {}).get(etudid, ""), style_note) + ) + co += 1 + cells.append(ws.make_cell(moy.get(etudid, ""), style_note_bold)) # moy gen + co += 1 + if moy_inter: + cells.append(ws.make_cell(moy_inter.get(etudid, ""), style_note)) + cells.append(ws.make_cell(str(nbabs.get(etudid, "")), style_center)) + cells.append(ws.make_cell(str(nbabsjust.get(etudid, "")), style_center)) + if code: + cells.append(ws.make_cell(code.get(etudid, ""), style_moy)) + if autorisations.get(etudid, ""): + cells.append(ws.make_row(autorisations.get(etudid, ""), style_center)) + # l.append(assidu.get(etudid, '')) + ws.append_row(cells) + i += 1 + # + ws.append_blank_row() + # Explications des codes + codes = list(sco_codes_parcours.CODES_EXPL.keys()) + codes.sort() + ws.append_single_cell_row("Explication des codes") + for code in codes: + ws.append_row( + ws.make_row(["", "", "", code, sco_codes_parcours.CODES_EXPL[code]]) + ) + ws.append_row( + ws.make_row( + [ + "", + "", + "", + "ADM+", + "indique que le semestre a déjà servi à en compenser un autre", + ] + ) + ) + # UE : Correspondances acronyme et titre complet + ws.append_blank_row() + ws.append_single_cell_row("Titre des UE") + if prev_moy: + for ue in ntp.get_ues(filter_sport=True): + ws.append_row(ws.make_row(["", "", "", ue["acronyme"], ue["titre"]])) + for ue in nt.get_ues(filter_sport=True): + ws.append_row(ws.make_row(["", "", "", ue["acronyme"], ue["titre"]])) + # + ws.append_blank_row() + ws.append_single_cell_row( + "Préparé par %s le %s sur %s pour %s" + % ( + VERSION.SCONAME, + time.strftime("%d/%m/%Y"), + REQUEST.BASE0, + REQUEST.AUTHENTICATED_USER, + ) + ) + xls = ws.generate_standalone() + + return sco_excel.send_excel_file(REQUEST, xls, "PrepaJury%s.xlsx" % sn)