diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 32cf27c0a..5ac78d9fd 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -40,10 +40,15 @@ from openpyxl.cell import WriteOnlyCell from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL from openpyxl.comments import Comment +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.sco_exceptions import ScoValueError +from app.scodoc import sco_preferences from app import log from app.scodoc.sco_exceptions import ScoValueError @@ -79,52 +84,14 @@ def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE): # 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): return openpyxl.utils.datetime.from_ISO8601(xldate) - # 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) + + +def adjust_sheetname(sheet_name): + # Le nom de la feuille ne peut faire plus de 31 caractères. + # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) + return sheet_name[:31] class ScoExcelBook: @@ -139,13 +106,16 @@ class ScoExcelBook: def __init__(self): self.sheets = [] # list of sheets + self.wb = Workbook(write_only=True) 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) + sheet_name = adjust_sheetname(sheet_name) + ws = self.wb.create_sheet(sheet_name) + sheet = ScoExcelSheet(sheet_name, default_style, ws) self.sheets.append(sheet) return sheet @@ -153,12 +123,11 @@ class ScoExcelBook: """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) + sheet.prepare() # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) with NamedTemporaryFile() as tmp: - wb.save(tmp.name) + self.wb.save(tmp.name) tmp.seek(0) return tmp.read() @@ -166,6 +135,7 @@ class ScoExcelBook: def excel_make_style( bold=False, italic=False, + outline=False, color: COLORS = COLORS.BLACK, bgcolor: COLORS = None, halign=None, @@ -186,7 +156,14 @@ def excel_make_style( size -- taille de police """ style = {} - font = Font(name=font_name, bold=bold, italic=italic, color=color.value, size=size) + font = Font( + name=font_name, + bold=bold, + italic=italic, + outline=outline, + color=color.value, + size=size, + ) style["font"] = font if bgcolor: style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) @@ -222,42 +199,94 @@ class ScoExcelSheet: * 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. + def __init__(self, sheet_name="feuille", default_style=None, ws=None): + """Création de la feuille. sheet_name + -- le nom de la feuille default_style + -- le style par défaut des cellules ws + -- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet + créée par le workbook propriétaire un workbook est crée et associé à cette feuille. """ # Le nom de la feuille ne peut faire plus de 31 caractères. # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?) - self.sheet_name = sheet_name[ - :31 - ] # if len(sheet_name) > 31: sheet_name = 'Feuille' ? - 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 } + self.sheet_name = adjust_sheetname(sheet_name) 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) + if ws is None: + self.wb = Workbook() + self.ws = self.wb.active + self.ws.title = self.sheet_name + else: + self.wb = None + self.ws = ws + # internal data + self.rows = [] # list of list of cells self.column_dimensions = {} + self.row_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) + def excel_make_composite_style( + self, + alignment=None, + border=None, + fill=None, + number_format=None, + font=None, + ): + style = {} + if font is not None: + style["font"] = font + if alignment is not None: + style["alignment"] = alignment + if border is not None: + style["border"] = border + if fill is not None: + style["fill"] = fill + if number_format is None: + style["number_format"] = FORMAT_GENERAL + else: + style["number_format"] = number_format + return style + + @staticmethod + def i2col(idx): + if idx < 26: # one letter key + return chr(idx + 65) + else: # two letters AA..ZZ + first = (idx // 26) + 66 + second = (idx % 26) + 65 + return "" + chr(first) + chr(second) + + def set_column_dimension_width(self, cle=None, value=21): + """Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None, + value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels + comme affiché dans Excel) """ - self.ws.column_dimensions[cle].width = value + if cle is None: + for i, val in enumerate(value): + self.ws.column_dimensions[self.i2col(i)].width = val + # No keys: value is a list of widths + elif type(cle) == str: # accepts set_column_with("D", ...) + self.ws.column_dimensions[cle].width = value + else: + self.ws.column_dimensions[self.i2col(cle)].width = value - def set_column_dimension_hidden(self, cle, value): - """Masque ou affiche une colonne. - cle -- identifie la colonne ("A"n "B", ...) + def set_row_dimension_height(self, cle=None, value=21): + """Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None, + value donne la liste des hauteurs de colonnes depuis 1, 2, 3, ... value -- la dimension + """ + if cle is None: + for i, val in enumerate(value, start=1): + self.ws.row_dimensions[i].height = val + # No keys: value is a list of widths + else: + self.ws.row_dimensions[cle].height = value + + def set_row_dimension_hidden(self, cle, value): + """Masque ou affiche une ligne. + cle -- identifie la colonne (1...) value -- boolean (vrai = colonne cachée) """ - self.ws.column_dimensions[cle].hidden = value + self.ws.row_dimensions[cle].hidden = value def make_cell(self, value: any = None, style=None, comment=None): """Construit une cellule. @@ -271,8 +300,12 @@ class ScoExcelSheet: style = self.default_style if "font" in style: cell.font = style["font"] + if "alignment" in style: + cell.alignment = style["alignment"] if "border" in style: cell.border = style["border"] + if "fill" in style: + cell.fill = style["fill"] if "number_format" in style: cell.number_format = style["number_format"] if "fill" in style: @@ -311,73 +344,31 @@ class ScoExcelSheet: """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): + def prepare(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 col in self.row_dimensions.keys(): + self.ws.row_dimensions[col] = self.row_dimensions[col] for row in self.rows: self.ws.append(row) - def generate_standalone(self): + def generate(self): """génération d'un classeur mono-feuille""" - self._generate_ws() + # this method makes sense only if it is a standalone worksheet (else call workbook.generate() + if self.wb is None: # embeded sheet + raise ScoValueError("can't generate a single sheet from a ScoWorkbook") + # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) + self.prepare() 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, comments=None @@ -416,7 +407,7 @@ def excel_simple_table( cell_style = text_style cells.append(ws.make_cell(it, cell_style)) ws.append_row(cells) - return ws.generate_standalone() + return ws.generate() def excel_feuille_saisie(e, titreannee, description, lines): @@ -577,7 +568,7 @@ def excel_feuille_saisie(e, titreannee, description, lines): ws.make_cell("cellule vide -> note non modifiée", style_expl), ] ) - return ws.generate_standalone() + return ws.generate() def excel_bytes_to_list(bytes_content): @@ -797,4 +788,4 @@ def excel_feuille_listeappel( cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i) ws.append_row([None, cell_2]) - return ws.generate_standalone() + return ws.generate() diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 4f9e9a0b7..e0cf5b800 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -33,13 +33,30 @@ Contribution M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016 import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error import random import time +from copy import copy import flask +import wtforms.validators +from flask import request, render_template +from flask_login import current_user +from werkzeug import Response +from flask_wtf import FlaskForm +from wtforms import ( + StringField, + PasswordField, + BooleanField, + SubmitField, + SelectField, + RadioField, + HiddenField, + SelectMultipleField, + validators, +) +from wtforms.validators import ValidationError, DataRequired, Email, EqualTo from app.scodoc.sco_exceptions import ScoValueError import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app import log from app.scodoc import html_sco_header from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations @@ -57,23 +74,116 @@ from app.scodoc.gen_tables import GenTable from app.scodoc.sco_excel import * # XXX à vérifier from app.scodoc.TrivialFormulator import TrivialFormulator +_ = lambda x: x # sans babel +_l = _ -def do_placement_selectetuds(REQUEST): + +class PlacementForm(FlaskForm): + TOUS = "Tous" + evaluation_id = HiddenField("evaluation_id") + file_format = RadioField( + "Format de fichier", + choices=["pdf", "xls"], + validators=[ + wtforms.validators.DataRequired("indiquez le format du fichier attendu"), + ], + ) + surveillants = StringField( + "Surveillants", validators=[wtforms.validators.DataRequired("Test")] + ) + batiment = StringField("Batiment") + salle = StringField("Salle") + nb_rangs = SelectField( + "nb_rangs", coerce=int, choices=[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + ) + etiquetage = RadioField( + "Numérotation", + choices=["Continue", "Coordonnées"], + validators=[ + wtforms.validators.DataRequired("indiquez le style de numérotation"), + ], + ) + groups = SelectMultipleField( + "Groupe(s)", + validators=[ + wtforms.validators.DataRequired("indiquez au moins un groupe"), + ], + ) + submit = SubmitField("OK") + + def _set_evaluation_infos(self, evaluation_id): + eval_data = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) + if not eval_data: + raise ScoValueError("invalid evaluation_id") + eval_data = eval_data[0] + # groupes + groups = sco_groups.do_evaluation_listegroupes( + evaluation_id, include_default=True + ) + self.groups_tree = {} + self.has_groups = False + for group in groups: + partition = group["partition_name"] or self.TOUS # TODO check required + group_id = group["group_id"] + group_name = group["group_name"] or self.TOUS + if partition not in self.groups_tree: + self.groups_tree[partition] = {} + self.groups_tree[partition][group_name] = group_id + if partition != self.TOUS: + self.has_groups = True + self.groups_tree_length = len(self.groups_tree) + if self.has_groups: + choices = [] + for partition in self.groups_tree: + for groupe in self.groups_tree[partition]: + id = str(self.groups_tree[partition][groupe]) + choices.append((id, "%s (%s)" % (str(groupe), partition))) + self.groups.choices = choices + + +def placement_eval_selectetuds(evaluation_id, REQUEST=None): + form = PlacementForm( + request.form, + data={"evaluation_id": int(evaluation_id), "groups": PlacementForm.TOUS}, + ) + form._set_evaluation_infos(evaluation_id) + if form.validate_on_submit(): + exec_placement(form) + return flask.redirect(titi()) + H = [html_sco_header.sco_header(init_jquery_ui=True)] + H.append(sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)) + H.append("

