From 770ccb4d6e9249c558ab4be26f85d9b444f276b7 Mon Sep 17 00:00:00 2001 From: jean-marie Place Date: Sun, 5 Sep 2021 14:50:35 +0200 Subject: [PATCH 01/22] fin fusion --- app/scodoc/sco_excel.py | 245 +++++---- app/scodoc/sco_placement.py | 783 ++++++++++++++++------------- app/templates/forms/placement.html | 56 +++ app/views/notes.py | 1 + 4 files changed, 612 insertions(+), 473 deletions(-) create mode 100644 app/templates/forms/placement.html diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 32cf27c0a3..5ac78d9fd9 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 4f9e9a0b79..e0cf5b800a 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 0000000000..53bfe534bb --- /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 7e80550b5d..41bad1dd05 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) From f828134ea29cc1c67c853fa8495b03b0eaf14064 Mon Sep 17 00:00:00 2001 From: jean-marie Place Date: Sat, 11 Sep 2021 10:04:52 +0200 Subject: [PATCH 02/22] complements scodoc.css pour formulaire --- app/static/css/scodoc.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 01213d548d..3072f8e6ca 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -1062,13 +1062,11 @@ h2.formsemestre, .gtrcontent h2 { #formnotes td.tf-fieldlabel { border-bottom: 1px dotted #fdcaca; } - -/* Formulaires ScoDoc 9 */ -form.sco-form { - margin-top: 1em; +.wtf-field li { + display: inline; } -div.sco-submit { - margin-top: 2em; +.wtf-field .errors { + color: red ; font-weight: bold; } /* .formsemestre_menubar { From 050e54de3ee7c40e57939c79906a086a563e5b5e Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 11 Sep 2021 18:33:55 +0200 Subject: [PATCH 03/22] placement fait --- app/scodoc/sco_evaluations.py | 2 +- app/scodoc/sco_placement.py | 255 ++++++++++++++++------------- app/templates/forms/placement.html | 22 ++- app/views/notes.py | 1 - 4 files changed, 167 insertions(+), 113 deletions(-) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index b9ba65c678..3697d598a1 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -1046,7 +1046,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): resp = u["prenomnom"] nomcomplet = u["nomcomplet"] can_edit = sco_permissions_check.can_edit_notes( - current_user, moduleimpl_id, allow_ens=False + , moduleimpl_id, allow_ens=False ) link = ( diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 75ec9437e2..9f63a6ecff 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -111,7 +111,7 @@ class PlacementForm(FlaskForm): ) submit = SubmitField("OK") - def _set_evaluation_infos(self, evaluation_id): + 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") @@ -146,7 +146,7 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None): request.form, data={"evaluation_id": int(evaluation_id), "groups": PlacementForm.TOUS}, ) - form._set_evaluation_infos(evaluation_id) + form.set_evaluation_infos(evaluation_id) if form.validate_on_submit(): exec_placement(form) return flask.redirect(titi()) @@ -154,27 +154,6 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None): 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

-
    -
  • préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;
  • -
  • deux types de placements sont possibles : -
      -
    • continue suppose que les tables ont toutes un numéro unique;
    • -
    • coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).
    • -
  • -
  • Choisir le format du fichier résultat : -
      -
    • le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;
    • -
    • le format xls produit un classeur avec deux onglets: -
        -
      • le premier onglet donne une vue de la salle avec la localisation des étudiants et - peut servir de feuille d'émargement;
      • -
      • le second onglet est un tableau similaire à celui du fichier pdf;
      • -
    • -
  • -
""" - ) F = html_sco_header.sco_footer() return "\n".join(H) + "

" + F @@ -323,17 +302,17 @@ def do_placement_selectetuds(): for x in group_ids ] query = ( - "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&" - % ( - evaluation_id, - placement_method, - teachers, - building, - room, - columns, - numbering, - ) - + "&".join(gs) + "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&" + % ( + evaluation_id, + placement_method, + teachers, + building, + room, + columns, + numbering, + ) + + "&".join(gs) ) return flask.redirect(scu.NotesURL() + "/do_placement?" + query) else: @@ -342,45 +321,78 @@ def do_placement_selectetuds(): ) -def do_placement(REQUEST): - """ - Choisi le placement - """ - authuser = REQUEST.AUTHENTICATED_USER - authusername = str(authuser) +def exec_placement(form): try: - evaluation_id = int(REQUEST.form["evaluation_id"]) + evaluation_id = int(form["evaluation_id"].data) except: raise ScoValueError( "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] + eval_data = 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"]): + if not sco_permissions_check.can_edit_notes( + current_user, eval_data["moduleimpl_id"] + ): return ( - "

Génération du placement impossible pour %s

" % authusername - + """

(vérifiez que le semestre n'est pas verrouillé et que vous + "

Génération du placement impossible pour %s

" % authusername + + """

(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)

Continuer

""" - % E["moduleimpl_id"] + % E["moduleimpl_id"] ) + plan = repartition(form, eval_data) + breakpoint() + sem_preferences = sco_preferences.SemPreferences() + space = sem_preferences.get("feuille_placement_emargement") + maxlines = sem_preferences.get("feuille_placement_positions") + + +def repartition(form, eval_data): + """ + Calcule le placement. retourne une liste de couples ((nom, prenom), position) + """ cnx = ndb.GetDBConnexion() # Infos transmises - placement_method = REQUEST.form["placement_method"] - teachers = REQUEST.form["teachers"] - building = REQUEST.form["building"] - room = REQUEST.form["room"] - columns = REQUEST.form["columns"] - numbering = REQUEST.form["numbering"] + evaluation_id = form["evaluation_id"].data + etiquetage = form["etiquetage"].data + teachers = form["surveillants"].data + building = form["batiment"].data + room = form["salle"].data + nb_rangs = form["nb_rangs"].data + group_ids = form["groups"].data # Construit liste des etudiants - group_ids = REQUEST.form.get("group_ids", []) groups = sco_groups.listgroups(group_ids) gr_title_filename = sco_groups.listgroups_filename(groups) # gr_title = sco_groups.listgroups_abbrev(groups) + moduleimpl_data = sco_moduleimpl.do_moduleimpl_list( + moduleimpl_id=eval_data["moduleimpl_id"] + )[0] + Mod = sco_edit_module.do_module_list( + args={"module_id": moduleimpl_data["module_id"]} + )[0] + sem = sco_formsemestre.get_formsemestre(moduleimpl_data["formsemestre_id"]) + evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(eval_data["jour"])) + if eval_data["description"]: + evaltitre = eval_data["description"] + else: + evaltitre = "évaluation du %s" % eval_data["jour"] + + desceval = [ + ["%s" % sem["titreannee"]], + ["Module : %s - %s" % (Mod["code"], Mod["abbrev"])], + ["Surveillants : %s" % teachers], + ["Batiment : %s - Salle : %s" % (building, room)], + ["Controle : %s (coef. %g)" % (evaltitre, eval_data["coefficient"])] + ] # une liste de liste de chaines: description de l'evaluation + listetud = build_listetud(cnx, groups, evaluation_id, moduleimpl_data) + return affectation_places(listetud, etiquetage, nb_rangs) + + +def build_listetud(cnx, groups, evaluation_id, moduleimpl_data): if None in [g["group_name"] for g in groups]: # tous les etudiants getallstudents = True gr_title_filename = "tous" @@ -389,47 +401,70 @@ def do_placement(REQUEST): etudids = sco_groups.do_evaluation_listeetuds_groups( evaluation_id, groups, getallstudents=getallstudents, include_dems=True ) - if not etudids: - return "

Aucun groupe sélectionné !

" - - M = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0] - Mod = sco_edit_module.do_module_list(args={"module_id": M["module_id"]})[0] - sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"]) - evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(E["jour"])) - if E["description"]: - evaltitre = E["description"] - else: - evaltitre = "évaluation du %s" % E["jour"] - - desceval = [] # une liste de liste de chaines: description de l'evaluation - desceval.append(["%s" % sem["titreannee"]]) - desceval.append(["Module : %s - %s" % (Mod["code"], Mod["abbrev"])]) - desceval.append(["Surveillants : %s" % teachers]) - desceval.append(["Batiment : %s - Salle : %s" % (building, room)]) - desceval.append(["Controle : %s (coef. %g)" % (evaltitre, E["coefficient"])]) - listetud = [] # liste de couples (nom,prenom) for etudid in etudids: # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) ident = sco_etud.etudident_list(cnx, {"etudid": etudid})[0] # infos inscription inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - {"etudid": etudid, "formsemestre_id": M["formsemestre_id"]} + {"etudid": etudid, "formsemestre_id": moduleimpl_data["formsemestre_id"]} )[0] if inscr["etat"] != "D": nom = ident["nom"].upper() prenom = ident["prenom"].lower().capitalize() listetud.append((nom, prenom)) random.shuffle(listetud) + return listetud - sem_preferences = sco_preferences.SemPreferences() - space = sem_preferences.get("feuille_placement_emargement") - maxlines = sem_preferences.get("feuille_placement_positions") - if placement_method == "xls": +class DistributeurContinu: + """Distribue les places selon un ordre numérique.""" + + def __init(self): + self.position = 1 + + def suivant(self): + retour = self.position + self.position += 1 + return retour + + +class Distributeur2D: + """Distribue les places selon des coordonnées sur nb_rangs.""" + + def __init__(self, nb_rangs): + self.nb_rangs = nb_rangs + self.rang = 1 + self.index = 1 + + def suivant(self): + retour = (self.index, self.rang) + self.rang += 1 + if self.rang > self.nb_rangs: + self.rang = 1 + self.index += 1 + return retour + + +def affectation_places(listetud, etiquetage, nb_rangs=1): + affectation = [] + if etiquetage == "continu": + distributeur = DistributeurContinu() + else: + distributeur = Distributeur2D(nb_rangs) + for etud in listetud: + affectation.append((etud, distributeur.suivant())) + return affectation + + +def production_xls(file_format, eval_dat, plan): + + +def production(file_format, eval_dat, plan): + if file_format == "xls": filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" xls = _excel_feuille_placement( - E, desceval, listetud, columns, space, maxlines, building, room, numbering + eval_data, desceval, listetud, columns, space, maxlines, building, room, numbering ) return sco_excel.send_excel_file(REQUEST, xls, filename) else: @@ -495,8 +530,8 @@ def do_placement(REQUEST): rows=rows, filename=filename, origin="Généré par %s le " % sco_version.SCONAME - + scu.timedate_human_repr() - + "", + + scu.timedate_human_repr() + + "", pdf_title=pdf_title, # pdf_shorttitle = '', preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), @@ -669,16 +704,16 @@ def _titres(ws, description, evaluation, building, room, styles): def _feuille0( - ws0, - description, - evaluation, - styles, - numbering, - listetud, - nbcolumns, - building, - room, - space, + ws0, + description, + evaluation, + styles, + numbering, + listetud, + nbcolumns, + building, + room, + space, ): _titres(ws0, description, evaluation, building, room, styles) # entetes colonnes - feuille0 @@ -766,16 +801,16 @@ def _next_page(ws): def _feuille1( - ws, - description, - evaluation, - styles, - numbering, - maxlines, - nbcolumns, - building, - room, - listetud, + ws, + description, + evaluation, + styles, + numbering, + maxlines, + nbcolumns, + building, + room, + listetud, ): # etudiants - feuille1 # structuration: @@ -832,15 +867,15 @@ def _feuille1( def _excel_feuille_placement( - evaluation, - description, - listetud, - columns, - space, - maxlines, - building, - room, - numbering, + evaluation, + description, + listetud, + columns, + space, + maxlines, + building, + room, + numbering, ): """Genere feuille excel pour placement des etudiants. E: evaluation (dict) @@ -862,7 +897,7 @@ def _excel_feuille_placement( 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 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1: col + 2], width ) SheetName1 = "Positions" diff --git a/app/templates/forms/placement.html b/app/templates/forms/placement.html index 53bfe534bb..1c5ca544e3 100644 --- a/app/templates/forms/placement.html +++ b/app/templates/forms/placement.html @@ -3,7 +3,7 @@ {% macro render_field(field) %} {{ field.label }} - {{ field(**kwargs)|safe }} + {{ field()|safe }} {% if field.errors %}
    {% for error in field.errors %} @@ -18,6 +18,7 @@
    {{ form.evaluation_id }} + {{ form.csrf_token }} {{ render_field(form.surveillants) }} @@ -51,6 +52,25 @@ +

    Explications

    +
      +
    • préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;
    • +
    • deux types de placements sont possibles : +
        +
      • continue suppose que les tables ont toutes un numéro unique;
      • +
      • coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).
      • +
    • +
    • Choisir le format du fichier résultat : +
        +
      • le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;
      • +
      • le format xls produit un classeur avec deux onglets: +
          +
        • le premier onglet donne une vue de la salle avec la localisation des étudiants et + peut servir de feuille d'émargement;
        • +
        • le second onglet est un tableau similaire à celui du fichier pdf;
        • +
      • +
    • +
    diff --git a/app/views/notes.py b/app/views/notes.py index a01f657c7c..f6d4e073b4 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1643,7 +1643,6 @@ sco_publish( Permission.ScoEnsView, methods=["GET", "POST"], ) -sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView) # --- Saisie des notes sco_publish( From 35768e9241c8bc9fa349d51392cae3d08d37ed3c Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 11 Sep 2021 19:35:30 +0200 Subject: [PATCH 04/22] placement fait --- app/scodoc/sco_placement.py | 658 ++++++++++++++++++------------------ 1 file changed, 324 insertions(+), 334 deletions(-) diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 9f63a6ecff..7e5b68b737 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -141,14 +141,15 @@ class PlacementForm(FlaskForm): self.groups.choices = choices -def placement_eval_selectetuds(evaluation_id, REQUEST=None): +def placement_eval_selectetuds(evaluation_id): + """Creation de l'écran de placement""" 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) + exec_placement(form) # calcul et generation du fichier return flask.redirect(titi()) H = [html_sco_header.sco_header(init_jquery_ui=True)] H.append(sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)) @@ -158,238 +159,227 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None): return "\n".join(H) + "

    " + F -def do_placement_selectetuds(): - """ - Choisi les étudiants et les infos sur la salle pour leur placement. - """ - # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0] - # description de l'evaluation - H = [ - sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), - "

    Placement et émargement des étudiants

    ", - ] - # - descr = [ - ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), - ( - "placement_method", - { - "input_type": "radio", - "default": "xls", - "allow_null": False, - "allowed_values": ["pdf", "xls"], - "labels": ["fichier pdf", "fichier xls"], - "title": "Format de fichier :", - }, - ), - ("teachers", {"size": 25, "title": "Surveillants :"}), - ("building", {"size": 25, "title": "Batiment :"}), - ("room", {"size": 10, "title": "Salle :"}), - ( - "columns", - { - "input_type": "radio", - "default": "5", - "allow_null": False, - "allowed_values": ["3", "4", "5", "6", "7", "8"], - "labels": [ - "3 colonnes", - "4 colonnes", - "5 colonnes", - "6 colonnes", - "7 colonnes", - "8 colonnes", - ], - "title": "Nombre de colonnes :", - }, - ), - ( - "numbering", - { - "input_type": "radio", - "default": "coordinate", - "allow_null": False, - "allowed_values": ["continuous", "coordinate"], - "labels": ["continue", "coordonnées"], - "title": "Numérotation :", - }, - ), - ] - if no_groups: - submitbuttonattributes = [] - descr += [ - ( - "group_ids", - { - "default": [ - g["group_id"] # pylint: disable=invalid-sequence-index - for g in groups - ], - "input_type": "hidden", - "type": "list", - }, - ) - ] - else: - descr += [ - ( - "group_ids", - { - "input_type": "checkbox", - "title": "Choix groupe(s) d'étudiants :", - "allowed_values": grnams, - "labels": grlabs, - "attributes": ['onchange="gr_change(this);"'], - }, - ) - ] - - if not ("group_ids" in REQUEST.form and REQUEST.form["group_ids"]): - submitbuttonattributes = ['disabled="1"'] - else: - submitbuttonattributes = [] # groupe(s) preselectionnés - H.append( - # JS pour desactiver le bouton OK si aucun groupe selectionné - """ - """ - ) - - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - descr, - cancelbutton="Annuler", - submitbuttonattributes=submitbuttonattributes, - submitlabel="OK", - formid="gr", - ) - if tf[0] == 0: - # H.append( """
    - # Choix du groupe et de la localisation - # """) - H.append("""
    """) - return "\n".join(H) + "\n" + tf[1] + "\n
    " - elif tf[0] == -1: - return flask.redirect( - "%s/Notes/moduleimpl_status?moduleimpl_id=%s" - % (scu.ScoURL(), E["moduleimpl_id"]) - ) - else: - placement_method = tf[2]["placement_method"] - teachers = tf[2]["teachers"] - building = tf[2]["building"] - room = tf[2]["room"] - group_ids = tf[2]["group_ids"] - columns = tf[2]["columns"] - numbering = tf[2]["numbering"] - if columns in ("3", "4", "5", "6", "7", "8"): - gs = [ - ("group_ids%3Alist=" + six.moves.urllib.parse.quote_plus(x)) - for x in group_ids - ] - query = ( - "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&" - % ( - evaluation_id, - placement_method, - teachers, - building, - room, - columns, - numbering, - ) - + "&".join(gs) - ) - return flask.redirect(scu.NotesURL() + "/do_placement?" + query) - else: - raise ValueError( - "invalid placement_method (%s)" % tf[2]["placement_method"] - ) +# def do_placement_selectetuds(): +# """ +# Choisi les étudiants et les infos sur la salle pour leur placement. +# """ +# # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0] +# # description de l'evaluation +# H = [ +# sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), +# "

    Placement et émargement des étudiants

    ", +# ] +# # +# descr = [ +# ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), +# ( +# "placement_method", +# { +# "input_type": "radio", +# "default": "xls", +# "allow_null": False, +# "allowed_values": ["pdf", "xls"], +# "labels": ["fichier pdf", "fichier xls"], +# "title": "Format de fichier :", +# }, +# ), +# ("teachers", {"size": 25, "title": "Surveillants :"}), +# ("building", {"size": 25, "title": "Batiment :"}), +# ("room", {"size": 10, "title": "Salle :"}), +# ( +# "columns", +# { +# "input_type": "radio", +# "default": "5", +# "allow_null": False, +# "allowed_values": ["3", "4", "5", "6", "7", "8"], +# "labels": [ +# "3 colonnes", +# "4 colonnes", +# "5 colonnes", +# "6 colonnes", +# "7 colonnes", +# "8 colonnes", +# ], +# "title": "Nombre de colonnes :", +# }, +# ), +# ( +# "numbering", +# { +# "input_type": "radio", +# "default": "coordinate", +# "allow_null": False, +# "allowed_values": ["continuous", "coordinate"], +# "labels": ["continue", "coordonnées"], +# "title": "Numérotation :", +# }, +# ), +# ] +# if no_groups: +# submitbuttonattributes = [] +# descr += [ +# ( +# "group_ids", +# { +# "default": [ +# g["group_id"] # pylint: disable=invalid-sequence-index +# for g in groups +# ], +# "input_type": "hidden", +# "type": "list", +# }, +# ) +# ] +# else: +# descr += [ +# ( +# "group_ids", +# { +# "input_type": "checkbox", +# "title": "Choix groupe(s) d'étudiants :", +# "allowed_values": grnams, +# "labels": grlabs, +# "attributes": ['onchange="gr_change(this);"'], +# }, +# ) +# ] +# +# if not ("group_ids" in REQUEST.form and REQUEST.form["group_ids"]): +# submitbuttonattributes = ['disabled="1"'] +# else: +# submitbuttonattributes = [] # groupe(s) preselectionnés +# H.append( +# # JS pour desactiver le bouton OK si aucun groupe selectionné +# """ +# """ +# ) +# +# tf = TrivialFormulator( +# REQUEST.URL0, +# REQUEST.form, +# descr, +# cancelbutton="Annuler", +# submitbuttonattributes=submitbuttonattributes, +# submitlabel="OK", +# formid="gr", +# ) +# if tf[0] == 0: +# # H.append( """
    +# # Choix du groupe et de la localisation +# # """) +# H.append("""
    """) +# return "\n".join(H) + "\n" + tf[1] + "\n
    " +# elif tf[0] == -1: +# return flask.redirect( +# "%s/Notes/moduleimpl_status?moduleimpl_id=%s" +# % (scu.ScoURL(), E["moduleimpl_id"]) +# ) +# else: +# placement_method = tf[2]["placement_method"] +# teachers = tf[2]["teachers"] +# building = tf[2]["building"] +# room = tf[2]["room"] +# group_ids = tf[2]["group_ids"] +# columns = tf[2]["columns"] +# numbering = tf[2]["numbering"] +# if columns in ("3", "4", "5", "6", "7", "8"): +# gs = [ +# ("group_ids%3Alist=" + six.moves.urllib.parse.quote_plus(x)) +# for x in group_ids +# ] +# query = ( +# "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&" +# % ( +# evaluation_id, +# placement_method, +# teachers, +# building, +# room, +# columns, +# numbering, +# ) +# + "&".join(gs) +# ) +# return flask.redirect(scu.NotesURL() + "/do_placement?" + query) +# else: +# raise ValueError( +# "invalid placement_method (%s)" % tf[2]["placement_method"] +# ) def exec_placement(form): - try: - evaluation_id = int(form["evaluation_id"].data) - except: - raise ScoValueError( - "Formulaire incomplet ! Vous avez sans doute attendu trop longtemps, veuillez vous reconnecter. Si le problème persiste, contacter l'administrateur. Merci." - ) - eval_data = 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( - current_user, eval_data["moduleimpl_id"] - ): - return ( - "

    Génération du placement impossible pour %s

    " % authusername - + """

    (vérifiez que le semestre n'est pas verrouillé et que vous - avez l'autorisation d'effectuer cette opération)

    -

    Continuer

    - """ - % E["moduleimpl_id"] - ) - plan = repartition(form, eval_data) + """Calcul et génération du fichier sur la base des données du formulaire""" breakpoint() - sem_preferences = sco_preferences.SemPreferences() - space = sem_preferences.get("feuille_placement_emargement") - maxlines = sem_preferences.get("feuille_placement_positions") + d = { + "evaluation_id": form["evaluation_id"].data, + "etiquetage": form["etiquetage"].data, + "surveillants": form["surveillants"].data, + "batiment": form["batiment"].data, + "salle": form["salle"].data, + "nb_rangs": form["nb_rangs"].data, + "groups": form["groups"].data, + } + d["eval_data"] = sco_evaluations.do_evaluation_list(d)[0] + # Check access (admin, respformation, and responsable_id) + d["current_user"] = current_user + d["moduleimpl_id"] = d["eval_data"]["moduleimpl_id"] + if not sco_permissions_check.can_edit_notes(d["current_user"], d["moduleimpl_id"]): + return ( + """

    Génération du placement impossible pour %(current_user)s

    +

    (vérifiez que le semestre n'est pas verrouillé et que vous +avez l'autorisation d'effectuer cette opération)

    +

    Continuer

    +""" + % d + ) + d["cnx"] = ndb.GetDBConnexion() + d["plan"] = repartition(d) + d["gr_title_filename"] = sco_groups.listgroups_filename(d['groups']) + # gr_title = sco_groups.listgroups_abbrev(d['groups']) + d["moduleimpl_data"] = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=d["moduleimpl_id"]) + d["Mod"] = sco_edit_module.do_module_list(args={"module_id": d["moduleimpl_id"]})[0] + d["sem"] = sco_formsemestre.get_formsemestre(d["moduleimpl_data"]["formsemestre_id"]) + d["evalname"] = "%s-%s" % (d["Mod"]["code"], ndb.DateDMYtoISO(eval_data["jour"])) + if d["eval_data"]["description"]: + d["evaltitre"] = d["eval_data"]["description"] + else: + d["evaltitre"] = "évaluation du %s" % eval_data["jour"] + d["desceval"] = [ + ["%s" % d["sem"]["titreannee"]], + ["Module : %s - %s" % (d["Mod"]["code"], d["Mod"]["abbrev"])], + ["Surveillants : %(surveillant)s" % d], + ["Batiment : %(batiment)s - Salle : %(salle)s" % d], + ["Controle : %s (coef. %g)" % (d["evaltitre"], d["eval_data"]["coefficient"])], + ] # une liste de liste de chaines: description de l'evaluation + if form["file_format"].data == "xls": + production_xls(d) + else: + production_pdf(d) -def repartition(form, eval_data): +def repartition(d): """ Calcule le placement. retourne une liste de couples ((nom, prenom), position) """ - cnx = ndb.GetDBConnexion() - # Infos transmises - evaluation_id = form["evaluation_id"].data - etiquetage = form["etiquetage"].data - teachers = form["surveillants"].data - building = form["batiment"].data - room = form["salle"].data - nb_rangs = form["nb_rangs"].data - group_ids = form["groups"].data - # Construit liste des etudiants - groups = sco_groups.listgroups(group_ids) - gr_title_filename = sco_groups.listgroups_filename(groups) - # gr_title = sco_groups.listgroups_abbrev(groups) - - moduleimpl_data = sco_moduleimpl.do_moduleimpl_list( - moduleimpl_id=eval_data["moduleimpl_id"] - )[0] - Mod = sco_edit_module.do_module_list( - args={"module_id": moduleimpl_data["module_id"]} - )[0] - sem = sco_formsemestre.get_formsemestre(moduleimpl_data["formsemestre_id"]) - evalname = "%s-%s" % (Mod["code"], ndb.DateDMYtoISO(eval_data["jour"])) - if eval_data["description"]: - evaltitre = eval_data["description"] - else: - evaltitre = "évaluation du %s" % eval_data["jour"] - - desceval = [ - ["%s" % sem["titreannee"]], - ["Module : %s - %s" % (Mod["code"], Mod["abbrev"])], - ["Surveillants : %s" % teachers], - ["Batiment : %s - Salle : %s" % (building, room)], - ["Controle : %s (coef. %g)" % (evaltitre, eval_data["coefficient"])] - ] # une liste de liste de chaines: description de l'evaluation - listetud = build_listetud(cnx, groups, evaluation_id, moduleimpl_data) - return affectation_places(listetud, etiquetage, nb_rangs) + groups = sco_groups.listgroups(d["groups"]) + d["listetud"] = build_listetud(d) + return affectation_places(d) def build_listetud(cnx, groups, evaluation_id, moduleimpl_data): @@ -446,99 +436,96 @@ class Distributeur2D: return retour -def affectation_places(listetud, etiquetage, nb_rangs=1): - affectation = [] - if etiquetage == "continu": +def affectation_places(d): + plan = [] + if d["etiquetage"] == "continu": distributeur = DistributeurContinu() else: - distributeur = Distributeur2D(nb_rangs) - for etud in listetud: - affectation.append((etud, distributeur.suivant())) - return affectation + distributeur = Distributeur2D(d["nb_rangs"]) + for etud in d["listetud"]: + plan.append((etud, distributeur.suivant())) + return plan -def production_xls(file_format, eval_dat, plan): +def production_xls(d): + filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" + xls = _excel_feuille_placement( + d["eval_data"], + d["desceval"], + d["listetud"], + d["nb_rangs"], + d["batiment"], + d["salle"], + d["etiquetage"], + ) + return sco_excel.send_excel_file(REQUEST, xls, filename) -def production(file_format, eval_dat, plan): - if file_format == "xls": - filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" - xls = _excel_feuille_placement( - eval_data, desceval, listetud, columns, space, maxlines, building, room, numbering - ) - return sco_excel.send_excel_file(REQUEST, xls, filename) +def production_pdf(d): + pdf_title = d["desceval"] + pdf_title += ( + "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" % d["eval_data"] + ) + + filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % d + titles = { + "nom": "Nom", + "prenom": "Prenom", + "colonne": "Colonne", + "ligne": "Ligne", + "place": "Place", + } + nbcolumns = int(columns) + if numbering == "coordinate": + columns_ids = ["nom", "prenom", "colonne", "ligne"] else: - nbcolumns = int(columns) + columns_ids = ["nom", "prenom", "place"] - pdf_title = "%s
    " % sem["titreannee"] - pdf_title += "Module : %s - %s
    " % (Mod["code"], Mod["abbrev"]) - pdf_title += "Surveillants : %s
    " % teachers - pdf_title += "Batiment : %s - Salle : %s
    " % (building, room) - pdf_title += "Controle : %s (coef. %g)
    " % (evaltitre, E["coefficient"]) - pdf_title += "Date : %s - Horaire : %s à %s" % ( - E["jour"], - E["heure_debut"], - E["heure_fin"], - ) - - filename = "placement_%s_%s.pdf" % (evalname, gr_title_filename) - titles = { - "nom": "Nom", - "prenom": "Prenom", - "colonne": "Colonne", - "ligne": "Ligne", - "place": "Place", - } + # etudiants + line = 1 + col = 1 + orderetud = [] + for etudid in listetud: if numbering == "coordinate": - columns_ids = ["nom", "prenom", "colonne", "ligne"] + orderetud.append((etudid[0], etudid[1], col, line)) else: - columns_ids = ["nom", "prenom", "place"] + orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns)) - # etudiants - line = 1 - col = 1 - orderetud = [] - for etudid in listetud: - if numbering == "coordinate": - orderetud.append((etudid[0], etudid[1], col, line)) - else: - orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns)) + if col == nbcolumns: + col = 0 + line += 1 + col += 1 - if col == nbcolumns: - col = 0 - line += 1 - col += 1 + rows = [] + orderetud.sort() + for etudid in orderetud: + if numbering == "coordinate": + rows.append( + { + "nom": etudid[0], + "prenom": etudid[1], + "colonne": etudid[2], + "ligne": etudid[3], + } + ) + else: + rows.append({"nom": etudid[0], "prenom": etudid[1], "place": etudid[2]}) - rows = [] - orderetud.sort() - for etudid in orderetud: - if numbering == "coordinate": - rows.append( - { - "nom": etudid[0], - "prenom": etudid[1], - "colonne": etudid[2], - "ligne": etudid[3], - } - ) - else: - rows.append({"nom": etudid[0], "prenom": etudid[1], "place": etudid[2]}) - - tab = GenTable( - titles=titles, - columns_ids=columns_ids, - rows=rows, - filename=filename, - origin="Généré par %s le " % sco_version.SCONAME - + scu.timedate_human_repr() - + "", - pdf_title=pdf_title, - # pdf_shorttitle = '', - preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), - # html_generate_cells=False # la derniere ligne (moyennes) est incomplete - ) - t = tab.make_page(format="pdf", with_html_headers=False, REQUEST=REQUEST) - return t + tab = GenTable( + titles=titles, + columns_ids=columns_ids, + rows=rows, + filename=filename, + origin="Généré par %s le " % sco_version.SCONAME + + scu.timedate_human_repr() + + "", + pdf_title=pdf_title, + # pdf_shorttitle = '', + preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), + # html_generate_cells=False # la derniere ligne (moyennes) est incomplete + ) + t = tab.make_page(format="pdf", with_html_headers=False, REQUEST=REQUEST) + return t def placement_eval_selectetuds_old(evaluation_id, REQUEST=None): @@ -704,16 +691,16 @@ def _titres(ws, description, evaluation, building, room, styles): def _feuille0( - ws0, - description, - evaluation, - styles, - numbering, - listetud, - nbcolumns, - building, - room, - space, + ws0, + description, + evaluation, + styles, + numbering, + listetud, + nbcolumns, + building, + room, + space, ): _titres(ws0, description, evaluation, building, room, styles) # entetes colonnes - feuille0 @@ -801,16 +788,16 @@ def _next_page(ws): def _feuille1( - ws, - description, - evaluation, - styles, - numbering, - maxlines, - nbcolumns, - building, - room, - listetud, + ws, + description, + evaluation, + styles, + numbering, + maxlines, + nbcolumns, + building, + room, + listetud, ): # etudiants - feuille1 # structuration: @@ -867,21 +854,24 @@ def _feuille1( def _excel_feuille_placement( - evaluation, - description, - listetud, - columns, - space, - maxlines, - building, - room, - numbering, + 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) """ + sem_preferences = sco_preferences.SemPreferences() + space = sem_preferences.get("feuille_placement_emargement") + maxlines = sem_preferences.get("feuille_placement_positions") nbcolumns = int(columns) column_width_ratio = 1 / 250 # changement d unités entre pyExcelerator et openpyxl @@ -897,7 +887,7 @@ def _excel_feuille_placement( 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 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1 : col + 2], width ) SheetName1 = "Positions" From ed07e422221a49d470102551cb9dd94063f23855 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 11 Sep 2021 19:37:32 +0200 Subject: [PATCH 05/22] placement fait --- app/scodoc/sco_placement.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 7e5b68b737..24795ab18e 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -324,7 +324,6 @@ def placement_eval_selectetuds(evaluation_id): def exec_placement(form): """Calcul et génération du fichier sur la base des données du formulaire""" - breakpoint() d = { "evaluation_id": form["evaluation_id"].data, "etiquetage": form["etiquetage"].data, @@ -334,6 +333,7 @@ def exec_placement(form): "nb_rangs": form["nb_rangs"].data, "groups": form["groups"].data, } + breakpoint() d["eval_data"] = sco_evaluations.do_evaluation_list(d)[0] # Check access (admin, respformation, and responsable_id) d["current_user"] = current_user From 7f63ab222b596250e9d586d5929ac86cb5af023c Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 12 Sep 2021 07:04:05 +0200 Subject: [PATCH 06/22] before refactoring --- app/scodoc/sco_excel.py | 62 +++-- app/scodoc/sco_placement.py | 508 ++++++++++++------------------------ 2 files changed, 197 insertions(+), 373 deletions(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index adeeebba6a..aae6a01d6a 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -35,6 +35,7 @@ from enum import Enum from tempfile import NamedTemporaryFile import openpyxl.utils.datetime +from flask import make_response from openpyxl import Workbook, load_workbook from openpyxl.cell import WriteOnlyCell from openpyxl.styles import Font, Border, Side, Alignment, PatternFill @@ -64,14 +65,21 @@ class COLORS(Enum): LIGHT_YELLOW = "FFFFFF99" +def send_from_flask(data, filename, mime=scu.XLSX_MIMETYPE): + scu.make_filename(filename) + response = make_response(data) + response.headers['Content-Type'] = mime + response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename + + def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE): """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(" ", "_") + .replace("&", "") + .replace(" ", "_") ) request.RESPONSE.setHeader("content-type", mime) request.RESPONSE.setHeader( @@ -136,16 +144,16 @@ class ScoExcelBook: def excel_make_style( - bold=False, - italic=False, - outline=False, - color: COLORS = COLORS.BLACK, - bgcolor: COLORS = None, - halign=None, - valign=None, - number_format=None, - font_name="Arial", - size=10, + bold=False, + italic=False, + outline=False, + color: COLORS = COLORS.BLACK, + bgcolor: COLORS = None, + halign=None, + valign=None, + number_format=None, + font_name="Arial", + size=10, ): """Contruit un style. Les couleurs peuvent être spécfiées soit par une valeur de COLORS, @@ -228,12 +236,12 @@ class ScoExcelSheet: self.row_dimensions = {} def excel_make_composite_style( - self, - alignment=None, - border=None, - fill=None, - number_format=None, - font=None, + self, + alignment=None, + border=None, + fill=None, + number_format=None, + font=None, ): style = {} if font is not None: @@ -374,7 +382,7 @@ class ScoExcelSheet: def excel_simple_table( - titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None + titles=None, lines=None, sheet_name=b"feuille", titles_styles=None, comments=None ): """Export simple type 'CSV': 1ere ligne en gras, le reste tel quel""" ws = ScoExcelSheet(sheet_name) @@ -643,13 +651,13 @@ def _excel_to_list(filelike): # we may need 'encoding' argument ? def excel_feuille_listeappel( - sem, - groupname, - lines, - partitions=None, - with_codes=False, - with_paiement=False, - server_name=None, + sem, + groupname, + lines, + partitions=None, + with_codes=False, + with_paiement=False, + server_name=None, ): """generation feuille appel""" if partitions is None: @@ -751,7 +759,7 @@ def excel_feuille_listeappel( for t in lines: n += 1 nomprenom = ( - t["civilite_str"] + " " + t["nom"] + " " + t["prenom"].lower().capitalize() + t["civilite_str"] + " " + t["nom"] + " " + t["prenom"].lower().capitalize() ) style_nom = style2t3 if with_paiement: diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 24795ab18e..665bf64f41 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -35,9 +35,8 @@ import random import time from copy import copy -import flask import wtforms.validators -from flask import request, render_template +from flask import request, render_template, url_for from flask_login import current_user from werkzeug import Response from flask_wtf import FlaskForm @@ -141,273 +140,7 @@ class PlacementForm(FlaskForm): self.groups.choices = choices -def placement_eval_selectetuds(evaluation_id): - """Creation de l'écran de placement""" - 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) # calcul et generation du fichier - 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)) - 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. -# """ -# # M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0] -# # description de l'evaluation -# H = [ -# sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), -# "

    Placement et émargement des étudiants

    ", -# ] -# # -# descr = [ -# ("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}), -# ( -# "placement_method", -# { -# "input_type": "radio", -# "default": "xls", -# "allow_null": False, -# "allowed_values": ["pdf", "xls"], -# "labels": ["fichier pdf", "fichier xls"], -# "title": "Format de fichier :", -# }, -# ), -# ("teachers", {"size": 25, "title": "Surveillants :"}), -# ("building", {"size": 25, "title": "Batiment :"}), -# ("room", {"size": 10, "title": "Salle :"}), -# ( -# "columns", -# { -# "input_type": "radio", -# "default": "5", -# "allow_null": False, -# "allowed_values": ["3", "4", "5", "6", "7", "8"], -# "labels": [ -# "3 colonnes", -# "4 colonnes", -# "5 colonnes", -# "6 colonnes", -# "7 colonnes", -# "8 colonnes", -# ], -# "title": "Nombre de colonnes :", -# }, -# ), -# ( -# "numbering", -# { -# "input_type": "radio", -# "default": "coordinate", -# "allow_null": False, -# "allowed_values": ["continuous", "coordinate"], -# "labels": ["continue", "coordonnées"], -# "title": "Numérotation :", -# }, -# ), -# ] -# if no_groups: -# submitbuttonattributes = [] -# descr += [ -# ( -# "group_ids", -# { -# "default": [ -# g["group_id"] # pylint: disable=invalid-sequence-index -# for g in groups -# ], -# "input_type": "hidden", -# "type": "list", -# }, -# ) -# ] -# else: -# descr += [ -# ( -# "group_ids", -# { -# "input_type": "checkbox", -# "title": "Choix groupe(s) d'étudiants :", -# "allowed_values": grnams, -# "labels": grlabs, -# "attributes": ['onchange="gr_change(this);"'], -# }, -# ) -# ] -# -# if not ("group_ids" in REQUEST.form and REQUEST.form["group_ids"]): -# submitbuttonattributes = ['disabled="1"'] -# else: -# submitbuttonattributes = [] # groupe(s) preselectionnés -# H.append( -# # JS pour desactiver le bouton OK si aucun groupe selectionné -# """ -# """ -# ) -# -# tf = TrivialFormulator( -# REQUEST.URL0, -# REQUEST.form, -# descr, -# cancelbutton="Annuler", -# submitbuttonattributes=submitbuttonattributes, -# submitlabel="OK", -# formid="gr", -# ) -# if tf[0] == 0: -# # H.append( """
    -# # Choix du groupe et de la localisation -# # """) -# H.append("""
    """) -# return "\n".join(H) + "\n" + tf[1] + "\n
    " -# elif tf[0] == -1: -# return flask.redirect( -# "%s/Notes/moduleimpl_status?moduleimpl_id=%s" -# % (scu.ScoURL(), E["moduleimpl_id"]) -# ) -# else: -# placement_method = tf[2]["placement_method"] -# teachers = tf[2]["teachers"] -# building = tf[2]["building"] -# room = tf[2]["room"] -# group_ids = tf[2]["group_ids"] -# columns = tf[2]["columns"] -# numbering = tf[2]["numbering"] -# if columns in ("3", "4", "5", "6", "7", "8"): -# gs = [ -# ("group_ids%3Alist=" + six.moves.urllib.parse.quote_plus(x)) -# for x in group_ids -# ] -# query = ( -# "evaluation_id=%s&placement_method=%s&teachers=%s&building=%s&room=%s&columns=%s&numbering=%s&" -# % ( -# evaluation_id, -# placement_method, -# teachers, -# building, -# room, -# columns, -# numbering, -# ) -# + "&".join(gs) -# ) -# return flask.redirect(scu.NotesURL() + "/do_placement?" + query) -# else: -# raise ValueError( -# "invalid placement_method (%s)" % tf[2]["placement_method"] -# ) - - -def exec_placement(form): - """Calcul et génération du fichier sur la base des données du formulaire""" - d = { - "evaluation_id": form["evaluation_id"].data, - "etiquetage": form["etiquetage"].data, - "surveillants": form["surveillants"].data, - "batiment": form["batiment"].data, - "salle": form["salle"].data, - "nb_rangs": form["nb_rangs"].data, - "groups": form["groups"].data, - } - breakpoint() - d["eval_data"] = sco_evaluations.do_evaluation_list(d)[0] - # Check access (admin, respformation, and responsable_id) - d["current_user"] = current_user - d["moduleimpl_id"] = d["eval_data"]["moduleimpl_id"] - if not sco_permissions_check.can_edit_notes(d["current_user"], d["moduleimpl_id"]): - return ( - """

    Génération du placement impossible pour %(current_user)s

    -

    (vérifiez que le semestre n'est pas verrouillé et que vous -avez l'autorisation d'effectuer cette opération)

    -

    Continuer

    -""" - % d - ) - d["cnx"] = ndb.GetDBConnexion() - d["plan"] = repartition(d) - d["gr_title_filename"] = sco_groups.listgroups_filename(d['groups']) - # gr_title = sco_groups.listgroups_abbrev(d['groups']) - d["moduleimpl_data"] = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=d["moduleimpl_id"]) - d["Mod"] = sco_edit_module.do_module_list(args={"module_id": d["moduleimpl_id"]})[0] - d["sem"] = sco_formsemestre.get_formsemestre(d["moduleimpl_data"]["formsemestre_id"]) - d["evalname"] = "%s-%s" % (d["Mod"]["code"], ndb.DateDMYtoISO(eval_data["jour"])) - if d["eval_data"]["description"]: - d["evaltitre"] = d["eval_data"]["description"] - else: - d["evaltitre"] = "évaluation du %s" % eval_data["jour"] - d["desceval"] = [ - ["%s" % d["sem"]["titreannee"]], - ["Module : %s - %s" % (d["Mod"]["code"], d["Mod"]["abbrev"])], - ["Surveillants : %(surveillant)s" % d], - ["Batiment : %(batiment)s - Salle : %(salle)s" % d], - ["Controle : %s (coef. %g)" % (d["evaltitre"], d["eval_data"]["coefficient"])], - ] # une liste de liste de chaines: description de l'evaluation - if form["file_format"].data == "xls": - production_xls(d) - else: - production_pdf(d) - - -def repartition(d): - """ - Calcule le placement. retourne une liste de couples ((nom, prenom), position) - """ - # Construit liste des etudiants - groups = sco_groups.listgroups(d["groups"]) - d["listetud"] = build_listetud(d) - return affectation_places(d) - - -def build_listetud(cnx, groups, evaluation_id, moduleimpl_data): - if None in [g["group_name"] for g in groups]: # tous les etudiants - getallstudents = True - gr_title_filename = "tous" - else: - getallstudents = False - etudids = sco_groups.do_evaluation_listeetuds_groups( - evaluation_id, groups, getallstudents=getallstudents, include_dems=True - ) - listetud = [] # liste de couples (nom,prenom) - for etudid in etudids: - # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) - ident = sco_etud.etudident_list(cnx, {"etudid": etudid})[0] - # infos inscription - inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - {"etudid": etudid, "formsemestre_id": moduleimpl_data["formsemestre_id"]} - )[0] - if inscr["etat"] != "D": - nom = ident["nom"].upper() - prenom = ident["prenom"].lower().capitalize() - listetud.append((nom, prenom)) - random.shuffle(listetud) - return listetud - - -class DistributeurContinu: +class _DistributeurContinu: """Distribue les places selon un ordre numérique.""" def __init(self): @@ -419,7 +152,7 @@ class DistributeurContinu: return retour -class Distributeur2D: +class _Distributeur2D: """Distribue les places selon des coordonnées sur nb_rangs.""" def __init__(self, nb_rangs): @@ -436,19 +169,140 @@ class Distributeur2D: return retour -def affectation_places(d): +def placement_eval_selectetuds(evaluation_id): + """Creation de l'écran de placement""" + form = PlacementForm( + request.form, + data={"evaluation_id": int(evaluation_id), "groups": PlacementForm.TOUS}, + ) + form.set_evaluation_infos(evaluation_id) + if form.validate_on_submit(): + return _exec_placement(form) # calcul et generation du fichier + # return flask.redirect(url_for("scodoc.index")) + 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)) + F = html_sco_header.sco_footer() + return "\n".join(H) + "

    " + F + + +def _exec_placement(form): + """Calcul et génération du fichier sur la base des données du formulaire""" + d = { + "evaluation_id": form["evaluation_id"].data, + "etiquetage": form["etiquetage"].data, + "surveillants": form["surveillants"].data, + "batiment": form["batiment"].data, + "salle": form["salle"].data, + "nb_rangs": form["nb_rangs"].data, + "groups_ids": form["groups"].data, + } + d["eval_data"] = sco_evaluations.do_evaluation_list( + {"evaluation_id": d["evaluation_id"]} + )[0] + # Check access (admin, respformation, and responsable_id) + d["current_user"] = current_user + d["moduleimpl_id"] = d["eval_data"]["moduleimpl_id"] + if not sco_permissions_check.can_edit_notes(d["current_user"], d["moduleimpl_id"]): + return """

    Génération du placement impossible pour %s

    +

    (vérifiez que le semestre n'est pas verrouillé et que vous +avez l'autorisation d'effectuer cette opération)

    +

    Continuer

    +""" % ( + d["current_user"].user_name, + d["module_id"], + ) + d["cnx"] = ndb.GetDBConnexion() + d["groups"] = sco_groups.listgroups(d["groups_ids"]) + d["gr_title_filename"] = sco_groups.listgroups_filename(d["groups"]) + # gr_title = sco_groups.listgroups_abbrev(d['groups']) + d["moduleimpl_data"] = sco_moduleimpl.do_moduleimpl_list( + moduleimpl_id=d["moduleimpl_id"] + )[0] + d["Mod"] = sco_edit_module.do_module_list( + args={"module_id": d["moduleimpl_data"]["module_id"]} + )[0] + d["sem"] = sco_formsemestre.get_formsemestre( + d["moduleimpl_data"]["formsemestre_id"] + ) + d["evalname"] = "%s-%s" % ( + d["Mod"]["code"], + ndb.DateDMYtoISO(d["eval_data"]["jour"]), + ) + if d["eval_data"]["description"]: + d["evaltitre"] = d["eval_data"]["description"] + else: + d["evaltitre"] = "évaluation du %s" % d["eval_data"]["jour"] + d["desceval"] = [ + ["%s" % d["sem"]["titreannee"]], + ["Module : %s - %s" % (d["Mod"]["code"], d["Mod"]["abbrev"])], + ["Surveillants : %(surveillants)s" % d], + ["Batiment : %(batiment)s - Salle : %(salle)s" % d], + ["Controle : %s (coef. %g)" % (d["evaltitre"], d["eval_data"]["coefficient"])], + ] # une liste de liste de chaines: description de l'evaluation + d["plan"] = _repartition(d) + if form["file_format"].data == "xls": + return _production_xls(d) + else: + return _production_pdf(d) + + +def _repartition(d): + """ + Calcule le placement. retourne une liste de couples ((nom, prenom), position) + """ + # Construit liste des etudiants + d["groups"] = sco_groups.listgroups(d["groups_ids"]) + d["listetud"] = _build_listetud(d) + return _affectation_places(d) + + +def _build_listetud(d): + if None in [g["group_name"] for g in d["groups"]]: # tous les etudiants + getallstudents = True + gr_title_filename = "tous" + else: + getallstudents = False + etudids = sco_groups.do_evaluation_listeetuds_groups( + d["evaluation_id"], + d["groups"], + getallstudents=getallstudents, + include_dems=True, + ) + listetud = [] # liste de couples (nom,prenom) + for etudid in etudids: + # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) + ident = sco_etud.etudident_list(d["cnx"], {"etudid": etudid})[0] + # infos inscription + inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( + { + "etudid": etudid, + "formsemestre_id": d["moduleimpl_data"]["formsemestre_id"], + } + )[0] + if inscr["etat"] != "D": + nom = ident["nom"].upper() + prenom = ident["prenom"].lower().capitalize() + listetud.append((nom, prenom)) + random.shuffle(listetud) + return listetud + + +def _affectation_places(d): plan = [] if d["etiquetage"] == "continu": - distributeur = DistributeurContinu() + distributeur = _DistributeurContinu() else: - distributeur = Distributeur2D(d["nb_rangs"]) + distributeur = _Distributeur2D(d["nb_rangs"]) for etud in d["listetud"]: plan.append((etud, distributeur.suivant())) return plan -def production_xls(d): - filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" +def _production_xls(d): + breakpoint() + filename = scu.make_filename("placement_%(evalname)s_%(gr_title_filename)s{scu.XLSX_SUFFIX}" % d) xls = _excel_feuille_placement( d["eval_data"], d["desceval"], @@ -458,13 +312,13 @@ def production_xls(d): d["salle"], d["etiquetage"], ) - return sco_excel.send_excel_file(REQUEST, xls, filename) + return sco_excel.send_from_flask(xls, filename) -def production_pdf(d): +def _production_pdf(d): pdf_title = d["desceval"] pdf_title += ( - "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" % d["eval_data"] + "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" % d["eval_data"] ) filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % d @@ -517,8 +371,8 @@ def production_pdf(d): rows=rows, filename=filename, origin="Généré par %s le " % sco_version.SCONAME - + scu.timedate_human_repr() - + "", + + scu.timedate_human_repr() + + "", pdf_title=pdf_title, # pdf_shorttitle = '', preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), @@ -528,42 +382,6 @@ def production_pdf(d): return t -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: - raise ScoValueError("invalid evaluation_id") - theeval = evals[0] - - if theeval["description"]: - page_title = 'Placement "%s"' % theeval["description"] - else: - page_title = "Placement des étudiants" - H = [html_sco_header.sco_header(page_title=page_title)] - - formid = "placementfile" - if not REQUEST.form.get("%s-submitted" % formid, False): - # not submitted, choix groupe - r = do_placement_selectetuds() - if r: - if isinstance(r, str): - H.append(r) - elif isinstance(r, Response): - H.append(r.get_data().decode("utf-8")) - H.append( - """

    Explications

    • Choisir le format du fichier résultat :
      • le format pdf - consiste en un tableau précisant pour chaque étudiant la localisation de sa table;
      • le format xls - produit un classeur avec deux onglets
        • le premier onglet donne une vue de la salle avec la - localisation des étudiants et peut servir de feuille d'émargement;
        • le second onglet est un tableau - similaire à celui du fichier pdf;
    • préciser les surveillants et la localisation (bâtiment - et salle) et indiquer le nombre de colonnes;
    • deux types de placements sont possibles :
      • -
      • continue suppose que les tables ont toutes un numéro unique;
      • coordonnées localise chaque table - via un numéro de colonne et un numéro de ligne (ou rangée).
    """ - ) - H.append(html_sco_header.sco_footer()) - return "\n".join(H) - - def _one_header(ws, numbering, styles): cells = [] if numbering == "coordinate": @@ -691,16 +509,16 @@ def _titres(ws, description, evaluation, building, room, styles): def _feuille0( - ws0, - description, - evaluation, - styles, - numbering, - listetud, - nbcolumns, - building, - room, - space, + ws0, + description, + evaluation, + styles, + numbering, + listetud, + nbcolumns, + building, + room, + space, ): _titres(ws0, description, evaluation, building, room, styles) # entetes colonnes - feuille0 @@ -788,16 +606,16 @@ def _next_page(ws): def _feuille1( - ws, - description, - evaluation, - styles, - numbering, - maxlines, - nbcolumns, - building, - room, - listetud, + ws, + description, + evaluation, + styles, + numbering, + maxlines, + nbcolumns, + building, + room, + listetud, ): # etudiants - feuille1 # structuration: @@ -854,15 +672,13 @@ def _feuille1( def _excel_feuille_placement( - evaluation, - description, - listetud, - columns, - space, - maxlines, - building, - room, - numbering, + evaluation, + description, + listetud, + columns, + building, + room, + numbering, ): """Genere feuille excel pour placement des etudiants. E: evaluation (dict) @@ -887,7 +703,7 @@ def _excel_feuille_placement( 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 + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1: col + 2], width ) SheetName1 = "Positions" From c8872bd2204bbc9eaf2e332c328361d5da205ef3 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sun, 12 Sep 2021 09:31:07 +0200 Subject: [PATCH 07/22] excel file returned --- app/scodoc/sco_excel.py | 2 +- app/scodoc/sco_placement.py | 979 ++++++++++++++++-------------------- 2 files changed, 433 insertions(+), 548 deletions(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index aae6a01d6a..337d33f08a 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -70,7 +70,7 @@ def send_from_flask(data, filename, mime=scu.XLSX_MIMETYPE): response = make_response(data) response.headers['Content-Type'] = mime response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename - + return response def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE): """publication fichier. diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 665bf64f41..e39d5f7479 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -76,6 +76,9 @@ from app.scodoc.TrivialFormulator import TrivialFormulator _ = lambda x: x # sans babel _l = _ +COORD = "Coordonnées" +SEQ = "Continue" + class PlacementForm(FlaskForm): TOUS = "Tous" @@ -87,9 +90,7 @@ class PlacementForm(FlaskForm): wtforms.validators.DataRequired("indiquez le format du fichier attendu"), ], ) - surveillants = StringField( - "Surveillants", validators=[wtforms.validators.DataRequired("Test")] - ) + surveillants = StringField("Surveillants", validators=[]) batiment = StringField("Batiment") salle = StringField("Salle") nb_rangs = SelectField( @@ -97,7 +98,7 @@ class PlacementForm(FlaskForm): ) etiquetage = RadioField( "Numérotation", - choices=["Continue", "Coordonnées"], + choices=[SEQ, COORD], validators=[ wtforms.validators.DataRequired("indiquez le style de numérotation"), ], @@ -177,7 +178,17 @@ def placement_eval_selectetuds(evaluation_id): ) form.set_evaluation_infos(evaluation_id) if form.validate_on_submit(): - return _exec_placement(form) # calcul et generation du fichier + runner = PlacementRunner(form) + if not runner.check_placement(): + return ( + """

    Génération du placement impossible pour %s

    +

    (vérifiez que le semestre n'est pas verrouillé et que vous + avez l'autorisation d'effectuer cette opération)

    +

    Continuer

    + """ + % runner.__dict__ + ) + return runner._exec_placement() # calcul et generation du fichier # return flask.redirect(url_for("scodoc.index")) H = [html_sco_header.sco_header(init_jquery_ui=True)] H.append(sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)) @@ -187,551 +198,425 @@ def placement_eval_selectetuds(evaluation_id): return "\n".join(H) + "

    " + F -def _exec_placement(form): - """Calcul et génération du fichier sur la base des données du formulaire""" - d = { - "evaluation_id": form["evaluation_id"].data, - "etiquetage": form["etiquetage"].data, - "surveillants": form["surveillants"].data, - "batiment": form["batiment"].data, - "salle": form["salle"].data, - "nb_rangs": form["nb_rangs"].data, - "groups_ids": form["groups"].data, - } - d["eval_data"] = sco_evaluations.do_evaluation_list( - {"evaluation_id": d["evaluation_id"]} - )[0] - # Check access (admin, respformation, and responsable_id) - d["current_user"] = current_user - d["moduleimpl_id"] = d["eval_data"]["moduleimpl_id"] - if not sco_permissions_check.can_edit_notes(d["current_user"], d["moduleimpl_id"]): - return """

    Génération du placement impossible pour %s

    -

    (vérifiez que le semestre n'est pas verrouillé et que vous -avez l'autorisation d'effectuer cette opération)

    -

    Continuer

    -""" % ( - d["current_user"].user_name, - d["module_id"], - ) - d["cnx"] = ndb.GetDBConnexion() - d["groups"] = sco_groups.listgroups(d["groups_ids"]) - d["gr_title_filename"] = sco_groups.listgroups_filename(d["groups"]) - # gr_title = sco_groups.listgroups_abbrev(d['groups']) - d["moduleimpl_data"] = sco_moduleimpl.do_moduleimpl_list( - moduleimpl_id=d["moduleimpl_id"] - )[0] - d["Mod"] = sco_edit_module.do_module_list( - args={"module_id": d["moduleimpl_data"]["module_id"]} - )[0] - d["sem"] = sco_formsemestre.get_formsemestre( - d["moduleimpl_data"]["formsemestre_id"] - ) - d["evalname"] = "%s-%s" % ( - d["Mod"]["code"], - ndb.DateDMYtoISO(d["eval_data"]["jour"]), - ) - if d["eval_data"]["description"]: - d["evaltitre"] = d["eval_data"]["description"] - else: - d["evaltitre"] = "évaluation du %s" % d["eval_data"]["jour"] - d["desceval"] = [ - ["%s" % d["sem"]["titreannee"]], - ["Module : %s - %s" % (d["Mod"]["code"], d["Mod"]["abbrev"])], - ["Surveillants : %(surveillants)s" % d], - ["Batiment : %(batiment)s - Salle : %(salle)s" % d], - ["Controle : %s (coef. %g)" % (d["evaltitre"], d["eval_data"]["coefficient"])], - ] # une liste de liste de chaines: description de l'evaluation - d["plan"] = _repartition(d) - if form["file_format"].data == "xls": - return _production_xls(d) - else: - return _production_pdf(d) - - -def _repartition(d): - """ - Calcule le placement. retourne une liste de couples ((nom, prenom), position) - """ - # Construit liste des etudiants - d["groups"] = sco_groups.listgroups(d["groups_ids"]) - d["listetud"] = _build_listetud(d) - return _affectation_places(d) - - -def _build_listetud(d): - if None in [g["group_name"] for g in d["groups"]]: # tous les etudiants - getallstudents = True - gr_title_filename = "tous" - else: - getallstudents = False - etudids = sco_groups.do_evaluation_listeetuds_groups( - d["evaluation_id"], - d["groups"], - getallstudents=getallstudents, - include_dems=True, - ) - listetud = [] # liste de couples (nom,prenom) - for etudid in etudids: - # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) - ident = sco_etud.etudident_list(d["cnx"], {"etudid": etudid})[0] - # infos inscription - inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( - { - "etudid": etudid, - "formsemestre_id": d["moduleimpl_data"]["formsemestre_id"], - } +class PlacementRunner: + def __init__(self, form): + """Calcul et génération du fichier sur la base des données du formulaire""" + self.evaluation_id = form["evaluation_id"].data + self.etiquetage = form["etiquetage"].data + self.surveillants = form["surveillants"].data + self.batiment = form["batiment"].data + self.salle = form["salle"].data + self.nb_rangs = form["nb_rangs"].data + self.file_format = form["file_format"].data + self.groups_ids = form["groups"].data + self.eval_data = sco_evaluations.do_evaluation_list( + {"evaluation_id": self.evaluation_id} )[0] - if inscr["etat"] != "D": - nom = ident["nom"].upper() - prenom = ident["prenom"].lower().capitalize() - listetud.append((nom, prenom)) - random.shuffle(listetud) - return listetud - - -def _affectation_places(d): - plan = [] - if d["etiquetage"] == "continu": - distributeur = _DistributeurContinu() - else: - distributeur = _Distributeur2D(d["nb_rangs"]) - for etud in d["listetud"]: - plan.append((etud, distributeur.suivant())) - return plan - - -def _production_xls(d): - breakpoint() - filename = scu.make_filename("placement_%(evalname)s_%(gr_title_filename)s{scu.XLSX_SUFFIX}" % d) - xls = _excel_feuille_placement( - d["eval_data"], - d["desceval"], - d["listetud"], - d["nb_rangs"], - d["batiment"], - d["salle"], - d["etiquetage"], - ) - return sco_excel.send_from_flask(xls, filename) - - -def _production_pdf(d): - pdf_title = d["desceval"] - pdf_title += ( - "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" % d["eval_data"] - ) - - filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % d - titles = { - "nom": "Nom", - "prenom": "Prenom", - "colonne": "Colonne", - "ligne": "Ligne", - "place": "Place", - } - nbcolumns = int(columns) - if numbering == "coordinate": - columns_ids = ["nom", "prenom", "colonne", "ligne"] - else: - columns_ids = ["nom", "prenom", "place"] - - # etudiants - line = 1 - col = 1 - orderetud = [] - for etudid in listetud: - if numbering == "coordinate": - orderetud.append((etudid[0], etudid[1], col, line)) + self.cnx = ndb.GetDBConnexion() + self.groups = sco_groups.listgroups(self.groups_ids) + self.gr_title_filename = sco_groups.listgroups_filename(self.groups) + # gr_title = sco_groups.listgroups_abbrev(d['groups']) + self.current_user = current_user + self.moduleimpl_id = self.eval_data["moduleimpl_id"] + self.moduleimpl_data = sco_moduleimpl.do_moduleimpl_list( + moduleimpl_id=self.moduleimpl_id + )[0] + self.Mod = sco_edit_module.do_module_list( + args={"module_id": self.moduleimpl_data["module_id"]} + )[0] + self.sem = sco_formsemestre.get_formsemestre( + self.moduleimpl_data["formsemestre_id"] + ) + self.evalname = "%s-%s" % ( + self.Mod["code"], + ndb.DateDMYtoISO(self.eval_data["jour"]), + ) + if self.eval_data["description"]: + self.evaltitre = self.eval_data["description"] else: - orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns)) + self.evaltitre = "évaluation du %s" % self.eval_data["jour"] + self.desceval = [ + ["%s" % self.sem["titreannee"]], + ["Module : %s - %s" % (self.Mod["code"], self.Mod["abbrev"])], + ["Surveillants : %s" % self.surveillants], + ["Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__], + [ + "Controle : %s (coef. %g)" + % (self.evaltitre, self.eval_data["coefficient"]) + ], + ] # une liste de liste de chaines: description de l'evaluation - if col == nbcolumns: - col = 0 - line += 1 - col += 1 - - rows = [] - orderetud.sort() - for etudid in orderetud: - if numbering == "coordinate": - rows.append( - { - "nom": etudid[0], - "prenom": etudid[1], - "colonne": etudid[2], - "ligne": etudid[3], - } - ) - else: - rows.append({"nom": etudid[0], "prenom": etudid[1], "place": etudid[2]}) - - tab = GenTable( - titles=titles, - columns_ids=columns_ids, - rows=rows, - filename=filename, - origin="Généré par %s le " % sco_version.SCONAME - + scu.timedate_human_repr() - + "", - pdf_title=pdf_title, - # pdf_shorttitle = '', - preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), - # html_generate_cells=False # la derniere ligne (moyennes) est incomplete - ) - t = tab.make_page(format="pdf", with_html_headers=False, REQUEST=REQUEST) - return t - - -def _one_header(ws, numbering, styles): - cells = [] - if numbering == "coordinate": - 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: - 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 - - -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 - 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 - - -def _init_lines(maxlines): - return [ - [] for _ in range(maxlines) - ] # lines[no_ligne] -> liste des cellules de la ligne (no_lignes de 1..maxlines - - -def _write_lines(ws, lines): - for line in lines: - ws.append_row(line) - - -def _titres(ws, description, evaluation, building, room, styles): - dt = time.strftime("%d/%m/%Y a %Hh%M") - ws.append_single_cell_row( - "Feuille placement etudiants éditée le %s" % dt, styles["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"], - ) - - -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): - cells.append(ws0.make_cell("colonne %s" % (col + 1), styles["2b"])) - ws0.append_row(cells) - # etudiants - line = 1 - col = 1 - linetud = [] - orderetud = [] - placementetud = [] - for etudid in listetud: - linetud.append(etudid) - if numbering == "coordinate": - orderetud.append((etudid[0], etudid[1], col, line)) - else: - orderetud.append((etudid[0], etudid[1], col + (line - 1) * nbcolumns)) - - if col == nbcolumns: - placementetud.append(linetud) - linetud = [] - col = 0 - line += 1 - col += 1 - if len(linetud) > 0: - placementetud.append(linetud) - - # etudiants - feuille0 - 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: - cells_a.append(ws0.make_cell(etudid[0], styles["1t"])) - cells_b.append(ws0.make_cell(etudid[1], styles["1m"])) - if numbering == "coordinate": - cell_c = ws0.make_cell("", styles["1bb"]) - else: - 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: - 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) - - -def _compute_ordretud(listetud, nbcolumns, numbering): - orderetud = [] - line = 1 - col = 1 - for etudid in listetud: - if numbering == "coordinate": - orderetud.append((etudid[0], etudid[1], col, line)) - else: - orderetud.append( - (etudid[0], etudid[1], "%s" % (col + (line - 1) * nbcolumns)) - ) - col += 1 - if col > nbcolumns: - col = 1 - line += 1 - orderetud.sort() - return orderetud - - -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 - if col >= maxlistes: # fin de page - _write_lines(ws, lines) - lines = _init_lines(maxlines) - col = 0 - 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) - - -def _excel_feuille_placement( - evaluation, - description, - listetud, - columns, - building, - room, - numbering, -): - """Genere feuille excel pour placement des etudiants. - E: evaluation (dict) - lines: liste de tuples - (etudid, nom, prenom, etat, groupe, val, explanation) - """ - sem_preferences = sco_preferences.SemPreferences() - space = sem_preferences.get("feuille_placement_emargement") - maxlines = sem_preferences.get("feuille_placement_positions") - 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 + def check_placement(self): + # Check access (admin, respformation, and responsable_id) + return sco_permissions_check.can_edit_notes( + self.current_user, self.moduleimpl_id ) - SheetName1 = "Positions" - ws1 = wb.create_sheet(SheetName1) + def _exec_placement(self): + self._repartition() + if self.file_format == "xls": + return self._production_xls() + else: + return self._production_pdf() - 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() + def _repartition(self): + """ + Calcule le placement. retourne une liste de couples ((nom, prenom), position) + """ + # Construit liste des etudiants et les réparti + self.groups = sco_groups.listgroups(self.groups_ids) + self.listetud = self._build_listetud() + self.plan = self._affectation_places() + + def _build_listetud(self): + if None in [g["group_name"] for g in self.groups]: # tous les etudiants + getallstudents = True + gr_title_filename = "tous" + else: + getallstudents = False + etudids = sco_groups.do_evaluation_listeetuds_groups( + self.evaluation_id, + self.groups, + getallstudents=getallstudents, + include_dems=True, + ) + listetud = [] # liste de couples (nom,prenom) + for etudid in etudids: + # infos identite etudiant (xxx sous-optimal: 1/select par etudiant) + ident = sco_etud.etudident_list(self.cnx, {"etudid": etudid})[0] + # infos inscription + inscr = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( + { + "etudid": etudid, + "formsemestre_id": self.moduleimpl_data["formsemestre_id"], + } + )[0] + if inscr["etat"] != "D": + nom = ident["nom"].upper() + prenom = ident["prenom"].lower().capitalize() + etudid = ident["etudid"] + listetud.append((nom, prenom, etudid)) + random.shuffle(listetud) + return listetud + + def _affectation_places(self): + plan = [] + if self.etiquetage == SEQ: + distributeur = _DistributeurContinu() + else: + distributeur = _Distributeur2D(self.nb_rangs) + for etud in self.listetud: + plan.append((etud, distributeur.suivant())) + return plan + + def _production_xls(self): + filename = scu.make_filename( + "placement_%s_%s%s" + % (self.evalname, self.gr_title_filename, scu.XLSX_SUFFIX) + ) + xls = self._excel_feuille_placement() + return sco_excel.send_from_flask(xls, filename) + + def _production_pdf(self): + pdf_title = self.desceval + pdf_title += ( + "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" + % self.eval_data + ) + + filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % self + titles = { + "nom": "Nom", + "prenom": "Prenom", + "colonne": "Colonne", + "ligne": "Ligne", + "place": "Place", + } + nb_rangs = int(self.nb_rangs) + if self.etiquetage == COORD: + columns_ids = ["nom", "prenom", "colonne", "ligne"] + else: + columns_ids = ["nom", "prenom", "place"] + + rows = [] + for etud in sorted(plan, key=lambda etud: etud[0][0]): # sort by name + if self.etiquetage == COORD: + rows.append( + { + "nom": etud[0][0], + "prenom": etud[0][1], + "colonne": etud[1][0], + "ligne": etud[1][1], + } + ) + else: + rows.append({"nom": etud[0][0], "prenom": etud[0][1], "place": etud[1]}) + + tab = GenTable( + titles=titles, + columns_ids=columns_ids, + rows=rows, + filename=filename, + origin="Généré par %s le " % sco_version.SCONAME + + scu.timedate_human_repr() + + "", + pdf_title=pdf_title, + # pdf_shorttitle = '', + preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), + # html_generate_cells=False # la derniere ligne (moyennes) est incomplete + ) + t = tab.make_page(format="pdf", with_html_headers=False, REQUEST=REQUEST) + return t + + def _one_header(self, ws): + cells = [ + ws.make_cell("Nom", self.styles["2bi"]), + ws.make_cell("Prénom", self.styles["2bi"]), + ] + if self.etiquetage == COORD: + cells.append(ws.make_cell("Colonne", self.styles["2bi"])) + cells.append(ws.make_cell("Ligne", self.styles["2bi"])) + else: + cells.append(ws.make_cell("Place", self.styles["2bi"])) + return cells + + def _headers(self, ws, nb_listes): + cells = [] + for _ in range(nb_listes): + cells += self._one_header(ws) + cells.append(ws.make_cell("")) + ws.append_row(cells) + + def _make_styles(self, 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 + self.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 + ), + } + + def _init_lines(self, maxlines): + return [ + [] for _ in range(maxlines) + ] # lines[no_ligne] -> liste des cellules de la ligne (no_lignes de 1..maxlines + + def _write_lines(self, ws, lines): + for line in lines: + ws.append_row(line) + + def _titres(self, ws): + dt = time.strftime("%d/%m/%Y a %Hh%M") + ws.append_single_cell_row( + "Feuille placement etudiants éditée le %s" % dt, self.styles["titres"] + ) + for line, desceval in enumerate(self.desceval): + if line in [1, 4, 7]: + ws.append_blank_row() + ws.append_single_cell_row(desceval[0], self.styles["titres"]) + ws.append_single_cell_row( + "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" + % self.eval_data, + self.styles["titres"], + ) + + def _feuille0(self, ws0, space): + self._titres(ws0) + # entetes colonnes - feuille0 + cells = [ws0.make_cell()] + for col in range(self.nb_rangs): + cells.append(ws0.make_cell("colonne %s" % (col + 1), self.styles["2b"])) + ws0.append_row(cells) + + # etudiants - feuille0 + place = 1 + for rang, linetud in enumerate(self.plan, 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, self.styles["2b"])] + cells_b = [ws0.make_cell("", self.styles["2b"])] + cells_c = [ws0.make_cell("", self.styles["2b"])] + row = 14 # premieère ligne de signature + for etudid in linetud: + cells_a.append(ws0.make_cell(etudid[0], self.styles["1t"])) + cells_b.append(ws0.make_cell(etudid[1], self.styles["1m"])) + if self.etiquetage == COORD: + cell_c = ws0.make_cell("", self.styles["1bb"]) + else: + cell_c = ws0.make_cell("place %s" % place, self.styles["1bb"]) + cells_c.append(cell_c) + ws0.set_row_dimension_height(row, space / 25) + row += 3 + place = place + 1 + if col == self.nb_rangs: + ws0.append_row(cells_a) + ws0.append_row(cells_b) + ws0.append_row(cells_c) + cells_a = [ws0.make_cell(rang, self.styles["2b"])] + cells_b = [ws0.make_cell("", self.styles["2b"])] + cells_c = [ws0.make_cell("", self.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) + + def _next_page(ws): + pass + + def _feuille1(self, ws, maxlines): + # 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 self.etiquetage == COORD: + 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(self.listetud) + self._titres(ws) + nb_listes = min( + maxlistes, nb_etu_restant // maxlines + 1 + ) # nombre de colonnes dans la page + self._headers(ws, nb_listes) + # construction liste alphabétique + # Affichage + lines = self._init_lines(maxlines) + line = 0 + col = 0 + for etud in sorted(self.plan, key=lambda etud: etud[0][0]): + # 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(etud[0][0], self.styles["2l"])) + lines[line].append(ws.make_cell(etud[0][1], self.styles["2m1"])) + if self.etiquetage == COORD: + lines[line].append(ws.make_cell(etud[1][0], self.styles["2m2"])) + lines[line].append(ws.make_cell(etud[1][1], self.styles["2r"])) + else: + lines[line].append(ws.make_cell(etud[1], self.styles["2r"])) + line = line + 1 + if line >= maxlines: # fin de liste + col = col + 1 + line = 0 + if col >= maxlistes: # fin de page + self._write_lines(ws, lines) + lines = self._init_lines(maxlines) + col = 0 + ws.append_blank_row() + nb_etu_restant -= maxlistes * maxlines + nb_listes = min( + maxlistes, nb_etu_restant // maxlines + 1 + ) # nombre de colonnes dans la page + self._headers(ws, nb_listes) + self._write_lines(ws, lines) + + def _excel_feuille_placement(self): + """Genere feuille excel pour placement des etudiants. + E: evaluation (dict) + lines: liste de tuples + (etudid, nom, prenom, etat, groupe, val, explanation) + """ + breakpoint() + sem_preferences = sco_preferences.SemPreferences() + space = sem_preferences.get("feuille_placement_emargement") + maxlines = sem_preferences.get("feuille_placement_positions") + nb_rangs = int(self.nb_rangs) + 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 nb_rangs > 5: + width = 22500 * column_width_ratio // nb_rangs + + ws0.set_column_dimension_width("A", 750 * column_width_ratio) + for col in range(nb_rangs): + ws0.set_column_dimension_width( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"[col + 1 : col + 2], width + ) + + SheetName1 = "Positions" + ws1 = wb.create_sheet(SheetName1) + + self._make_styles(ws0, ws1) + self._feuille0(ws0, space) + self._feuille1(ws1, maxlines) + return wb.generate() From c44aa808df93bd7337dca0cda1e80b1ec2408456 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Mon, 13 Sep 2021 07:16:37 +0200 Subject: [PATCH 08/22] preparation envoi fichier --- app/scodoc/gen_tables.py | 26 +++++++----- app/scodoc/sco_excel.py | 2 +- app/scodoc/sco_placement.py | 13 +++--- app/scodoc/sco_utils.py | 79 ++++++++++++++++++++++++++----------- 4 files changed, 77 insertions(+), 43 deletions(-) diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index a318f93eec..e594cdb2dd 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -60,6 +60,7 @@ from app.scodoc import sco_pdf from app.scodoc import sco_xml from app.scodoc.sco_pdf import SU from app import log +from app.scodoc.sco_utils import flaskPDFResponse def mark_paras(L, tags): @@ -648,30 +649,35 @@ class GenTable(object): objects, title=title, preferences=self.preferences ) if publish: - return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") + return scu.PDF_FORMAT.send_file(doc, filename, add_suffix=True, attached=True) + # return scu.flaskPDFResponse(doc, filename + ".pdf") + # return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") else: return doc elif format == "xls" or format == "xlsx": xls = self.excel() if publish: - return sco_excel.send_excel_file( - REQUEST, xls, filename + scu.XLSX_SUFFIX - ) + return scu.XLSX_FORMAT.send_file(xls, filename, add_suffix=True, attached=True) + # return sco_excel.send_from_flask(xls, filename + scu.XLSX_SUFFIX) + # return sco_excel.send_excel_file(REQUEST, xls, filename + scu.XLSX_SUFFIX) else: return xls elif format == "text": return self.text() elif format == "csv": - return scu.sendCSVFile(REQUEST, self.text(), filename + ".csv") + return scu.CSV_FORMAT.send_file(self.text(), filename, add_suffix=True, attached=True) + # return scu.sendCSVFile(REQUEST, self.text(), filename + ".csv") elif format == "xml": xml = self.xml() - if REQUEST and publish: - REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) - return xml + if publish: + # REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) + # return xml + return scu.XML_FORMAT.send_file(self.xml) elif format == "json": js = self.json() - if REQUEST and publish: - REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE) + if publish: + return scu.JSON_FORMAT.send_file(self.xml) + # REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE) return js else: log("make_page: format=%s" % format) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 337d33f08a..54728a16c7 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -66,7 +66,7 @@ class COLORS(Enum): def send_from_flask(data, filename, mime=scu.XLSX_MIMETYPE): - scu.make_filename(filename) + filename = scu.make_filename(filename) response = make_response(data) response.headers['Content-Type'] = mime response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index e39d5f7479..500c5a98b5 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -310,10 +310,7 @@ class PlacementRunner: return plan def _production_xls(self): - filename = scu.make_filename( - "placement_%s_%s%s" - % (self.evalname, self.gr_title_filename, scu.XLSX_SUFFIX) - ) + filename = "placement_%s_%s%s" % (self.evalname, self.gr_title_filename, scu.XLSX_SUFFIX) xls = self._excel_feuille_placement() return sco_excel.send_from_flask(xls, filename) @@ -323,8 +320,8 @@ class PlacementRunner: "Date : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s" % self.eval_data ) - - filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % self + breakpoint() + filename = "placement_%(evalname)s_%(gr_title_filename)s.pdf" % self.__dict__ titles = { "nom": "Nom", "prenom": "Prenom", @@ -339,7 +336,7 @@ class PlacementRunner: columns_ids = ["nom", "prenom", "place"] rows = [] - for etud in sorted(plan, key=lambda etud: etud[0][0]): # sort by name + for etud in sorted(self.plan, key=lambda item: item[0][0]): # sort by name if self.etiquetage == COORD: rows.append( { @@ -362,7 +359,7 @@ class PlacementRunner: + "", pdf_title=pdf_title, # pdf_shorttitle = '', - preferences=sco_preferences.SemPreferences(M["formsemestre_id"]), + preferences=sco_preferences.SemPreferences(self.moduleimpl_data["formsemestre_id"]), # html_generate_cells=False # la derniere ligne (moyennes) est incomplete ) t = tab.make_page(format="pdf", with_html_headers=False, REQUEST=REQUEST) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index b897110fd4..96a4d796c8 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -49,7 +49,7 @@ import unicodedata import urllib from xml.etree import ElementTree -from flask import g, current_app +from flask import g, current_app, make_response from PIL import Image as PILImage @@ -64,7 +64,6 @@ from app.scodoc import sco_exceptions from app.scodoc import sco_xml import sco_version - # ----- CALCUL ET PRESENTATION DES NOTES NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis NOTES_MIN = 0.0 # valeur minimale admise pour une note (sauf malus, dans [-20, 20]) @@ -73,7 +72,6 @@ NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes NOTES_SUPPRESS = -1001.0 # note a supprimer NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) - # Types de modules MODULE_STANDARD = 0 MODULE_MALUS = 1 @@ -88,7 +86,7 @@ IT_SITUATION_MISSING_STR = ( "____" # shown on ficheEtud (devenir) in place of empty situation ) -RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente +RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente # borne supérieure de chaque mention NOTES_MENTIONS_TH = ( @@ -234,11 +232,9 @@ if not os.path.exists(SCO_TMP_DIR): SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf - # ----- Les outils distribués SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools") - # ----- Lecture du fichier de configuration from app.scodoc import sco_config from app.scodoc import sco_config_load @@ -272,7 +268,6 @@ else: SCO_ENCODING = "utf-8" # used by Excel, XML, PDF, ... - SCO_DEFAULT_SQL_USER = "scodoc" # should match Zope process UID SCO_DEFAULT_SQL_PORT = "5432" SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT @@ -304,8 +299,11 @@ XLS_MIMETYPE = "application/vnd.ms-excel" XLSX_MIMETYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" XLSX_SUFFIX = ".xlsx" PDF_MIMETYPE = "application/pdf" +PDF_SUFFIX = ".pdf" XML_MIMETYPE = "text/xml" +XML_SUFFIX = ".xml" JSON_MIMETYPE = "application/json" +JSON_SUFFIX = ".json" # Admissions des étudiants # Différents types de voies d'admission: @@ -315,6 +313,31 @@ TYPES_ADMISSION = (TYPE_ADMISSION_DEFAULT, "APB", "APB-PC", "CEF", "Direct") BULLETINS_VERSIONS = ("short", "selectedevals", "long") + +# File format management +class FileFormat: + def __init__(self, suffix, mime): + self._mime = mime + self._suffix = suffix + + def send_file(self, data, filename, add_suffix=False, attached=False): + filename = make_filename(filename) + if add_suffix: + filename += self._suffix + response = make_response(data) + response.headers['Content-Type'] = self._mime + if attached: + response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename + return response + + +CSV_FORMAT = FileFormat(CSV_SUFFIX, CSV_MIMETYPE) +XLSX_FORMAT = FileFormat(XLSX_SUFFIX, XLSX_MIMETYPE) +XLS_FORMAT = FileFormat(XLS_SUFFIX, XLS_MIMETYPE) +PDF_FORMAT = FileFormat(PDF_SUFFIX, PDF_MIMETYPE) +XML_FORMAT = FileFormat(XML_SUFFIX, XML_MIMETYPE) +JSON_FORMAT = FileFormat(JSON_SUFFIX, JSON_MIMETYPE) + # Support for ScoDoc7 compatibility @@ -324,8 +347,8 @@ def ScoURL(): = page accueil département """ return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] + : -len("/index_html") + ] def NotesURL(): @@ -352,8 +375,8 @@ def EntreprisesURL(): def AbsencesURL(): """URL of Absences""" return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] + : -len("/index_html") + ] def UsersURL(): @@ -439,8 +462,8 @@ def suppress_accents(s): if isinstance(s, str): return ( unicodedata.normalize("NFD", s) - .encode("ascii", "ignore") - .decode(SCO_ENCODING) + .encode("ascii", "ignore") + .decode(SCO_ENCODING) ) return s # may be int @@ -504,6 +527,14 @@ def sendCSVFile(REQUEST, data, filename): return data +def flaskPDFResponse(data, filename, mime=PDF_MIMETYPE): + filename = make_filename(filename) + response = make_response(data) + response.headers['Content-Type'] = mime + response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename + return response + + def sendPDFFile(REQUEST, data, filename): filename = ( unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_") @@ -641,7 +672,7 @@ def sem_decale_str(sem): if sem["semestre_id"] <= 0: return "" if (sem["semestre_id"] % 2 and sem["mois_debut_ord"] <= 6) or ( - not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6 + not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6 ): return "D" else: @@ -800,15 +831,15 @@ def return_text_if_published(val, REQUEST): def confirm_dialog( - message="

    Confirmer ?

    ", - OK="OK", - Cancel="Annuler", - dest_url="", - cancel_url="", - target_variable="dialog_confirmed", - parameters={}, - add_headers=True, # complete page - helpmsg=None, + message="

    Confirmer ?

    ", + OK="OK", + Cancel="Annuler", + dest_url="", + cancel_url="", + target_variable="dialog_confirmed", + parameters={}, + add_headers=True, # complete page + helpmsg=None, ): from app.scodoc import html_sco_header @@ -850,7 +881,7 @@ def confirm_dialog( H.append('

    ' + helpmsg + "

    ") if add_headers: return ( - html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() + html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() ) else: return "\n".join(H) From 01c264c3c70a82933f9e762914955166c7742da3 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Mon, 13 Sep 2021 07:17:18 +0200 Subject: [PATCH 09/22] add copy to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6d49cf2c81..9a2e411be1 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,4 @@ Thumbs.db *.code-workspace +copy From a23ae3801488f39ee2b5024028dc43e099aad8e4 Mon Sep 17 00:00:00 2001 From: jean-marie Place Date: Fri, 17 Sep 2021 05:03:34 +0200 Subject: [PATCH 10/22] integration master --- app/scodoc/gen_tables.py | 55 +++++++++------ app/scodoc/sco_evaluations.py | 8 +-- app/scodoc/sco_utils.py | 128 +++++++++++++++++----------------- 3 files changed, 102 insertions(+), 89 deletions(-) diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index e594cdb2dd..67a52261c8 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -60,7 +60,6 @@ from app.scodoc import sco_pdf from app.scodoc import sco_xml from app.scodoc.sco_pdf import SU from app import log -from app.scodoc.sco_utils import flaskPDFResponse def mark_paras(L, tags): @@ -611,7 +610,6 @@ class GenTable(object): format="html", page_title="", filename=None, - REQUEST=None, javascripts=[], with_html_headers=True, publish=True, @@ -644,40 +642,55 @@ class GenTable(object): H.append(html_sco_header.sco_footer()) return "\n".join(H) elif format == "pdf": - objects = self.pdf() - doc = sco_pdf.pdf_basic_page( - objects, title=title, preferences=self.preferences + pdf_objs = self.pdf() + pdf_doc = sco_pdf.pdf_basic_page( + pdf_objs, title=title, preferences=self.preferences ) if publish: - return scu.PDF_FORMAT.send_file(doc, filename, add_suffix=True, attached=True) - # return scu.flaskPDFResponse(doc, filename + ".pdf") - # return scu.sendPDFFile(REQUEST, doc, filename + ".pdf") + return scu.send_file( + pdf_doc, + filename, + suffix=".pdf", + mime=scu.PDF_MIMETYPE, + attached=True, + ) else: - return doc - elif format == "xls" or format == "xlsx": + return pdf_doc + elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx xls = self.excel() if publish: - return scu.XLSX_FORMAT.send_file(xls, filename, add_suffix=True, attached=True) - # return sco_excel.send_from_flask(xls, filename + scu.XLSX_SUFFIX) - # return sco_excel.send_excel_file(REQUEST, xls, filename + scu.XLSX_SUFFIX) + return scu.send_file( + xls, + filename, + suffix=scu.XLSX_SUFFIX, + mime=scu.XLSX_MIMETYPE, + attached=True, + ) else: return xls elif format == "text": return self.text() elif format == "csv": - return scu.CSV_FORMAT.send_file(self.text(), filename, add_suffix=True, attached=True) - # return scu.sendCSVFile(REQUEST, self.text(), filename + ".csv") + return scu.send_file( + self.text(), + filename, + suffix=".csv", + mime=scu.CSV_MIMETYPE, + attached=True, + ) elif format == "xml": xml = self.xml() if publish: - # REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) - # return xml - return scu.XML_FORMAT.send_file(self.xml) + return scu.send_file( + xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE, attached=True + ) + return xml elif format == "json": js = self.json() if publish: - return scu.JSON_FORMAT.send_file(self.xml) - # REQUEST.RESPONSE.setHeader("content-type", scu.JSON_MIMETYPE) + return scu.send_file( + js, filename, suffix=".json", mime=scu.JSON_MIMETYPE, attached=True + ) return js else: log("make_page: format=%s" % format) @@ -738,5 +751,5 @@ if __name__ == "__main__": document.build(objects) data = doc.getvalue() open("/tmp/gen_table.pdf", "wb").write(data) - p = T.make_page(format="pdf", REQUEST=None) + p = T.make_page(format="pdf") open("toto.pdf", "wb").write(p) diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 3697d598a1..2397886d05 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -31,7 +31,7 @@ import datetime import operator import pprint import time -import six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error +import urllib import flask from flask import url_for @@ -921,7 +921,7 @@ def formsemestre_evaluations_delai_correction( + "", filename=scu.make_filename("evaluations_delais_" + sem["titreannee"]), ) - return tab.make_page(format=format, REQUEST=REQUEST) + return tab.make_page(format=format) def module_evaluation_insert_before(ModEvals, next_eval): @@ -1046,7 +1046,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): resp = u["prenomnom"] nomcomplet = u["nomcomplet"] can_edit = sco_permissions_check.can_edit_notes( - , moduleimpl_id, allow_ens=False + current_user, moduleimpl_id, allow_ens=False ) link = ( @@ -1089,7 +1089,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True): % ( scu.ScoURL(), group_id, - six.moves.urllib.parse.quote(E["jour"], safe=""), + urllib.parse.quote(E["jour"], safe=""), ) ) H.append( diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 96a4d796c8..087902a7b0 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -49,11 +49,12 @@ import unicodedata import urllib from xml.etree import ElementTree -from flask import g, current_app, make_response +from flask import g, current_app from PIL import Image as PILImage -from flask import g, url_for, request +from flask import g, url_for, request, make_response +from werkzeug.wrappers import response from config import Config from app import log @@ -64,6 +65,7 @@ from app.scodoc import sco_exceptions from app.scodoc import sco_xml import sco_version + # ----- CALCUL ET PRESENTATION DES NOTES NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis NOTES_MIN = 0.0 # valeur minimale admise pour une note (sauf malus, dans [-20, 20]) @@ -72,6 +74,7 @@ NOTES_NEUTRALISE = -1000.0 # notes non prises en comptes dans moyennes NOTES_SUPPRESS = -1001.0 # note a supprimer NOTES_ATTENTE = -1002.0 # note "en attente" (se calcule comme une note neutralisee) + # Types de modules MODULE_STANDARD = 0 MODULE_MALUS = 1 @@ -86,7 +89,7 @@ IT_SITUATION_MISSING_STR = ( "____" # shown on ficheEtud (devenir) in place of empty situation ) -RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente +RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente # borne supérieure de chaque mention NOTES_MENTIONS_TH = ( @@ -232,9 +235,11 @@ if not os.path.exists(SCO_TMP_DIR): SCODOC_LOGOS_DIR = os.path.join(SCODOC_CFG_DIR, "logos") LOGOS_IMAGES_ALLOWED_TYPES = ("jpg", "jpeg", "png") # remind that PIL does not read pdf + # ----- Les outils distribués SCO_TOOLS_DIR = os.path.join(Config.SCODOC_DIR, "tools") + # ----- Lecture du fichier de configuration from app.scodoc import sco_config from app.scodoc import sco_config_load @@ -268,6 +273,7 @@ else: SCO_ENCODING = "utf-8" # used by Excel, XML, PDF, ... + SCO_DEFAULT_SQL_USER = "scodoc" # should match Zope process UID SCO_DEFAULT_SQL_PORT = "5432" SCO_DEFAULT_SQL_USERS_CNX = "dbname=SCOUSERS port=%s" % SCO_DEFAULT_SQL_PORT @@ -295,15 +301,33 @@ SCO_DUMP_UP_URL = "https://scodoc.iutv.univ-paris13.fr/scodoc-installmgr/upload- CSV_FIELDSEP = ";" CSV_LINESEP = "\n" CSV_MIMETYPE = "text/comma-separated-values" +CSV_SUFFIX = ".csv" +JSON_MIMETYPE = "application/json" +JSON_SUFFIX = ".json" +PDF_MIMETYPE = "application/pdf" +PDF_SUFFIX = ".pdf" XLS_MIMETYPE = "application/vnd.ms-excel" XLSX_MIMETYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" XLSX_SUFFIX = ".xlsx" -PDF_MIMETYPE = "application/pdf" -PDF_SUFFIX = ".pdf" XML_MIMETYPE = "text/xml" XML_SUFFIX = ".xml" -JSON_MIMETYPE = "application/json" -JSON_SUFFIX = ".json" + + +def get_mime_suffix(format_code: str) -> tuple[str, str]: + """Returns (MIME, SUFFIX) from format_code == "xls", "xml", ... + SUFFIX includes the dot: ".xlsx", ".xml", ... + "xls" and "xlsx" format codes give XLSX + """ + d = { + "csv": (CSV_MIMETYPE, CSV_SUFFIX), + "xls": (XLSX_MIMETYPE, XLSX_SUFFIX), + "xlsx": (XLSX_MIMETYPE, XLSX_SUFFIX), + "pdf": (PDF_MIMETYPE, PDF_SUFFIX), + "xml": (XML_MIMETYPE, XML_SUFFIX), + "json": (JSON_MIMETYPE, JSON_SUFFIX), + } + return d[format_code] + # Admissions des étudiants # Différents types de voies d'admission: @@ -313,31 +337,6 @@ TYPES_ADMISSION = (TYPE_ADMISSION_DEFAULT, "APB", "APB-PC", "CEF", "Direct") BULLETINS_VERSIONS = ("short", "selectedevals", "long") - -# File format management -class FileFormat: - def __init__(self, suffix, mime): - self._mime = mime - self._suffix = suffix - - def send_file(self, data, filename, add_suffix=False, attached=False): - filename = make_filename(filename) - if add_suffix: - filename += self._suffix - response = make_response(data) - response.headers['Content-Type'] = self._mime - if attached: - response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename - return response - - -CSV_FORMAT = FileFormat(CSV_SUFFIX, CSV_MIMETYPE) -XLSX_FORMAT = FileFormat(XLSX_SUFFIX, XLSX_MIMETYPE) -XLS_FORMAT = FileFormat(XLS_SUFFIX, XLS_MIMETYPE) -PDF_FORMAT = FileFormat(PDF_SUFFIX, PDF_MIMETYPE) -XML_FORMAT = FileFormat(XML_SUFFIX, XML_MIMETYPE) -JSON_FORMAT = FileFormat(JSON_SUFFIX, JSON_MIMETYPE) - # Support for ScoDoc7 compatibility @@ -347,8 +346,8 @@ def ScoURL(): = page accueil département """ return url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] + : -len("/index_html") + ] def NotesURL(): @@ -375,8 +374,8 @@ def EntreprisesURL(): def AbsencesURL(): """URL of Absences""" return url_for("absences.index_html", scodoc_dept=g.scodoc_dept)[ - : -len("/index_html") - ] + : -len("/index_html") + ] def UsersURL(): @@ -462,8 +461,8 @@ def suppress_accents(s): if isinstance(s, str): return ( unicodedata.normalize("NFD", s) - .encode("ascii", "ignore") - .decode(SCO_ENCODING) + .encode("ascii", "ignore") + .decode(SCO_ENCODING) ) return s # may be int @@ -513,7 +512,7 @@ def is_valid_filename(filename): return VALID_EXP.match(filename) -def sendCSVFile(REQUEST, data, filename): +def sendCSVFile(REQUEST, data, filename): # DEPRECATED ne plus utiliser """publication fichier. (on ne doit rien avoir émis avant, car ici sont générés les entetes) """ @@ -527,14 +526,6 @@ def sendCSVFile(REQUEST, data, filename): return data -def flaskPDFResponse(data, filename, mime=PDF_MIMETYPE): - filename = make_filename(filename) - response = make_response(data) - response.headers['Content-Type'] = mime - response.headers['Content-Disposition'] = 'attachment; filename="%s"' % filename - return response - - def sendPDFFile(REQUEST, data, filename): filename = ( unescape_html(suppress_accents(filename)).replace("&", "").replace(" ", "_") @@ -559,19 +550,16 @@ class ScoDocJSONEncoder(json.JSONEncoder): def sendJSON(REQUEST, data): js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) - if REQUEST: - REQUEST.RESPONSE.setHeader("content-type", JSON_MIMETYPE) - return js + return send_file(js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=False) def sendXML(REQUEST, data, tagname=None, force_outer_xml_tag=True): if type(data) != list: data = [data] # always list-of-dicts if force_outer_xml_tag: - root_tagname = tagname + "_list" - data = [{root_tagname: data}] + data = [{tagname: data}] + tagname += "_list" doc = sco_xml.simple_dictlist2xml(data, tagname=tagname) - if REQUEST: REQUEST.RESPONSE.setHeader("content-type", XML_MIMETYPE) return doc @@ -590,6 +578,18 @@ def sendResult(REQUEST, data, name=None, format=None, force_outer_xml_tag=True): raise ValueError("invalid format: %s" % format) +def send_file(data, filename, suffix="", mime=None, attached=True): + "Build Flask Response for file download of given type" + if suffix: + filename += suffix + filename = make_filename(filename) + response = make_response(data) + response.headers["Content-Type"] = mime + if attached: + response.headers["Content-Disposition"] = 'attachment; filename="%s"' % filename + return response + + def get_scodoc_version(): "return a string identifying ScoDoc version" return sco_version.SCOVERSION @@ -672,7 +672,7 @@ def sem_decale_str(sem): if sem["semestre_id"] <= 0: return "" if (sem["semestre_id"] % 2 and sem["mois_debut_ord"] <= 6) or ( - not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6 + not sem["semestre_id"] % 2 and sem["mois_debut_ord"] > 6 ): return "D" else: @@ -831,15 +831,15 @@ def return_text_if_published(val, REQUEST): def confirm_dialog( - message="

    Confirmer ?

    ", - OK="OK", - Cancel="Annuler", - dest_url="", - cancel_url="", - target_variable="dialog_confirmed", - parameters={}, - add_headers=True, # complete page - helpmsg=None, + message="

    Confirmer ?

    ", + OK="OK", + Cancel="Annuler", + dest_url="", + cancel_url="", + target_variable="dialog_confirmed", + parameters={}, + add_headers=True, # complete page + helpmsg=None, ): from app.scodoc import html_sco_header @@ -881,7 +881,7 @@ def confirm_dialog( H.append('

    ' + helpmsg + "

    ") if add_headers: return ( - html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() + html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() ) else: return "\n".join(H) From 0ef822cfd80f1803f8e4b8cb014e6ac9cc653510 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Fri, 17 Sep 2021 05:54:11 +0200 Subject: [PATCH 11/22] =?UTF-8?q?d=C3=A9placement=20du=20template=20placem?= =?UTF-8?q?ent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_placement.py | 71 ++++++++++--------- .../{ => scodoc}/forms/placement.html | 3 +- 2 files changed, 39 insertions(+), 35 deletions(-) rename app/templates/{ => scodoc}/forms/placement.html (92%) diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 500c5a98b5..32f8943739 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -144,7 +144,7 @@ class PlacementForm(FlaskForm): class _DistributeurContinu: """Distribue les places selon un ordre numérique.""" - def __init(self): + def __init__(self): self.position = 1 def suivant(self): @@ -193,7 +193,7 @@ def placement_eval_selectetuds(evaluation_id): 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(render_template("scodoc/forms/placement.html", form=form)) F = html_sco_header.sco_footer() return "\n".join(H) + "

    " + F @@ -493,36 +493,40 @@ class PlacementRunner: # etudiants - feuille0 place = 1 - for rang, linetud in enumerate(self.plan, 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, self.styles["2b"])] - cells_b = [ws0.make_cell("", self.styles["2b"])] - cells_c = [ws0.make_cell("", self.styles["2b"])] - row = 14 # premieère ligne de signature - for etudid in linetud: - cells_a.append(ws0.make_cell(etudid[0], self.styles["1t"])) - cells_b.append(ws0.make_cell(etudid[1], self.styles["1m"])) - if self.etiquetage == COORD: - cell_c = ws0.make_cell("", self.styles["1bb"]) - else: - cell_c = ws0.make_cell("place %s" % place, self.styles["1bb"]) - cells_c.append(cell_c) - ws0.set_row_dimension_height(row, space / 25) - row += 3 + col = 0 + rang = 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, self.styles["2b"])] + cells_b = [ws0.make_cell("", self.styles["2b"])] + cells_c = [ws0.make_cell("", self.styles["2b"])] + row = 13 # première ligne de signature + for linetud in self.plan: + cells_a.append(ws0.make_cell(linetud[0][0], self.styles["1t"])) # nom + cells_b.append(ws0.make_cell(linetud[0][1], self.styles["1m"])) # prenom + if self.etiquetage == COORD: + cell_c = ws0.make_cell("", self.styles["1bb"]) + else: + cell_c = ws0.make_cell("place %s" % place, self.styles["1bb"]) place = place + 1 - if col == self.nb_rangs: - ws0.append_row(cells_a) - ws0.append_row(cells_b) - ws0.append_row(cells_c) - cells_a = [ws0.make_cell(rang, self.styles["2b"])] - cells_b = [ws0.make_cell("", self.styles["2b"])] - cells_c = [ws0.make_cell("", self.styles["2b"])] - # publication du rang final incomplet - ws0.append_row(cells_a) - ws0.append_row(cells_b) - ws0.append_row(cells_c) + cells_c.append(cell_c) ws0.set_row_dimension_height(row, space / 25) + row += 3 + col += 1 + if col == self.nb_rangs: # On a fini la rangée courante + ws0.append_row(cells_a) # on affiche les 3 lignes construites + ws0.append_row(cells_b) + ws0.append_row(cells_c) + cells_a = [ws0.make_cell(rang, self.styles["2b"])] # on réinitialise les 3 lignes + cells_b = [ws0.make_cell("", self.styles["2b"])] + cells_c = [ws0.make_cell("", self.styles["2b"])] + col = 0 + rang += 1 + # publication du rang final incomplet + ws0.append_row(cells_a) # Affiche des 3 lignes (dernières lignes incomplètes) + ws0.append_row(cells_b) + ws0.append_row(cells_c) + ws0.set_row_dimension_height(row, space / 25) def _next_page(ws): pass @@ -553,15 +557,15 @@ class PlacementRunner: lines = self._init_lines(maxlines) line = 0 col = 0 - for etud in sorted(self.plan, key=lambda etud: etud[0][0]): + for etud in sorted(self.plan, key=lambda e: e[0][0]): # tri alphabétique # 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(etud[0][0], self.styles["2l"])) lines[line].append(ws.make_cell(etud[0][1], self.styles["2m1"])) if self.etiquetage == COORD: - lines[line].append(ws.make_cell(etud[1][0], self.styles["2m2"])) - lines[line].append(ws.make_cell(etud[1][1], self.styles["2r"])) + lines[line].append(ws.make_cell(etud[1][1], self.styles["2m2"])) + lines[line].append(ws.make_cell(etud[1][0], self.styles["2r"])) else: lines[line].append(ws.make_cell(etud[1], self.styles["2r"])) line = line + 1 @@ -586,7 +590,6 @@ class PlacementRunner: lines: liste de tuples (etudid, nom, prenom, etat, groupe, val, explanation) """ - breakpoint() sem_preferences = sco_preferences.SemPreferences() space = sem_preferences.get("feuille_placement_emargement") maxlines = sem_preferences.get("feuille_placement_positions") diff --git a/app/templates/forms/placement.html b/app/templates/scodoc/forms/placement.html similarity index 92% rename from app/templates/forms/placement.html rename to app/templates/scodoc/forms/placement.html index 1c5ca544e3..7f169cd38f 100644 --- a/app/templates/forms/placement.html +++ b/app/templates/scodoc/forms/placement.html @@ -28,7 +28,7 @@ {{ render_field(form.etiquetage) }} {% if form.has_groups %} {{ render_field(form.groups) }} - -

    -
    - """, - groups_table( - groups_infos=groups_infos, - format=format, - REQUEST=REQUEST, - with_codes=with_codes, - etat=etat, - with_paiement=with_paiement, - with_archives=with_archives, - with_annotations=with_annotations, - ), - "
    ", - """
    """, - tab_photos_html(groups_infos, etat=etat, REQUEST=REQUEST), - #'

    hello

    ', - "
    ", - '
    ', - tab_absences_html(groups_infos, etat=etat, REQUEST=REQUEST), - "
    ", - ) - ) - - H.append(html_sco_header.sco_footer()) - return "\n".join(H) - - -def form_groups_choice(groups_infos, with_selectall_butt=False, submit_on_change=False): - """form pour selection groupes - group_ids est la liste des groupes actuellement sélectionnés - et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. - (utilisé pour retrouver le semestre et proposer la liste des autres groupes) - - Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS - """ - default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - - H = [ - """
    - - - Groupes: - """ - % (groups_infos.formsemestre_id, default_group_id) - ] - - H.append(menu_groups_choice(groups_infos, submit_on_change=submit_on_change)) - - if with_selectall_butt: - H.append( - """""" - ) - H.append("") - - return "\n".join(H) - - -def menu_groups_choice(groups_infos, submit_on_change=False): - """menu pour selection groupes - group_ids est la liste des groupes actuellement sélectionnés - et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. - (utilisé pour retrouver le semestre et proposer la liste des autres groupes) - """ - default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - - if submit_on_change: - klass = "submit_on_change" - else: - klass = "" - H = [ - """ ") - return "\n".join(H) - - -def menu_group_choice(group_id=None, formsemestre_id=None): - """Un bête menu pour choisir un seul groupe - group_id est le groupe actuellement sélectionné. - Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes. - """ - if group_id: - group = sco_groups.get_group(group_id) - formsemestre_id = group["formsemestre_id"] - elif not formsemestre_id: - raise ValueError("missing formsemestre_id") - H = [ - """ - - - """ - ) - return "\n".join(H) - - -class DisplayedGroupsInfos(object): - """Container with attributes describing groups to display in the page - .groups_query_args : 'group_ids=xxx&group_ids=yyy' - .base_url : url de la requete, avec les groupes, sans les autres paramètres - .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste) - .members - .groups_titles - """ - - def __init__( - self, - group_ids=[], # groupes specifies dans l'URL, ou un seul int - formsemestre_id=None, - etat=None, - select_all_when_unspecified=False, - moduleimpl_id=None, # used to find formsemestre when unspecified - ): - if isinstance(group_ids, int): - if group_ids: - group_ids = [group_ids] # cas ou un seul parametre, pas de liste - else: - group_ids = [] - if not formsemestre_id and moduleimpl_id: - mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) - if len(mods) != 1: - raise ValueError("invalid moduleimpl_id") - formsemestre_id = mods[0]["formsemestre_id"] - - if not group_ids: # appel sans groupe (eg page accueil) - if not formsemestre_id: - raise Exception("missing parameter formsemestre_id or group_ids") - if select_all_when_unspecified: - group_ids = [sco_groups.get_default_group(formsemestre_id)] - else: - # selectionne le premier groupe trouvé, s'il y en a un - partition = sco_groups.get_partitions_list( - formsemestre_id, with_default=True - )[0] - groups = sco_groups.get_partition_groups(partition) - if groups: - group_ids = [groups[0]["group_id"]] - else: - group_ids = [sco_groups.get_default_group(formsemestre_id)] - - gq = [] - for group_id in group_ids: - gq.append("group_ids=" + str(group_id)) - self.groups_query_args = "&".join(gq) - self.base_url = request.base_url + "?" + self.groups_query_args - self.group_ids = group_ids - self.groups = [] - groups_titles = [] - self.members = [] - self.tous_les_etuds_du_sem = ( - False # affiche tous les etuds du semestre ? (si un seul semestre) - ) - self.sems = collections.OrderedDict() # formsemestre_id : sem - self.formsemestre = None - self.formsemestre_id = formsemestre_id - self.nbdem = 0 # nombre d'étudiants démissionnaires en tout - sem = None - selected_partitions = set() - for group_id in group_ids: - group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos( - group_id, etat=etat - ) - self.groups.append(group) - self.nbdem += nbdem - self.sems[sem["formsemestre_id"]] = sem - if not self.formsemestre_id: - self.formsemestre_id = sem["formsemestre_id"] - self.formsemestre = sem - self.members.extend(group_members) - groups_titles.append(group_tit) - if group["group_name"] == None: - self.tous_les_etuds_du_sem = True - else: - # liste les partitions explicitement sélectionnés (= des groupes de group_ids) - selected_partitions.add((group["numero"], group["partition_id"])) - - self.selected_partitions = [ - x[1] for x in sorted(list(selected_partitions)) - ] # -> [ partition_id ] - - if not self.formsemestre: # aucun groupe selectionne - self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id) - - self.sortuniq() - - if len(self.sems) > 1: - self.tous_les_etuds_du_sem = False # plusieurs semestres - if self.tous_les_etuds_du_sem: - if sem and sem["semestre_id"] >= 0: - self.groups_titles = "S%d" % sem["semestre_id"] - else: - self.groups_titles = "tous" - self.groups_filename = self.groups_titles - else: - self.groups_titles = ", ".join(groups_titles) - self.groups_filename = "_".join(groups_titles).replace(" ", "_") - # Sanitize filename: - self.groups_filename = scu.make_filename(self.groups_filename) - - # colonnes pour affichages nom des groupes: - # gère le cas où les étudiants appartiennent à des semestres différents - self.partitions = [] # les partitions, sans celle par defaut - for formsemestre_id in self.sems: - for partition in sco_groups.get_partitions_list(formsemestre_id): - if partition["partition_name"]: - self.partitions.append(partition) - - def sortuniq(self): - "Trie les étudiants (de plusieurs groupes) et elimine les doublons" - if (len(self.group_ids) <= 1) or len(self.members) <= 1: - return # on suppose que les etudiants d'un groupe sont deja triés - self.members.sort( - key=operator.itemgetter("nom_disp", "prenom") - ) # tri selon nom_usuel ou nom - to_remove = [] - T = self.members - for i in range(len(T) - 1, 0, -1): - if T[i - 1]["etudid"] == T[i]["etudid"]: - to_remove.append(i) - for i in to_remove: - del T[i] - - def get_form_elem(self): - """html hidden input with groups""" - H = [] - for group_id in self.group_ids: - H.append('' % group_id) - return "\n".join(H) - - -# Ancien ZScolar.group_list renommé ici en group_table -def groups_table( - REQUEST=None, - groups_infos=None, # instance of DisplayedGroupsInfos - with_codes=0, - etat=None, - format="html", - with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) - with_archives=0, # ajoute colonne avec noms fichiers archivés - with_annotations=0, -): - """liste etudiants inscrits dans ce semestre - format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf - Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape - """ - from app.scodoc import sco_report - - # log( - # "enter groups_table %s: %s" - # % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-")) - # ) - with_codes = int(with_codes) - with_paiement = int(with_paiement) - with_archives = int(with_archives) - with_annotations = int(with_annotations) - - base_url_np = groups_infos.base_url + "&with_codes=%s" % with_codes - base_url = ( - base_url_np - + "&with_paiement=%s&with_archives=%s&with_annotations=%s" - % (with_paiement, with_archives, with_annotations) - ) - # - columns_ids = ["civilite_str", "nom_disp", "prenom"] # colonnes a inclure - titles = { - "civilite_str": "Civ.", - "nom_disp": "Nom", - "prenom": "Prénom", - "email": "Mail", - "emailperso": "Personnel", - "etat": "Etat", - "etudid": "etudid", - "code_nip": "code_nip", - "code_ine": "code_ine", - "datefinalisationinscription_str": "Finalisation inscr.", - "paiementinscription_str": "Paiement", - "etudarchive": "Fichiers", - "annotations_str": "Annotations", - "etape": "Etape", - "semestre_groupe": "Semestre-Groupe", # pour Moodle - } - - # ajoute colonnes pour groupes - columns_ids.extend([p["partition_id"] for p in groups_infos.partitions]) - titles.update( - dict( - [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] - ) - ) - partitions_name = { - p["partition_id"]: p["partition_name"] for p in groups_infos.partitions - } - - if format != "html": # ne mentionne l'état que en Excel (style en html) - columns_ids.append("etat") - columns_ids.append("email") - columns_ids.append("emailperso") - - if format == "moodlecsv": - columns_ids = ["email", "semestre_groupe"] - - if with_codes: - columns_ids += ["etape", "etudid", "code_nip", "code_ine"] - if with_paiement: - columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] - if with_paiement or with_codes: - sco_portal_apogee.check_paiement_etuds(groups_infos.members) - if with_archives: - from app.scodoc import sco_archives_etud - - sco_archives_etud.add_archives_info_to_etud_list(groups_infos.members) - columns_ids += ["etudarchive"] - if with_annotations: - sco_etud.add_annotations_to_etud_list(groups_infos.members) - columns_ids += ["annotations_str"] - moodle_sem_name = groups_infos.formsemestre["session_id"] - moodle_groupenames = set() - # ajoute liens - for etud in groups_infos.members: - if etud["email"]: - etud["_email_target"] = "mailto:" + etud["email"] - else: - etud["_email_target"] = "" - if etud["emailperso"]: - etud["_emailperso_target"] = "mailto:" + etud["emailperso"] - else: - etud["_emailperso_target"] = "" - fiche_url = url_for( - "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] - ) - etud["_nom_disp_target"] = fiche_url - etud["_prenom_target"] = fiche_url - - etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) - - if etud["etat"] == "D": - etud["_css_row_class"] = "etuddem" - # et groupes: - for partition_id in etud["partitions"]: - etud[partition_id] = etud["partitions"][partition_id]["group_name"] - # Ajoute colonne pour moodle: semestre_groupe, de la forme RT-DUT-FI-S3-2021-PARTITION-GROUPE - moodle_groupename = [] - if groups_infos.selected_partitions: - # il y a des groupes selectionnes, utilise leurs partitions - for partition_id in groups_infos.selected_partitions: - if partition_id in etud["partitions"]: - moodle_groupename.append( - partitions_name[partition_id] - + "-" - + etud["partitions"][partition_id]["group_name"] - ) - else: - # pas de groupes sélectionnés: prend le premier s'il y en a un - moodle_groupename = ["tous"] - if etud["partitions"]: - for p in etud["partitions"].items(): # partitions is an OrderedDict - moodle_groupename = [ - partitions_name[p[0]] + "-" + p[1]["group_name"] - ] - break - - moodle_groupenames |= set(moodle_groupename) - etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) - - if groups_infos.nbdem > 1: - s = "s" - else: - s = "" - - if format == "moodlecsv": - # de la forme S1-[FI][FA]-groupe.csv - if not moodle_groupenames: - moodle_groupenames = {"tous"} - filename = ( - moodle_sem_name - + "-" - + groups_infos.formsemestre["modalite"] - + "-" - + "+".join(sorted(moodle_groupenames)) - ) - else: - filename = "etudiants_%s" % groups_infos.groups_filename - - prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) - tab = GenTable( - rows=groups_infos.members, - columns_ids=columns_ids, - titles=titles, - caption="soit %d étudiants inscrits et %d démissionaire%s." - % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s), - base_url=base_url, - filename=filename, - pdf_link=False, # pas d'export pdf - html_sortable=True, - html_class="table_leftalign table_listegroupe", - xml_outer_tag="group_list", - xml_row_tag="etud", - text_fields_separator=prefs["moodle_csv_separator"], - text_with_titles=prefs["moodle_csv_with_headerline"], - preferences=prefs, - ) - # - if format == "html": - amail_inst = [ - x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" - ] - amail_perso = [ - x["emailperso"] - for x in groups_infos.members - if x["emailperso"] and x["etat"] != "D" - ] - - if len(groups_infos.members): - if groups_infos.tous_les_etuds_du_sem: - htitle = "Les %d étudiants inscrits" % len(groups_infos.members) - else: - htitle = "Groupe %s (%d étudiants)" % ( - groups_infos.groups_titles, - len(groups_infos.members), - ) - else: - htitle = "Aucun étudiant !" - H = [ - '
    ' '

    ', - htitle, - "", - ] - if groups_infos.members: - Of = [] - options = { - "with_paiement": "Paiement inscription", - "with_archives": "Fichiers archivés", - "with_annotations": "Annotations", - "with_codes": "Codes", - } - for option in options: - if locals().get(option, False): - selected = "selected" - else: - selected = "" - Of.append( - """""" - % (option, selected, options[option]) - ) - - H.extend( - [ - """ - - """, - ] - ) - H.append("

    ") - if groups_infos.members: - H.extend( - [ - tab.html(), - "") - - return "".join(H) + "
    " - - elif ( - format == "pdf" - or format == "xml" - or format == "json" - or format == "xls" - or format == "moodlecsv" - ): - if format == "moodlecsv": - format = "csv" - return tab.make_page(format=format) - - elif format == "xlsappel": - xls = sco_excel.excel_feuille_listeappel( - groups_infos.formsemestre, - groups_infos.groups_titles, - groups_infos.members, - partitions=groups_infos.partitions, - with_codes=with_codes, - with_paiement=with_paiement, - server_name=REQUEST.BASE0, - ) - filename = "liste_%s" % groups_infos.groups_filename + ".xlsx" - return sco_excel.send_excel_file(REQUEST, xls, filename) - elif format == "allxls": - # feuille Excel avec toutes les infos etudiants - if not groups_infos.members: - return "" - keys = [ - "etudid", - "code_nip", - "etat", - "civilite_str", - "nom", - "nom_usuel", - "prenom", - "inscriptionstr", - ] - if with_paiement: - keys.append("paiementinscription") - keys += [ - "email", - "emailperso", - "domicile", - "villedomicile", - "codepostaldomicile", - "paysdomicile", - "telephone", - "telephonemobile", - "fax", - "date_naissance", - "lieu_naissance", - "bac", - "specialite", - "annee_bac", - "nomlycee", - "villelycee", - "codepostallycee", - "codelycee", - "type_admission", - "boursier_prec", - "debouche", - "parcours", - "codeparcours", - ] - titles = keys[:] - other_partitions = sco_groups.get_group_other_partitions(groups_infos.groups[0]) - keys += [p["partition_id"] for p in other_partitions] - titles += [p["partition_name"] for p in other_partitions] - # remplis infos lycee si on a que le code lycée - # et ajoute infos inscription - for m in groups_infos.members: - etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] - m.update(etud) - sco_etud.etud_add_lycee_infos(etud) - # et ajoute le parcours - Se = sco_parcours_dut.SituationEtudParcours( - etud, groups_infos.formsemestre_id - ) - m["parcours"] = Se.get_parcours_descr() - m["codeparcours"], _ = sco_report.get_codeparcoursetud(etud) - - def dicttakestr(d, keys): - r = [] - for k in keys: - r.append(str(d.get(k, ""))) - return r - - L = [dicttakestr(m, keys) for m in groups_infos.members] - title = "etudiants_%s" % groups_infos.groups_filename - xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) - filename = title + scu.XLSX_SUFFIX - return sco_excel.send_excel_file(REQUEST, xls, filename) - else: - raise ValueError("unsupported format") - - -def tab_absences_html(groups_infos, etat=None, REQUEST=None): - """contenu du tab "absences et feuilles diverses" """ - authuser = REQUEST.AUTHENTICATED_USER - H = ['
    '] - if not groups_infos.members: - return "".join(H) + "

    Aucun étudiant !

    " - H.extend( - [ - "

    Absences

    ", - '
      ', - "
    • ", - form_choix_saisie_semaine(groups_infos, REQUEST=REQUEST), # Ajout Le Havre - "
    • ", - "
    • ", - form_choix_jour_saisie_hebdo(groups_infos, REQUEST=REQUEST), - "
    • ", - """
    • État des absences du groupe
    • """ - % ( - groups_infos.groups_query_args, - groups_infos.formsemestre["date_debut"], - groups_infos.formsemestre["date_fin"], - ), - "
    ", - "

    Feuilles

    ", - '", - ] - ) - - H.append('

    Opérations diverses

      ') - # Lien pour verif codes INE/NIP - # (pour tous les etudiants du semestre) - group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) - if authuser.has_permission(Permission.ScoEtudInscrit): - H.append( - '
    • Vérifier codes Apogée (de tous les groupes)
    • ' - % (group_id, etat or "") - ) - # Lien pour ajout fichiers étudiants - if authuser.has_permission(Permission.ScoEtudAddAnnotations): - H.append( - """
    • Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)
    • """ - % (group_id) - ) - - H.append("
    ") - return "".join(H) - - -def tab_photos_html(groups_infos, etat=None, REQUEST=None): - """contenu du tab "photos" """ - from app.scodoc import sco_trombino - - if not groups_infos.members: - return '

    Aucun étudiant !

    ' - - return sco_trombino.trombino_html(groups_infos, REQUEST=REQUEST) - - -def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None, REQUEST=None): - """Formulaire choix jour semaine pour saisie.""" - authuser = REQUEST.AUTHENTICATED_USER - if not authuser.has_permission(Permission.ScoAbsChange): - return "" - sem = groups_infos.formsemestre - first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() - today_idx = datetime.date.today().weekday() - - FA = [] # formulaire avec menu saisi absences - FA.append( - '
    ' - ) - FA.append('' % sem) - FA.append(groups_infos.get_form_elem()) - if moduleimpl_id: - FA.append( - '' % moduleimpl_id - ) - FA.append('') - - FA.append( - """""" - ) - FA.append("""") - FA.append("") - return "\n".join(FA) - - -# Ajout Le Havre -# Formulaire saisie absences semaine -def form_choix_saisie_semaine(groups_infos, REQUEST=None): - authuser = REQUEST.AUTHENTICATED_USER - if not authuser.has_permission(Permission.ScoAbsChange): - return "" - # construit l'URL "destination" - # (a laquelle on revient apres saisie absences) - query_args = parse_qs(REQUEST.QUERY_STRING) - moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] - if "head_message" in query_args: - del query_args["head_message"] - destination = "%s?%s" % ( - REQUEST.URL, - urllib.parse.urlencode(query_args, True), - ) - destination = destination.replace( - "%", "%%" - ) # car ici utilisee dans un format string ! - - DateJour = time.strftime("%d/%m/%Y") - datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() - FA = [] # formulaire avec menu saisie hebdo des absences - FA.append('
    ') - FA.append('' % datelundi) - FA.append('' % moduleimpl_id) - FA.append('' % destination) - FA.append(groups_infos.get_form_elem()) - FA.append('') - FA.append("") - return "\n".join(FA) - - -def export_groups_as_moodle_csv(formsemestre_id=None): - """Export all students/groups, in a CSV format suitable for Moodle - Each (student,group) will be listed on a separate line - jo@univ.fr,S3-A - jo@univ.fr,S3-B1 - if jo belongs to group A in a partition, and B1 in another one. - Caution: if groups in different partitions share the same name, there will be - duplicates... should we prefix the group names with the partition's name ? - """ - if not formsemestre_id: - raise ScoValueError("missing parameter: formsemestre_id") - _, partitions_etud_groups = sco_groups.get_formsemestre_groups( - formsemestre_id, with_default=True - ) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - moodle_sem_name = sem["session_id"] - - columns_ids = ("email", "semestre_groupe") - T = [] - for partition_id in partitions_etud_groups: - partition = sco_groups.get_partition(partition_id) - members = partitions_etud_groups[partition_id] - for etudid in members: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - group_name = members[etudid]["group_name"] - elts = [moodle_sem_name] - if partition["partition_name"]: - elts.append(partition["partition_name"]) - if group_name: - elts.append(group_name) - T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) - # Make table - prefs = sco_preferences.SemPreferences(formsemestre_id) - tab = GenTable( - rows=T, - columns_ids=("email", "semestre_groupe"), - filename=moodle_sem_name + "-moodle", - titles={x: x for x in columns_ids}, - text_fields_separator=prefs["moodle_csv_separator"], - text_with_titles=prefs["moodle_csv_with_headerline"], - preferences=prefs, - ) - return tab.make_page(format="csv") +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Affichage étudiants d'un ou plusieurs groupes + sous forme: de liste html (table exportable), de trombinoscope (exportable en pdf) +""" + +# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) + +import collections +import datetime +import operator +import urllib +from urllib.parse import parse_qs +import time + + +from flask import url_for, g, request + +import app.scodoc.sco_utils as scu +from app.scodoc import html_sco_header +from app.scodoc import sco_abs +from app.scodoc import sco_excel +from app.scodoc import sco_formsemestre +from app.scodoc import sco_groups +from app.scodoc import sco_moduleimpl +from app.scodoc import sco_parcours_dut +from app.scodoc import sco_portal_apogee +from app.scodoc import sco_preferences +from app.scodoc import sco_etud +from app.scodoc.gen_tables import GenTable +from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_permissions import Permission +from six.moves import range + +JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [ + "js/etud_info.js", + "js/groups_view.js", +] + +CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS + + +def groups_view( + group_ids=[], + format="html", + REQUEST=None, + # Options pour listes: + with_codes=0, + etat=None, + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, + formsemestre_id=None, # utilise si aucun groupe selectionné +): + """Affichage des étudiants des groupes indiqués + group_ids: liste de group_id + format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + """ + # Informations sur les groupes à afficher: + groups_infos = DisplayedGroupsInfos( + group_ids, + formsemestre_id=formsemestre_id, + etat=etat, + select_all_when_unspecified=True, + ) + # Formats spéciaux: download direct + if format != "html": + return groups_table( + groups_infos=groups_infos, + format=format, + REQUEST=REQUEST, + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, + with_archives=with_archives, + with_annotations=with_annotations, + ) + + H = [ + html_sco_header.sco_header( + javascripts=JAVASCRIPTS, + cssstyles=CSSSTYLES, + init_qtip=True, + ) + ] + # Menu choix groupe + H.append("""
    """) + H.append(form_groups_choice(groups_infos, submit_on_change=True)) + # Note: le formulaire est soumis a chaque modif des groupes + # on pourrait faire comme pour le form de saisie des notes. Il faudrait pour cela: + # - charger tous les etudiants au debut, quels que soient les groupes selectionnés + # - ajouter du JS pour modifier les liens (arguments group_ids) quand le menu change + + # Tabs + # H.extend( ("""toto
    • item 1
    • item 2
    """,) ) + H.extend( + ( + """ +
    + +
    +
    + """, + groups_table( + groups_infos=groups_infos, + format=format, + REQUEST=REQUEST, + with_codes=with_codes, + etat=etat, + with_paiement=with_paiement, + with_archives=with_archives, + with_annotations=with_annotations, + ), + "
    ", + """
    """, + tab_photos_html(groups_infos, etat=etat, REQUEST=REQUEST), + #'

    hello

    ', + "
    ", + '
    ', + tab_absences_html(groups_infos, etat=etat, REQUEST=REQUEST), + "
    ", + ) + ) + + H.append(html_sco_header.sco_footer()) + return "\n".join(H) + + +def form_groups_choice(groups_infos, with_selectall_butt=False, submit_on_change=False): + """form pour selection groupes + group_ids est la liste des groupes actuellement sélectionnés + et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. + (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + + Si submit_on_change, ajoute une classe "submit_on_change" qui est utilisee en JS + """ + default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + + H = [ + """
    + + + Groupes: + """ + % (groups_infos.formsemestre_id, default_group_id) + ] + + H.append(menu_groups_choice(groups_infos, submit_on_change=submit_on_change)) + + if with_selectall_butt: + H.append( + """""" + ) + H.append("") + + return "\n".join(H) + + +def menu_groups_choice(groups_infos, submit_on_change=False): + """menu pour selection groupes + group_ids est la liste des groupes actuellement sélectionnés + et doit comporter au moins un élément, sauf si formsemestre_id est spécifié. + (utilisé pour retrouver le semestre et proposer la liste des autres groupes) + """ + default_group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + + if submit_on_change: + klass = "submit_on_change" + else: + klass = "" + H = [ + """ ") + return "\n".join(H) + + +def menu_group_choice(group_id=None, formsemestre_id=None): + """Un bête menu pour choisir un seul groupe + group_id est le groupe actuellement sélectionné. + Si aucun groupe selectionné, utilise formsemestre_id pour lister les groupes. + """ + if group_id: + group = sco_groups.get_group(group_id) + formsemestre_id = group["formsemestre_id"] + elif not formsemestre_id: + raise ValueError("missing formsemestre_id") + H = [ + """ + + + """ + ) + return "\n".join(H) + + +class DisplayedGroupsInfos(object): + """Container with attributes describing groups to display in the page + .groups_query_args : 'group_ids=xxx&group_ids=yyy' + .base_url : url de la requete, avec les groupes, sans les autres paramètres + .formsemestre_id : semestre "principal" (en fait celui du 1er groupe de la liste) + .members + .groups_titles + """ + + def __init__( + self, + group_ids=[], # groupes specifies dans l'URL, ou un seul int + formsemestre_id=None, + etat=None, + select_all_when_unspecified=False, + moduleimpl_id=None, # used to find formsemestre when unspecified + ): + if isinstance(group_ids, int): + if group_ids: + group_ids = [group_ids] # cas ou un seul parametre, pas de liste + else: + group_ids = [] + if not formsemestre_id and moduleimpl_id: + mods = sco_moduleimpl.do_moduleimpl_list(moduleimpl_id=moduleimpl_id) + if len(mods) != 1: + raise ValueError("invalid moduleimpl_id") + formsemestre_id = mods[0]["formsemestre_id"] + + if not group_ids: # appel sans groupe (eg page accueil) + if not formsemestre_id: + raise Exception("missing parameter formsemestre_id or group_ids") + if select_all_when_unspecified: + group_ids = [sco_groups.get_default_group(formsemestre_id)] + else: + # selectionne le premier groupe trouvé, s'il y en a un + partition = sco_groups.get_partitions_list( + formsemestre_id, with_default=True + )[0] + groups = sco_groups.get_partition_groups(partition) + if groups: + group_ids = [groups[0]["group_id"]] + else: + group_ids = [sco_groups.get_default_group(formsemestre_id)] + + gq = [] + for group_id in group_ids: + gq.append("group_ids=" + str(group_id)) + self.groups_query_args = "&".join(gq) + self.base_url = request.base_url + "?" + self.groups_query_args + self.group_ids = group_ids + self.groups = [] + groups_titles = [] + self.members = [] + self.tous_les_etuds_du_sem = ( + False # affiche tous les etuds du semestre ? (si un seul semestre) + ) + self.sems = collections.OrderedDict() # formsemestre_id : sem + self.formsemestre = None + self.formsemestre_id = formsemestre_id + self.nbdem = 0 # nombre d'étudiants démissionnaires en tout + sem = None + selected_partitions = set() + for group_id in group_ids: + group_members, group, group_tit, sem, nbdem = sco_groups.get_group_infos( + group_id, etat=etat + ) + self.groups.append(group) + self.nbdem += nbdem + self.sems[sem["formsemestre_id"]] = sem + if not self.formsemestre_id: + self.formsemestre_id = sem["formsemestre_id"] + self.formsemestre = sem + self.members.extend(group_members) + groups_titles.append(group_tit) + if group["group_name"] == None: + self.tous_les_etuds_du_sem = True + else: + # liste les partitions explicitement sélectionnés (= des groupes de group_ids) + selected_partitions.add((group["numero"], group["partition_id"])) + + self.selected_partitions = [ + x[1] for x in sorted(list(selected_partitions)) + ] # -> [ partition_id ] + + if not self.formsemestre: # aucun groupe selectionne + self.formsemestre = sco_formsemestre.get_formsemestre(formsemestre_id) + + self.sortuniq() + + if len(self.sems) > 1: + self.tous_les_etuds_du_sem = False # plusieurs semestres + if self.tous_les_etuds_du_sem: + if sem and sem["semestre_id"] >= 0: + self.groups_titles = "S%d" % sem["semestre_id"] + else: + self.groups_titles = "tous" + self.groups_filename = self.groups_titles + else: + self.groups_titles = ", ".join(groups_titles) + self.groups_filename = "_".join(groups_titles).replace(" ", "_") + # Sanitize filename: + self.groups_filename = scu.make_filename(self.groups_filename) + + # colonnes pour affichages nom des groupes: + # gère le cas où les étudiants appartiennent à des semestres différents + self.partitions = [] # les partitions, sans celle par defaut + for formsemestre_id in self.sems: + for partition in sco_groups.get_partitions_list(formsemestre_id): + if partition["partition_name"]: + self.partitions.append(partition) + + def sortuniq(self): + "Trie les étudiants (de plusieurs groupes) et elimine les doublons" + if (len(self.group_ids) <= 1) or len(self.members) <= 1: + return # on suppose que les etudiants d'un groupe sont deja triés + self.members.sort( + key=operator.itemgetter("nom_disp", "prenom") + ) # tri selon nom_usuel ou nom + to_remove = [] + T = self.members + for i in range(len(T) - 1, 0, -1): + if T[i - 1]["etudid"] == T[i]["etudid"]: + to_remove.append(i) + for i in to_remove: + del T[i] + + def get_form_elem(self): + """html hidden input with groups""" + H = [] + for group_id in self.group_ids: + H.append('' % group_id) + return "\n".join(H) + + +# Ancien ZScolar.group_list renommé ici en group_table +def groups_table( + REQUEST=None, + groups_infos=None, # instance of DisplayedGroupsInfos + with_codes=0, + etat=None, + format="html", + with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) + with_archives=0, # ajoute colonne avec noms fichiers archivés + with_annotations=0, +): + """liste etudiants inscrits dans ce semestre + format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf + Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape + """ + from app.scodoc import sco_report + + # log( + # "enter groups_table %s: %s" + # % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-")) + # ) + with_codes = int(with_codes) + with_paiement = int(with_paiement) + with_archives = int(with_archives) + with_annotations = int(with_annotations) + + base_url_np = groups_infos.base_url + "&with_codes=%s" % with_codes + base_url = ( + base_url_np + + "&with_paiement=%s&with_archives=%s&with_annotations=%s" + % (with_paiement, with_archives, with_annotations) + ) + # + columns_ids = ["civilite_str", "nom_disp", "prenom"] # colonnes a inclure + titles = { + "civilite_str": "Civ.", + "nom_disp": "Nom", + "prenom": "Prénom", + "email": "Mail", + "emailperso": "Personnel", + "etat": "Etat", + "etudid": "etudid", + "code_nip": "code_nip", + "code_ine": "code_ine", + "datefinalisationinscription_str": "Finalisation inscr.", + "paiementinscription_str": "Paiement", + "etudarchive": "Fichiers", + "annotations_str": "Annotations", + "etape": "Etape", + "semestre_groupe": "Semestre-Groupe", # pour Moodle + } + + # ajoute colonnes pour groupes + columns_ids.extend([p["partition_id"] for p in groups_infos.partitions]) + titles.update( + dict( + [(p["partition_id"], p["partition_name"]) for p in groups_infos.partitions] + ) + ) + partitions_name = { + p["partition_id"]: p["partition_name"] for p in groups_infos.partitions + } + + if format != "html": # ne mentionne l'état que en Excel (style en html) + columns_ids.append("etat") + columns_ids.append("email") + columns_ids.append("emailperso") + + if format == "moodlecsv": + columns_ids = ["email", "semestre_groupe"] + + if with_codes: + columns_ids += ["etape", "etudid", "code_nip", "code_ine"] + if with_paiement: + columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] + if with_paiement or with_codes: + sco_portal_apogee.check_paiement_etuds(groups_infos.members) + if with_archives: + from app.scodoc import sco_archives_etud + + sco_archives_etud.add_archives_info_to_etud_list(groups_infos.members) + columns_ids += ["etudarchive"] + if with_annotations: + sco_etud.add_annotations_to_etud_list(groups_infos.members) + columns_ids += ["annotations_str"] + moodle_sem_name = groups_infos.formsemestre["session_id"] + moodle_groupenames = set() + # ajoute liens + for etud in groups_infos.members: + if etud["email"]: + etud["_email_target"] = "mailto:" + etud["email"] + else: + etud["_email_target"] = "" + if etud["emailperso"]: + etud["_emailperso_target"] = "mailto:" + etud["emailperso"] + else: + etud["_emailperso_target"] = "" + fiche_url = url_for( + "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] + ) + etud["_nom_disp_target"] = fiche_url + etud["_prenom_target"] = fiche_url + + etud["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"]) + + if etud["etat"] == "D": + etud["_css_row_class"] = "etuddem" + # et groupes: + for partition_id in etud["partitions"]: + etud[partition_id] = etud["partitions"][partition_id]["group_name"] + # Ajoute colonne pour moodle: semestre_groupe, de la forme RT-DUT-FI-S3-2021-PARTITION-GROUPE + moodle_groupename = [] + if groups_infos.selected_partitions: + # il y a des groupes selectionnes, utilise leurs partitions + for partition_id in groups_infos.selected_partitions: + if partition_id in etud["partitions"]: + moodle_groupename.append( + partitions_name[partition_id] + + "-" + + etud["partitions"][partition_id]["group_name"] + ) + else: + # pas de groupes sélectionnés: prend le premier s'il y en a un + moodle_groupename = ["tous"] + if etud["partitions"]: + for p in etud["partitions"].items(): # partitions is an OrderedDict + moodle_groupename = [ + partitions_name[p[0]] + "-" + p[1]["group_name"] + ] + break + + moodle_groupenames |= set(moodle_groupename) + etud["semestre_groupe"] = moodle_sem_name + "-" + "+".join(moodle_groupename) + + if groups_infos.nbdem > 1: + s = "s" + else: + s = "" + + if format == "moodlecsv": + # de la forme S1-[FI][FA]-groupe.csv + if not moodle_groupenames: + moodle_groupenames = {"tous"} + filename = ( + moodle_sem_name + + "-" + + groups_infos.formsemestre["modalite"] + + "-" + + "+".join(sorted(moodle_groupenames)) + ) + else: + filename = "etudiants_%s" % groups_infos.groups_filename + + prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id) + tab = GenTable( + rows=groups_infos.members, + columns_ids=columns_ids, + titles=titles, + caption="soit %d étudiants inscrits et %d démissionaire%s." + % (len(groups_infos.members) - groups_infos.nbdem, groups_infos.nbdem, s), + base_url=base_url, + filename=filename, + pdf_link=False, # pas d'export pdf + html_sortable=True, + html_class="table_leftalign table_listegroupe", + xml_outer_tag="group_list", + xml_row_tag="etud", + text_fields_separator=prefs["moodle_csv_separator"], + text_with_titles=prefs["moodle_csv_with_headerline"], + preferences=prefs, + ) + # + if format == "html": + amail_inst = [ + x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" + ] + amail_perso = [ + x["emailperso"] + for x in groups_infos.members + if x["emailperso"] and x["etat"] != "D" + ] + + if len(groups_infos.members): + if groups_infos.tous_les_etuds_du_sem: + htitle = "Les %d étudiants inscrits" % len(groups_infos.members) + else: + htitle = "Groupe %s (%d étudiants)" % ( + groups_infos.groups_titles, + len(groups_infos.members), + ) + else: + htitle = "Aucun étudiant !" + H = [ + '
    ' '

    ', + htitle, + "", + ] + if groups_infos.members: + Of = [] + options = { + "with_paiement": "Paiement inscription", + "with_archives": "Fichiers archivés", + "with_annotations": "Annotations", + "with_codes": "Codes", + } + for option in options: + if locals().get(option, False): + selected = "selected" + else: + selected = "" + Of.append( + """""" + % (option, selected, options[option]) + ) + + H.extend( + [ + """ + + """, + ] + ) + H.append("

    ") + if groups_infos.members: + H.extend( + [ + tab.html(), + "") + + return "".join(H) + "
    " + + elif ( + format == "pdf" + or format == "xml" + or format == "json" + or format == "xls" + or format == "moodlecsv" + ): + if format == "moodlecsv": + format = "csv" + return tab.make_page(format=format) + + elif format == "xlsappel": + xls = sco_excel.excel_feuille_listeappel( + groups_infos.formsemestre, + groups_infos.groups_titles, + groups_infos.members, + partitions=groups_infos.partitions, + with_codes=with_codes, + with_paiement=with_paiement, + server_name=REQUEST.BASE0, + ) + filename = "liste_%s" % groups_infos.groups_filename + ".xlsx" + breakpoint() + return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, xls, filename) + elif format == "allxls": + # feuille Excel avec toutes les infos etudiants + if not groups_infos.members: + return "" + keys = [ + "etudid", + "code_nip", + "etat", + "civilite_str", + "nom", + "nom_usuel", + "prenom", + "inscriptionstr", + ] + if with_paiement: + keys.append("paiementinscription") + keys += [ + "email", + "emailperso", + "domicile", + "villedomicile", + "codepostaldomicile", + "paysdomicile", + "telephone", + "telephonemobile", + "fax", + "date_naissance", + "lieu_naissance", + "bac", + "specialite", + "annee_bac", + "nomlycee", + "villelycee", + "codepostallycee", + "codelycee", + "type_admission", + "boursier_prec", + "debouche", + "parcours", + "codeparcours", + ] + titles = keys[:] + other_partitions = sco_groups.get_group_other_partitions(groups_infos.groups[0]) + keys += [p["partition_id"] for p in other_partitions] + titles += [p["partition_name"] for p in other_partitions] + # remplis infos lycee si on a que le code lycée + # et ajoute infos inscription + for m in groups_infos.members: + etud = sco_etud.get_etud_info(m["etudid"], filled=True)[0] + m.update(etud) + sco_etud.etud_add_lycee_infos(etud) + # et ajoute le parcours + Se = sco_parcours_dut.SituationEtudParcours( + etud, groups_infos.formsemestre_id + ) + m["parcours"] = Se.get_parcours_descr() + m["codeparcours"], _ = sco_report.get_codeparcoursetud(etud) + + def dicttakestr(d, keys): + r = [] + for k in keys: + r.append(str(d.get(k, ""))) + return r + + L = [dicttakestr(m, keys) for m in groups_infos.members] + title = "etudiants_%s" % groups_infos.groups_filename + xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) + filename = title + scu.XLSX_SUFFIX + breakpoint() + return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, xls, filename) + else: + raise ValueError("unsupported format") + + +def tab_absences_html(groups_infos, etat=None, REQUEST=None): + """contenu du tab "absences et feuilles diverses" """ + authuser = REQUEST.AUTHENTICATED_USER + H = ['
    '] + if not groups_infos.members: + return "".join(H) + "

    Aucun étudiant !

    " + H.extend( + [ + "

    Absences

    ", + '
      ', + "
    • ", + form_choix_saisie_semaine(groups_infos, REQUEST=REQUEST), # Ajout Le Havre + "
    • ", + "
    • ", + form_choix_jour_saisie_hebdo(groups_infos, REQUEST=REQUEST), + "
    • ", + """
    • État des absences du groupe
    • """ + % ( + groups_infos.groups_query_args, + groups_infos.formsemestre["date_debut"], + groups_infos.formsemestre["date_fin"], + ), + "
    ", + "

    Feuilles

    ", + '", + ] + ) + + H.append('

    Opérations diverses

      ') + # Lien pour verif codes INE/NIP + # (pour tous les etudiants du semestre) + group_id = sco_groups.get_default_group(groups_infos.formsemestre_id) + if authuser.has_permission(Permission.ScoEtudInscrit): + H.append( + '
    • Vérifier codes Apogée (de tous les groupes)
    • ' + % (group_id, etat or "") + ) + # Lien pour ajout fichiers étudiants + if authuser.has_permission(Permission.ScoEtudAddAnnotations): + H.append( + """
    • Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)
    • """ + % (group_id) + ) + + H.append("
    ") + return "".join(H) + + +def tab_photos_html(groups_infos, etat=None, REQUEST=None): + """contenu du tab "photos" """ + from app.scodoc import sco_trombino + + if not groups_infos.members: + return '

    Aucun étudiant !

    ' + + return sco_trombino.trombino_html(groups_infos, REQUEST=REQUEST) + + +def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None, REQUEST=None): + """Formulaire choix jour semaine pour saisie.""" + authuser = REQUEST.AUTHENTICATED_USER + if not authuser.has_permission(Permission.ScoAbsChange): + return "" + sem = groups_infos.formsemestre + first_monday = sco_abs.ddmmyyyy(sem["date_debut"]).prev_monday() + today_idx = datetime.date.today().weekday() + + FA = [] # formulaire avec menu saisi absences + FA.append( + '
    ' + ) + FA.append('' % sem) + FA.append(groups_infos.get_form_elem()) + if moduleimpl_id: + FA.append( + '' % moduleimpl_id + ) + FA.append('') + + FA.append( + """""" + ) + FA.append("""") + FA.append("") + return "\n".join(FA) + + +# Ajout Le Havre +# Formulaire saisie absences semaine +def form_choix_saisie_semaine(groups_infos, REQUEST=None): + authuser = REQUEST.AUTHENTICATED_USER + if not authuser.has_permission(Permission.ScoAbsChange): + return "" + # construit l'URL "destination" + # (a laquelle on revient apres saisie absences) + query_args = parse_qs(REQUEST.QUERY_STRING) + moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] + if "head_message" in query_args: + del query_args["head_message"] + destination = "%s?%s" % ( + REQUEST.URL, + urllib.parse.urlencode(query_args, True), + ) + destination = destination.replace( + "%", "%%" + ) # car ici utilisee dans un format string ! + + DateJour = time.strftime("%d/%m/%Y") + datelundi = sco_abs.ddmmyyyy(DateJour).prev_monday() + FA = [] # formulaire avec menu saisie hebdo des absences + FA.append('
    ') + FA.append('' % datelundi) + FA.append('' % moduleimpl_id) + FA.append('' % destination) + FA.append(groups_infos.get_form_elem()) + FA.append('') + FA.append("") + return "\n".join(FA) + + +def export_groups_as_moodle_csv(formsemestre_id=None): + """Export all students/groups, in a CSV format suitable for Moodle + Each (student,group) will be listed on a separate line + jo@univ.fr,S3-A + jo@univ.fr,S3-B1 + if jo belongs to group A in a partition, and B1 in another one. + Caution: if groups in different partitions share the same name, there will be + duplicates... should we prefix the group names with the partition's name ? + """ + if not formsemestre_id: + raise ScoValueError("missing parameter: formsemestre_id") + _, partitions_etud_groups = sco_groups.get_formsemestre_groups( + formsemestre_id, with_default=True + ) + sem = sco_formsemestre.get_formsemestre(formsemestre_id) + moodle_sem_name = sem["session_id"] + + columns_ids = ("email", "semestre_groupe") + T = [] + for partition_id in partitions_etud_groups: + partition = sco_groups.get_partition(partition_id) + members = partitions_etud_groups[partition_id] + for etudid in members: + etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + group_name = members[etudid]["group_name"] + elts = [moodle_sem_name] + if partition["partition_name"]: + elts.append(partition["partition_name"]) + if group_name: + elts.append(group_name) + T.append({"email": etud["email"], "semestre_groupe": "-".join(elts)}) + # Make table + prefs = sco_preferences.SemPreferences(formsemestre_id) + tab = GenTable( + rows=T, + columns_ids=("email", "semestre_groupe"), + filename=moodle_sem_name + "-moodle", + titles={x: x for x in columns_ids}, + text_fields_separator=prefs["moodle_csv_separator"], + text_with_titles=prefs["moodle_csv_with_headerline"], + preferences=prefs, + ) + return tab.make_page(format="csv") diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index c20d11034e..d0efe74458 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -322,5 +322,7 @@ def feuille_preparation_jury(formsemestre_id, REQUEST): REQUEST.AUTHENTICATED_USER, ) ) - xls = ws.generate_standalone() - return sco_excel.send_excel_file(REQUEST, xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}") + xls = ws.generate() + breakpoint() + return scu.send_file(xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}") diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 3cd47d5416..4691c5a003 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -829,7 +829,9 @@ def feuille_saisie_notes(evaluation_id, group_ids=[], REQUEST=None): filename = "notes_%s_%s.xlsx" % (evalname, gr_title_filename) xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) - return sco_excel.send_excel_file(REQUEST, xls, filename) + breakpoint() + return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, xls, filename) def has_existing_decision(M, E, etudid): diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index a0f858b0f0..6edd01854d 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -486,7 +486,9 @@ def photos_generate_excel_sample(group_ids=[], REQUEST=None): ], extra_cols=["fichier_photo"], ) - return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX) + breakpoint() + return scu.send_file(data, "ImportPhotos", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX) def photos_import_files_form(group_ids=[], REQUEST=None): diff --git a/app/views/scolar.py b/app/views/scolar.py index 043683f7a7..3dd12ca4ae 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1937,7 +1937,9 @@ def import_generate_excel_sample(REQUEST, with_codesemestre="1"): data = sco_import_etuds.sco_import_generate_excel_sample( format, with_codesemestre, exclude_cols=["photo_filename"] ) - return sco_excel.send_excel_file(REQUEST, data, "ImportEtudiants" + scu.XLSX_SUFFIX) + breakpoint() + return scu.send_file(data, "ImportEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, data, "ImportEtudiants" + scu.XLSX_SUFFIX) # --- Données admission @@ -1955,9 +1957,9 @@ def import_generate_admission_sample(REQUEST, formsemestre_id): exclude_cols=["nationalite", "foto", "photo_filename"], group_ids=[group["group_id"]], ) - return sco_excel.send_excel_file( - REQUEST, data, "AdmissionEtudiants" + scu.XLSX_SUFFIX - ) + breakpoint() + return scu.send_file(data, "AdmissionEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, data, "AdmissionEtudiants" + scu.XLSX_SUFFIX) # --- Données admission depuis fichier excel (version nov 2016) diff --git a/app/views/users.py b/app/views/users.py index 17e9987fe2..cde830a9ce 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -65,8 +65,6 @@ from app import log from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_permissions_check import can_handle_passwd from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message -from app.scodoc.sco_excel import send_excel_file -from app.scodoc.sco_import_users import generate_excel_sample from app.views import users_bp as bp @@ -490,9 +488,9 @@ def create_user_form(REQUEST, user_name=None, edit=0, all_roles=1): def import_users_generate_excel_sample(REQUEST): "une feuille excel pour importation utilisateurs" data = sco_import_users.generate_excel_sample() - return sco_excel.send_excel_file( - REQUEST, data, "ImportUtilisateurs" + scu.XLSX_SUFFIX - ) + breakpoint() + return scu.send_file(data, "ImportUtilisateurs", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + # return sco_excel.send_excel_file(REQUEST, data, "ImportUtilisateurs" + scu.XLSX_SUFFIX) @bp.route("/import_users_form", methods=["GET", "POST"]) From 2519d08e40661e5eb19a197510cacd16185defd0 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 18 Sep 2021 10:11:46 +0200 Subject: [PATCH 20/22] remove breakpoints() + blackify --- app/scodoc/sco_archives_etud.py | 9 +++++++-- app/scodoc/sco_groups_view.py | 10 ++++++---- app/scodoc/sco_prepajury.py | 9 +++++++-- app/scodoc/sco_saisie_notes.py | 5 +++-- app/scodoc/sco_trombino.py | 5 +++-- app/views/scolar.py | 2 -- app/views/users.py | 1 - 7 files changed, 26 insertions(+), 15 deletions(-) diff --git a/app/scodoc/sco_archives_etud.py b/app/scodoc/sco_archives_etud.py index 49432a6e6b..d2482a8f15 100644 --- a/app/scodoc/sco_archives_etud.py +++ b/app/scodoc/sco_archives_etud.py @@ -269,8 +269,13 @@ def etudarchive_generate_excel_sample(group_id=None, REQUEST=None): ], extra_cols=["fichier_a_charger"], ) - breakpoint() - return scu.send_file(data, "ImportFichiersEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + data, + "ImportFichiersEtudiants", + scu.XLSX_SUFFIX, + scu.XLSX_MIMETYPE, + attached=True, + ) # return sco_excel.send_excel_file(REQUEST, data, "ImportFichiersEtudiants" + scu.XLSX_SUFFIX) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index 67f8c9da7a..219f103c8b 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -722,8 +722,9 @@ def groups_table( server_name=REQUEST.BASE0, ) filename = "liste_%s" % groups_infos.groups_filename + ".xlsx" - breakpoint() - return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, xls, filename) elif format == "allxls": # feuille Excel avec toutes les infos etudiants @@ -793,8 +794,9 @@ def groups_table( title = "etudiants_%s" % groups_infos.groups_filename xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) filename = title + scu.XLSX_SUFFIX - breakpoint() - return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, xls, filename) else: raise ValueError("unsupported format") diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index d0efe74458..3e4559a4fc 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -323,6 +323,11 @@ def feuille_preparation_jury(formsemestre_id, REQUEST): ) ) xls = ws.generate() - breakpoint() - return scu.send_file(xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + xls, + f"PrepaJury{sn}{scu.XLSX_SUFFIX}", + scu.XLSX_SUFFIX, + scu.XLSX_MIMETYPE, + attached=True, + ) # return sco_excel.send_excel_file(REQUEST, xls, f"PrepaJury{sn}{scu.XLSX_SUFFIX}") diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index 4691c5a003..dc50a1ae44 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -829,8 +829,9 @@ def feuille_saisie_notes(evaluation_id, group_ids=[], REQUEST=None): filename = "notes_%s_%s.xlsx" % (evalname, gr_title_filename) xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) - breakpoint() - return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, xls, filename) diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 6edd01854d..89b3935e6a 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -486,8 +486,9 @@ def photos_generate_excel_sample(group_ids=[], REQUEST=None): ], extra_cols=["fichier_photo"], ) - breakpoint() - return scu.send_file(data, "ImportPhotos", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + data, "ImportPhotos", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, data, "ImportPhotos" + scu.XLSX_SUFFIX) diff --git a/app/views/scolar.py b/app/views/scolar.py index 3dd12ca4ae..781b966fdb 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1937,7 +1937,6 @@ def import_generate_excel_sample(REQUEST, with_codesemestre="1"): data = sco_import_etuds.sco_import_generate_excel_sample( format, with_codesemestre, exclude_cols=["photo_filename"] ) - breakpoint() return scu.send_file(data, "ImportEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) # return sco_excel.send_excel_file(REQUEST, data, "ImportEtudiants" + scu.XLSX_SUFFIX) @@ -1957,7 +1956,6 @@ def import_generate_admission_sample(REQUEST, formsemestre_id): exclude_cols=["nationalite", "foto", "photo_filename"], group_ids=[group["group_id"]], ) - breakpoint() return scu.send_file(data, "AdmissionEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) # return sco_excel.send_excel_file(REQUEST, data, "AdmissionEtudiants" + scu.XLSX_SUFFIX) diff --git a/app/views/users.py b/app/views/users.py index cde830a9ce..e69b02a7d0 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -488,7 +488,6 @@ def create_user_form(REQUEST, user_name=None, edit=0, all_roles=1): def import_users_generate_excel_sample(REQUEST): "une feuille excel pour importation utilisateurs" data = sco_import_users.generate_excel_sample() - breakpoint() return scu.send_file(data, "ImportUtilisateurs", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) # return sco_excel.send_excel_file(REQUEST, data, "ImportUtilisateurs" + scu.XLSX_SUFFIX) From c955870e1ece7c2923e887178384eb48efed115e Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 18 Sep 2021 10:51:19 +0200 Subject: [PATCH 21/22] recheck --- app/scodoc/sco_groups_view.py | 5 ++++- app/views/scolar.py | 8 ++++++-- app/views/users.py | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_groups_view.py b/app/scodoc/sco_groups_view.py index f1962212cf..82c137e2de 100644 --- a/app/scodoc/sco_groups_view.py +++ b/app/scodoc/sco_groups_view.py @@ -794,7 +794,10 @@ def groups_table( title = "etudiants_%s" % groups_infos.groups_filename xls = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title) filename = title + scu.XLSX_SUFFIX - return sco_excel.send_excel_file(REQUEST, xls, filename) + return scu.send_file( + xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) + # return sco_excel.send_excel_file(REQUEST, xls, filename) else: raise ValueError("unsupported format") diff --git a/app/views/scolar.py b/app/views/scolar.py index e57e0a01bd..24d03bc40a 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -1937,7 +1937,9 @@ def import_generate_excel_sample(REQUEST, with_codesemestre="1"): data = sco_import_etuds.sco_import_generate_excel_sample( format, with_codesemestre, exclude_cols=["photo_filename"] ) - return scu.send_file(data, "ImportEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + data, "ImportEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, data, "ImportEtudiants" + scu.XLSX_SUFFIX) @@ -1956,7 +1958,9 @@ def import_generate_admission_sample(REQUEST, formsemestre_id): exclude_cols=["nationalite", "foto", "photo_filename"], group_ids=[group["group_id"]], ) - return scu.send_file(data, "AdmissionEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + data, "AdmissionEtudiants", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, data, "AdmissionEtudiants" + scu.XLSX_SUFFIX) diff --git a/app/views/users.py b/app/views/users.py index c9f680d617..9726efd174 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -488,7 +488,9 @@ def create_user_form(REQUEST, user_name=None, edit=0, all_roles=1): def import_users_generate_excel_sample(REQUEST): "une feuille excel pour importation utilisateurs" data = sco_import_users.generate_excel_sample() - return scu.send_file(data, "ImportUtilisateurs", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True) + return scu.send_file( + data, "ImportUtilisateurs", scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE, attached=True + ) # return sco_excel.send_excel_file(REQUEST, data, "ImportUtilisateurs" + scu.XLSX_SUFFIX) From f7961a135a56b3230c4c2881a2c1699b90adbec9 Mon Sep 17 00:00:00 2001 From: Place Jean-Marie Date: Sat, 18 Sep 2021 11:08:04 +0200 Subject: [PATCH 22/22] finalisation --- app/scodoc/sco_excel.py | 24 ++++-------------------- app/scodoc/sco_placement.py | 9 ++------- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 3533dc0630..1e70858aab 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -59,22 +59,6 @@ class COLORS(Enum): LIGHT_YELLOW = "FFFFFF99" -# def send_excel_file(request, data, filename, mime=scu.XLSX_MIMETYPE): -# """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", mime) -# 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) @@ -351,10 +335,10 @@ class ScoExcelSheet: 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.column_dimensions.keys(): + self.ws.column_dimensions[row] = self.column_dimensions[row] + for row in self.row_dimensions.keys(): + self.ws.row_dimensions[row] = self.row_dimensions[row] for row in self.rows: self.ws.append(row) diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index d0e19c673b..ac12fcb0dd 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -34,10 +34,6 @@ import random import time from copy import copy -import flask -from flask import request - -from app.scodoc.sco_exceptions import ScoValueError import wtforms.validators from flask import request, render_template from flask_login import current_user @@ -50,7 +46,6 @@ from wtforms import ( RadioField, HiddenField, SelectMultipleField, - BooleanField, ) import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -69,8 +64,8 @@ from app.scodoc.gen_tables import GenTable from app.scodoc import sco_etud import sco_version -# _ = lambda x: x # sans babel -# _l = _ +_ = lambda x: x # sans babel +_l = _ COORD = "Coordonnées" SEQ = "Continue"