Placement et émargement des étudiants

") + H.append(render_template("forms/placement.html", form=form)) + H.append( + """

Explications

+ """ + ) + F = html_sco_header.sco_footer() + return "\n".join(H) + "

" + F + + +def do_placement_selectetuds(): """ Choisi les étudiants et les infos sur la salle pour leur placement. """ - evaluation_id = int(REQUEST.form["evaluation_id"]) - E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) - if not E: - raise ScoValueError("invalid evaluation_id") - E = E[0] # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0] - # groupes - groups = sco_groups.do_evaluation_listegroupes(evaluation_id, include_default=True) - grlabs = [g["group_name"] or "tous" for g in groups] # legendes des boutons - grnams = [g["group_id"] for g in groups] # noms des checkbox - no_groups = (len(groups) == 1) and groups[0]["group_name"] is None - # description de l'evaluation H = [ sco_evaluations.evaluation_describe( @@ -247,7 +357,6 @@ def do_placement(REQUEST): "Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci." ) E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0] - # Check access # (admin, respformation, and responsable_id) if not sco_permissions_check.can_edit_notes(authuser, E["moduleimpl_id"]): @@ -321,7 +430,7 @@ def do_placement(REQUEST): if placement_method == "xls": filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" - xls = Excel_feuille_placement( + xls = _excel_feuille_placement( E, desceval, listetud, columns, space, maxlines, building, room, numbering ) return sco_excel.send_excel_file(REQUEST, xls, filename) @@ -399,7 +508,7 @@ def do_placement(REQUEST): return t -def placement_eval_selectetuds(evaluation_id, REQUEST=None): +def placement_eval_selectetuds_old(evaluation_id, REQUEST=None): """Dialogue placement etudiants: choix methode et localisation""" evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) if not evals: @@ -415,298 +524,170 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None): formid = "placementfile" if not REQUEST.form.get("%s-submitted" % formid, False): # not submitted, choix groupe - r = do_placement_selectetuds(REQUEST) + r = do_placement_selectetuds() if r: - H.append(r) - + if isinstance(r, str): + H.append(r) + elif isinstance(r, Response): + H.append(r.get_data().decode("utf-8")) H.append( - """

Explications

- -""" + """

Explications

""" ) H.append(html_sco_header.sco_footer()) return "\n".join(H) -def Excel_feuille_placement( - E, description, listetud, columns, space, maxlines, building, room, numbering -): - """Genere feuille excel pour placement des etudiants. - E: evaluation (dict) - lines: liste de tuples - (etudid, nom, prenom, etat, groupe, val, explanation) - """ - nbcolumns = int(columns) - - wb = Workbook() - - SheetName0 = "Emargement" - ws0 = wb.add_sheet(SheetName0.decode(scu.SCO_ENCODING)) - # ajuste largeurs colonnes (unite inconnue, empirique) - width = 4500 - if nbcolumns > 5: - width = 22500 // nbcolumns - - for col in range(nbcolumns): - ws0.col(col + 1).width = width - ws0.col(0).width = 750 - - SheetName1 = "Positions" - ws1 = wb.add_sheet(SheetName1.decode(scu.SCO_ENCODING)) +def _one_header(ws, numbering, styles): + cells = [] if numbering == "coordinate": - ws1.col(0).width = 4000 - ws1.col(1).width = 4500 - ws1.col(2).width = 1500 - ws1.col(3).width = 1500 - - ws1.col(4).width = 500 - - ws1.col(5).width = 4000 - ws1.col(6).width = 4500 - ws1.col(7).width = 1500 - ws1.col(8).width = 1500 + cells.append(ws.make_cell("Nom", styles["2bi"])) + cells.append(ws.make_cell("Prénom", styles["2bi"])) + cells.append(ws.make_cell("Colonne", styles["2bi"])) + cells.append(ws.make_cell("Ligne", styles["2bi"])) else: - ws1.col(0).width = 4000 - ws1.col(1).width = 4500 - ws1.col(2).width = 3000 + cells.append(ws.make_cell("Nom", styles["2bi"])) + cells.append(ws.make_cell("Prénom", styles["2bi"])) + cells.append(ws.make_cell("Place", styles["2bi"])) + return cells - ws1.col(3).width = 500 - ws1.col(4).width = 4000 - ws1.col(5).width = 4500 - ws1.col(6).width = 3000 +def _headers(ws, numbering, styles, nb_listes): + cells = [] + for _ in range(nb_listes): + cells += _one_header(ws, numbering, styles) + cells.append(ws.make_cell("")) + ws.append_row(cells) + + +def _make_styles(ws0, ws1): + # polices + font0 = Font(name="Calibri", bold=True, size=12) + font1b = copy(font0) + font1b.size = 9 + font1i = Font(name="Arial", italic=True, size=10) + font1o = Font(name="Arial", outline=True, size=10) + font2bi = Font(name="Arial", bold=True, italic=True, size=8) + font2 = Font(name="Arial", size=10) + + # bordures + side_double = Side(border_style="double", color=COLORS.BLACK.value) + side_thin = Side(border_style="thin", color=COLORS.BLACK.value) + + # bordures + border1t = Border(left=side_double, top=side_double, right=side_double) + border1bb = Border(left=side_double, bottom=side_double, right=side_double) + border1bm = Border(left=side_double, right=side_double) + border1m = Border(left=side_double, bottom=side_thin, right=side_double) + border2m = Border(top=side_thin, bottom=side_thin) + border2r = Border(top=side_thin, bottom=side_thin, right=side_thin) + border2l = Border(left=side_thin, top=side_thin, bottom=side_thin) + border2b = Border(left=side_thin, top=side_thin, bottom=side_thin, right=side_thin) + + # alignements + align_center_center = Alignment(horizontal="center", vertical="center") + align_right_bottom = Alignment(horizontal="right", vertical="bottom") + align_left_center = Alignment(horizontal="left", vertical="center") + align_right_center = Alignment(horizontal="right", vertical="center") + + # patterns + pattern = PatternFill( + fill_type="solid", fgColor=sco_excel.COLORS.LIGHT_YELLOW.value + ) # styles - font0 = Font() - font0.name = "Arial" - font0.bold = True - font0.height = 12 * 0x14 + styles = { + "titres": sco_excel.excel_make_style(font_name="Arial", bold=True, size=12), + "1t": ws0.excel_make_composite_style( + font=font0, alignment=align_center_center, border=border1t + ), + "1m": ws0.excel_make_composite_style( + font=font1b, alignment=align_center_center, border=border1m + ), + "1bm": ws0.excel_make_composite_style( + font=font1b, alignment=align_center_center, border=border1bm + ), + "1bb": ws0.excel_make_composite_style( + font=font1o, alignment=align_right_bottom, border=border1bb + ), + "2b": ws1.excel_make_composite_style( + font=font1i, alignment=align_center_center, border=border2b + ), + "2bi": ws1.excel_make_composite_style( + font=font2bi, alignment=align_center_center, border=border2b, fill=pattern + ), + "2l": ws1.excel_make_composite_style( + font=font2, alignment=align_left_center, border=border2l + ), + "2m1": ws1.excel_make_composite_style( + font=font2, alignment=align_left_center, border=border2m + ), + "2m2": ws1.excel_make_composite_style( + font=font2, alignment=align_right_center, border=border2m + ), + "2r": ws1.excel_make_composite_style( + font=font2, alignment=align_right_center, border=border2r + ), + } + return styles - font1b = Font() - font1b.name = "Arial" - font1b.bold = True - font1b.height = 9 * 0x14 - font1i = Font() - font1i.name = "Arial" - font1i.height = 10 * 0x14 - font1i.italic = True +def _init_lines(maxlines): + return [ + [] for _ in range(maxlines) + ] # lines[no_ligne] -> liste des cellules de la ligne (no_lignes de 1..maxlines - font1o = Font() - font1o.name = "Arial" - font1o.height = 10 * 0x14 - font1o.outline = True - font2bi = Font() - font2bi.name = "Arial" - font2bi.height = 8 * 0x14 - font2bi.bold = True - font2bi.italic = True +def _write_lines(ws, lines): + for line in lines: + ws.append_row(line) - font2 = Font() - font2.name = "Arial" - font2.height = 10 * 0x14 - style_titres = XFStyle() - style_titres.font = font0 - - style1t = XFStyle() - style1t.font = font1b - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style1t.alignment = alignment - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.DOUBLE - borders.bottom = Borders.NO_LINE - borders.right = Borders.DOUBLE - style1t.borders = borders - - style1m = XFStyle() - style1m.font = font1b - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style1m.alignment = alignment - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.NO_LINE - borders.bottom = Borders.THIN - borders.right = Borders.DOUBLE - style1m.borders = borders - - style1bm = XFStyle() - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.NO_LINE - borders.bottom = Borders.NO_LINE - borders.right = Borders.DOUBLE - style1bm.borders = borders - - style1bb = XFStyle() - style1bb.font = font1o - alignment = Alignment() - alignment.horz = Alignment.HORZ_RIGHT - alignment.vert = Alignment.VERT_BOTTOM - style1bb.alignment = alignment - borders = Borders() - borders.left = Borders.DOUBLE - borders.top = Borders.NO_LINE - borders.bottom = Borders.DOUBLE - borders.right = Borders.DOUBLE - style1bb.borders = borders - - style2b = XFStyle() - style2b.font = font1i - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style2b.alignment = alignment - borders = Borders() - borders.left = Borders.THIN - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.THIN - style2b.borders = borders - - style2bi = XFStyle() - style2bi.font = font2bi - alignment = Alignment() - alignment.horz = Alignment.HORZ_CENTER - alignment.vert = Alignment.VERT_CENTER - style2bi.alignment = alignment - borders = Borders() - borders.left = Borders.THIN - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.THIN - style2bi.borders = borders - pattern = Pattern() - pattern.pattern = Pattern.SOLID_PATTERN - pattern._pattern_back_colour = "gray" - style2bi.pattern = pattern - - style2l = XFStyle() - style2l.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_LEFT - alignment.vert = Alignment.VERT_CENTER - style2l.alignment = alignment - borders = Borders() - borders.left = Borders.THIN - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.NO_LINE - style2l.borders = borders - - style2m1 = XFStyle() - style2m1.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_LEFT - alignment.vert = Alignment.VERT_CENTER - style2m1.alignment = alignment - borders = Borders() - borders.left = Borders.NO_LINE - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.NO_LINE - style2m1.borders = borders - - style2m2 = XFStyle() - style2l.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_RIGHT - alignment.vert = Alignment.VERT_CENTER - style2m2.alignment = alignment - borders = Borders() - borders.left = Borders.NO_LINE - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.NO_LINE - style2m2.borders = borders - - style2r = XFStyle() - style2l.font = font2 - alignment = Alignment() - alignment.horz = Alignment.HORZ_RIGHT - alignment.vert = Alignment.VERT_CENTER - style2r.alignment = alignment - borders = Borders() - borders.left = Borders.NO_LINE - borders.top = Borders.THIN - borders.bottom = Borders.THIN - borders.right = Borders.THIN - style2r.borders = borders - - # ligne de titres - li = 0 - line = 0 +def _titres(ws, description, evaluation, building, room, styles): dt = time.strftime("%d/%m/%Y a %Hh%M") - ws0.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres) - ws1.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres) - for desceval in description: - if line % 2 == 0: - li += 2 - else: - li += 1 - line += 1 - ws0.write(li, 0, desceval[0].decode(scu.SCO_ENCODING), style_titres) - ws1.write(li, 0, desceval[0].decode(scu.SCO_ENCODING), style_titres) - li += 1 - ws0.write( - li, - 0, - "Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]), - style_titres, + ws.append_single_cell_row( + "Feuille placement etudiants éditée le %s" % dt, styles["titres"] ) - ws1.write( - li, - 0, - "Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]), - style_titres, + for line, desceval in enumerate(description): + if line in [1, 4, 7]: + ws.append_blank_row() + ws.append_single_cell_row(desceval[0], styles["titres"]) + ws.append_single_cell_row( + "Date : %s - Horaire : %s à %s" + % (evaluation["jour"], evaluation["heure_debut"], evaluation["heure_fin"]), + styles["titres"], + ) + ws.append_single_cell_row( + "Date : %s - Horaire : %s à %s" + % (evaluation["jour"], evaluation["heure_debut"], evaluation["heure_fin"]), + styles["titres"], ) - li += 1 + +def _feuille0( + ws0, + description, + evaluation, + styles, + numbering, + listetud, + nbcolumns, + building, + room, + space, +): + _titres(ws0, description, evaluation, building, room, styles) # entetes colonnes - feuille0 + cells = [ws0.make_cell()] for col in range(nbcolumns): - ws0.write(li, col + 1, "colonne %s" % (col + 1), style2b) - # entetes colonnes - feuille1 - if numbering == "coordinate": - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Colonne", style2bi) - ws1.write(li, 3, "Ligne", style2bi) - - ws1.write(li, 5, "Nom", style2bi) - ws1.write(li, 6, "Prénom", style2bi) - ws1.write(li, 7, "Colonne", style2bi) - ws1.write(li, 8, "Ligne", style2bi) - else: - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Place", style2bi) - - ws1.write(li, 4, "Nom", style2bi) - ws1.write(li, 5, "Prénom", style2bi) - ws1.write(li, 6, "Place", style2bi) - + cells.append(ws0.make_cell("colonne %s" % (col + 1), styles["2b"])) + ws0.append_row(cells) # etudiants line = 1 col = 1 @@ -730,78 +711,188 @@ def Excel_feuille_placement( placementetud.append(linetud) # etudiants - feuille0 - line = 0 - li0 = li - for linetud in placementetud: - li0 += 1 - line += 1 - ws0.write(li0, 0, line, style2b) - col = 1 + place = 1 + for rang, linetud in enumerate(placementetud, start=1): + # Chaque rang est affiché sur 3 lignes xlsx (notées A, B, C) + # ligne A: le nom, ligne B: le prénom, ligne C: un espace ou la place + cells_a = [ws0.make_cell(rang, styles["2b"])] + cells_b = [ws0.make_cell("", styles["2b"])] + cells_c = [ws0.make_cell("", styles["2b"])] + row = 14 # premieère ligne de signature for etudid in linetud: - ws0.write(li0, col, (etudid[0]).decode(scu.SCO_ENCODING), style1t) - ws0.write(li0 + 1, col, (etudid[1]).decode(scu.SCO_ENCODING), style1m) - ws0.row(li0 + 2).height = space + cells_a.append(ws0.make_cell(etudid[0], styles["1t"])) + cells_b.append(ws0.make_cell(etudid[1], styles["1m"])) if numbering == "coordinate": - ws0.write(li0 + 2, col, " ", style1bb) + cell_c = ws0.make_cell("", styles["1bb"]) else: - ws0.write( - li0 + 2, col, "place %s" % (col + (line - 1) * nbcolumns), style1bb - ) - # ws0.write(li+3,col, ' ', style1bm ) - # ws0.write(li+4,col, ' ', style1bb ) - + cell_c = ws0.make_cell("place %s" % place, styles["1bb"]) + cells_c.append(cell_c) + ws0.set_row_dimension_height(row, space / 25) + row += 3 + place = place + 1 if col == nbcolumns: - col = 0 - li0 += 2 - col += 1 + ws0.append_row(cells_a) + ws0.append_row(cells_b) + ws0.append_row(cells_c) + cells_a = [ws0.make_cell(rang, styles["2b"])] + cells_b = [ws0.make_cell("", styles["2b"])] + cells_c = [ws0.make_cell("", styles["2b"])] + # publication du rang final incomplet + ws0.append_row(cells_a) + ws0.append_row(cells_b) + ws0.append_row(cells_c) + ws0.set_row_dimension_height(row, space / 25) - # etudiants - feuille1 - if numbering == "coordinate": - coloffset = 5 - else: - coloffset = 4 - line = 0 - li1 = li - nbcol = 0 - col = 0 - orderetud.sort() - for etudid in orderetud: - li1 += 1 - line += 1 - ws1.write(li1, col, (etudid[0]).decode(scu.SCO_ENCODING), style2l) - ws1.write(li1, col + 1, (etudid[1]).decode(scu.SCO_ENCODING), style2m1) + +def _compute_ordretud(listetud, nbcolumns, numbering): + orderetud = [] + line = 1 + col = 1 + for etudid in listetud: if numbering == "coordinate": - ws1.write(li1, col + 2, etudid[2], style2m2) - ws1.write(li1, col + 3, etudid[3], style2r) + orderetud.append((etudid[0], etudid[1], col, line)) else: - ws1.write(li1, col + 2, etudid[2], style2r) + orderetud.append( + (etudid[0], etudid[1], "%s" % (col + (line - 1) * nbcolumns)) + ) + col += 1 + if col > nbcolumns: + col = 1 + line += 1 + orderetud.sort() + return orderetud - if line == maxlines: + +def _next_page(ws): + pass + + +def _feuille1( + ws, + description, + evaluation, + styles, + numbering, + maxlines, + nbcolumns, + building, + room, + listetud, +): + # etudiants - feuille1 + # structuration: + # 1 page = maxlistes listes + # 1 liste = 3 ou 4 colonnes(excel) (selon numbering) et (maximum maxlines) lignes + maxlistes = 2 # nombre de listes par page + # computes excel columns widths + if numbering == "coordinate": + gabarit = [16, 18, 6, 6, 2] + else: + gabarit = [16, 18, 12, 2] + widths = [] + for _ in range(maxlistes): + widths += gabarit + ws.set_column_dimension_width(value=widths) + nb_etu_restant = len(listetud) + _titres(ws, description, evaluation, building, room, styles) + nb_listes = min( + maxlistes, nb_etu_restant // maxlines + 1 + ) # nombre de colonnes dans la page + _headers(ws, numbering, styles, nb_listes) + # construction liste alphabétique + # Affichage + lines = _init_lines(maxlines) + orderetud = _compute_ordretud(listetud, nbcolumns, numbering) + line = 0 + col = 0 + for etudid in orderetud: + # check for skip of list or page + if col > 0: # add a empty cell between lists + lines[line].append(ws.make_cell()) + lines[line].append(ws.make_cell(etudid[0], styles["2l"])) + lines[line].append(ws.make_cell(etudid[1], styles["2m1"])) + if numbering == "coordinate": + lines[line].append(ws.make_cell(etudid[2], styles["2m2"])) + lines[line].append(ws.make_cell(etudid[3], styles["2r"])) + else: + lines[line].append(ws.make_cell(etudid[2], styles["2r"])) + line = line + 1 + if line >= maxlines: # fin de liste + col = col + 1 line = 0 - li1 = li - nbcol = nbcol + 1 - col = col + coloffset - if nbcol == 2: - li = li + maxlines + 2 - li1 = li - nbcol = 0 + if col >= maxlistes: # fin de page + _write_lines(ws, lines) + lines = _init_lines(maxlines) col = 0 - if numbering == "coordinate": - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Colonne", style2bi) - ws1.write(li, 3, "Ligne", style2bi) + ws.append_blank_row() + nb_etu_restant -= maxlistes * maxlines + nb_listes = min( + maxlistes, nb_etu_restant // maxlines + 1 + ) # nombre de colonnes dans la page + _headers(ws, numbering, styles, nb_listes) + _write_lines(ws, lines) - ws1.write(li, 5, "Nom", style2bi) - ws1.write(li, 6, "Prénom", style2bi) - ws1.write(li, 7, "Colonne", style2bi) - ws1.write(li, 8, "Ligne", style2bi) - else: - ws1.write(li, 0, "Nom", style2bi) - ws1.write(li, 1, "Prénom", style2bi) - ws1.write(li, 2, "Place", style2bi) - ws1.write(li, 4, "Nom", style2bi) - ws1.write(li, 5, "Prénom", style2bi) - ws1.write(li, 6, "Place", style2bi) - return wb.savetostr() +def _excel_feuille_placement( + evaluation, + description, + listetud, + columns, + space, + maxlines, + building, + room, + numbering, +): + """Genere feuille excel pour placement des etudiants. + E: evaluation (dict) + lines: liste de tuples + (etudid, nom, prenom, etat, groupe, val, explanation) + """ + nbcolumns = int(columns) + column_width_ratio = 1 / 250 # changement d unités entre pyExcelerator et openpyxl + + wb = ScoExcelBook() + + SheetName0 = "Emargement" + ws0 = wb.create_sheet(SheetName0) + # ajuste largeurs colonnes (unite inconnue, empirique) + width = 4500 * column_width_ratio + if nbcolumns > 5: + width = 22500 * column_width_ratio // nbcolumns + + ws0.set_column_dimension_width("A", 750 * column_width_ratio) + for col in range(nbcolumns): + ws0.set_column_dimension_width( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1 : col + 2], width + ) + + SheetName1 = "Positions" + ws1 = wb.create_sheet(SheetName1) + + styles = _make_styles(ws0, ws1) + _feuille0( + ws0, + description, + evaluation, + styles, + numbering, + listetud, + nbcolumns, + building, + room, + space, + ) + _feuille1( + ws1, + description, + evaluation, + styles, + numbering, + maxlines, + nbcolumns, + building, + room, + listetud, + ) + return wb.generate() diff --git a/app/templates/forms/placement.html b/app/templates/forms/placement.html new file mode 100644 index 000000000..53bfe534b --- /dev/null +++ b/app/templates/forms/placement.html @@ -0,0 +1,56 @@ +{% import 'bootstrap/wtf.html' as wtf %} + +{% macro render_field(field) %} + + {{ field.label }} + {{ field(**kwargs)|safe }} + {% if field.errors %} + + {% endif %} + + +{% endmacro %} + +
+
+ {{ form.evaluation_id }} + + + {{ render_field(form.surveillants) }} + {{ render_field(form.batiment) }} + {{ render_field(form.salle) }} + {{ render_field(form.nb_rangs) }} + {{ render_field(form.etiquetage) }} + {% if form.has_groups %} + {{ render_field(form.groups) }} + + {% endif %} + {{ render_field(form.file_format) }} + +
+

+ + + +

+
+ + diff --git a/app/views/notes.py b/app/views/notes.py index 7e80550b5..41bad1dd0 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1611,6 +1611,7 @@ sco_publish( "/placement_eval_selectetuds", sco_placement.placement_eval_selectetuds, Permission.ScoEnsView, + methods=["GET", "POST"], ) sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView)