fin fusion

This commit is contained in:
Jean-Marie Place 2021-09-05 14:50:35 +02:00
parent dccae56fe7
commit 770ccb4d6e
4 changed files with 612 additions and 473 deletions

View File

@ -40,10 +40,15 @@ from openpyxl.cell import WriteOnlyCell
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL
from openpyxl.comments import Comment 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 import app.scodoc.sco_utils as scu
from app.scodoc import notesdb from app.scodoc import notesdb
from app.scodoc import sco_preferences 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 import log
from app.scodoc.sco_exceptions import ScoValueError 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) # 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.
# <br>Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time;
# a datetime.time object will be returned.
# <br>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): def xldate_as_datetime(xldate, datemode=0):
return openpyxl.utils.datetime.from_ISO8601(xldate) return openpyxl.utils.datetime.from_ISO8601(xldate)
# if datemode not in (0, 1):
# raise ValueError("invalid mode %s" % datemode)
# if xldate == 0.00: def adjust_sheetname(sheet_name):
# return datetime.time(0, 0, 0) # Le nom de la feuille ne peut faire plus de 31 caractères.
# if xldate < 0.00: # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
# raise ValueError("invalid date code %s" % xldate) return sheet_name[:31]
# xldays = int(xldate)
# frac = xldate - xldays
# seconds = int(round(frac * 86400.0))
# assert 0 <= seconds <= 86400
# if seconds == 86400:
# seconds = 0
# xldays += 1
# if xldays >= _XLDAYS_TOO_LARGE[datemode]:
# raise ValueError("date too large %s" % xldate)
#
# if xldays == 0:
# # second = seconds % 60; minutes = seconds // 60
# minutes, second = divmod(seconds, 60)
# # minute = minutes % 60; hour = minutes // 60
# hour, minute = divmod(minutes, 60)
# return datetime.time(hour, minute, second)
#
# if xldays < 61 and datemode == 0:
# raise ValueError("ambiguous date %s" % xldate)
#
# return datetime.datetime.fromordinal(
# xldays + 693594 + 1462 * datemode
# ) + datetime.timedelta(seconds=seconds)
class ScoExcelBook: class ScoExcelBook:
@ -139,13 +106,16 @@ class ScoExcelBook:
def __init__(self): def __init__(self):
self.sheets = [] # list of sheets self.sheets = [] # list of sheets
self.wb = Workbook(write_only=True)
def create_sheet(self, sheet_name="feuille", default_style=None): def create_sheet(self, sheet_name="feuille", default_style=None):
"""Crée une nouvelle feuille dans ce classeur """Crée une nouvelle feuille dans ce classeur
sheet_name -- le nom de la feuille sheet_name -- le nom de la feuille
default_style -- le style par défaut 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) self.sheets.append(sheet)
return sheet return sheet
@ -153,12 +123,11 @@ class ScoExcelBook:
"""génération d'un stream binaire représentant la totalité du classeur. """génération d'un stream binaire représentant la totalité du classeur.
retourne le flux retourne le flux
""" """
wb = Workbook(write_only=True)
for sheet in self.sheets: 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) # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
with NamedTemporaryFile() as tmp: with NamedTemporaryFile() as tmp:
wb.save(tmp.name) self.wb.save(tmp.name)
tmp.seek(0) tmp.seek(0)
return tmp.read() return tmp.read()
@ -166,6 +135,7 @@ class ScoExcelBook:
def excel_make_style( def excel_make_style(
bold=False, bold=False,
italic=False, italic=False,
outline=False,
color: COLORS = COLORS.BLACK, color: COLORS = COLORS.BLACK,
bgcolor: COLORS = None, bgcolor: COLORS = None,
halign=None, halign=None,
@ -186,7 +156,14 @@ def excel_make_style(
size -- taille de police size -- taille de police
""" """
style = {} 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 style["font"] = font
if bgcolor: if bgcolor:
style["fill"] = PatternFill(fill_type="solid", fgColor=bgcolor.value) 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 * pour finit appel de la méthode de génération
""" """
def __init__(self, sheet_name="feuille", default_style=None, wb=None): def __init__(self, sheet_name="feuille", default_style=None, ws=None):
"""Création de la feuille. """Création de la feuille. sheet_name
sheet_name -- le nom de la feuille -- le nom de la feuille default_style
default_style -- le style par défaut des cellules -- le style par défaut des cellules ws
wb -- le WorkBook dans laquelle se trouve la feuille. Si wb est None (cas d'un classeur mono-feuille), -- None si la feuille est autonome (dans ce cas ell crée son propre wb), sinon c'est la worksheet
un workbook est crée et associé à cette feuille. 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. # 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' ?) # si la taille du nom de feuille est > 31 on tronque (on pourrait remplacer par 'feuille' ?)
self.sheet_name = sheet_name[ self.sheet_name = adjust_sheetname(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 }
if default_style is None: if default_style is None:
default_style = excel_make_style() default_style = excel_make_style()
self.default_style = default_style self.default_style = default_style
self.wb = wb or Workbook(write_only=True) # Création de workbook si nécessaire if ws is None:
self.ws = self.wb.create_sheet(title=self.sheet_name) 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.column_dimensions = {}
self.row_dimensions = {}
def set_column_dimension_width(self, cle, value): def excel_make_composite_style(
"""Détermine la largeur d'une colonne. self,
cle -- identifie la colonne ("A"n "B", ...) alignment=None,
value -- la dimension (unité : 7 pixels comme affiché dans Excel) 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): def set_row_dimension_height(self, cle=None, value=21):
"""Masque ou affiche une colonne. """Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
cle -- identifie la colonne ("A"n "B", ...) 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) 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): def make_cell(self, value: any = None, style=None, comment=None):
"""Construit une cellule. """Construit une cellule.
@ -271,8 +300,12 @@ class ScoExcelSheet:
style = self.default_style style = self.default_style
if "font" in style: if "font" in style:
cell.font = style["font"] cell.font = style["font"]
if "alignment" in style:
cell.alignment = style["alignment"]
if "border" in style: if "border" in style:
cell.border = style["border"] cell.border = style["border"]
if "fill" in style:
cell.fill = style["fill"]
if "number_format" in style: if "number_format" in style:
cell.number_format = style["number_format"] cell.number_format = style["number_format"]
if "fill" in style: if "fill" in style:
@ -311,73 +344,31 @@ class ScoExcelSheet:
"""ajoute une ligne déjà construite à la feuille.""" """ajoute une ligne déjà construite à la feuille."""
self.rows.append(row) self.rows.append(row)
# def set_style(self, style=None, li=None, co=None): def prepare(self):
# if li is not None and co is not None:
# self.cells_styles_lico[(li, co)] = style
# elif li is None:
# self.cells_styles_li[li] = style
# elif co is None:
# self.cells_styles_co[co] = style
#
# def get_cell_style(self, li, co):
# """Get style for specified cell"""
# return (
# self.cells_styles_lico.get((li, co), None)
# or self.cells_styles_li.get(li, None)
# or self.cells_styles_co.get(co, None)
# or self.default_style
# )
def _generate_ws(self):
"""génére un flux décrivant la feuille. """génére un flux décrivant la feuille.
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille) Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
ou pour la génération d'un classeur multi-feuilles ou pour la génération d'un classeur multi-feuilles
""" """
for col in self.column_dimensions.keys(): for col in self.column_dimensions.keys():
self.ws.column_dimensions[col] = self.column_dimensions[col] 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: for row in self.rows:
self.ws.append(row) self.ws.append(row)
def generate_standalone(self): def generate(self):
"""génération d'un classeur mono-feuille""" """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) # construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
self.prepare()
with NamedTemporaryFile() as tmp: with NamedTemporaryFile() as tmp:
self.wb.save(tmp.name) self.wb.save(tmp.name)
tmp.seek(0) tmp.seek(0)
return tmp.read() 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( 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
@ -416,7 +407,7 @@ def excel_simple_table(
cell_style = text_style cell_style = text_style
cells.append(ws.make_cell(it, cell_style)) cells.append(ws.make_cell(it, cell_style))
ws.append_row(cells) ws.append_row(cells)
return ws.generate_standalone() return ws.generate()
def excel_feuille_saisie(e, titreannee, description, lines): 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), 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): 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) cell_2 = ws.make_cell(("Liste éditée le " + dt), style1i)
ws.append_row([None, cell_2]) ws.append_row([None, cell_2])
return ws.generate_standalone() return ws.generate()

View File

@ -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 six.moves.urllib.request, six.moves.urllib.parse, six.moves.urllib.error
import random import random
import time import time
from copy import copy
import flask 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 from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_evaluations 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.sco_excel import * # XXX à vérifier
from app.scodoc.TrivialFormulator import TrivialFormulator 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("<h3>Placement et émargement des étudiants</h3>")
H.append(render_template("forms/placement.html", form=form))
H.append(
"""<h3>Explications</h3>
<ul>
<li>préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;</li>
<li>deux types de placements sont possibles :
<ul>
<li>continue suppose que les tables ont toutes un numéro unique;</li>
<li>coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).</li>
</ul></li>
<li>Choisir le format du fichier résultat :
<ul>
<li>le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li>
<li>le format xls produit un classeur avec deux onglets:
<ul>
<li>le premier onglet donne une vue de la salle avec la localisation des étudiants et
peut servir de feuille d'émargement;</li>
<li>le second onglet est un tableau similaire à celui du fichier pdf;</li>
</ul></li>
</ul> </li>
</ul> """
)
F = html_sco_header.sco_footer()
return "\n".join(H) + "<p>" + F
def do_placement_selectetuds():
""" """
Choisi les étudiants et les infos sur la salle pour leur placement. 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] # 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 # description de l'evaluation
H = [ H = [
sco_evaluations.evaluation_describe( 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." "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] E = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})[0]
# Check access # Check access
# (admin, respformation, and responsable_id) # (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(authuser, E["moduleimpl_id"]):
@ -321,7 +430,7 @@ def do_placement(REQUEST):
if placement_method == "xls": if placement_method == "xls":
filename = f"placement_{evalname}_{gr_title_filename}{scu.XLSX_SUFFIX}" 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 E, desceval, listetud, columns, space, maxlines, building, room, numbering
) )
return sco_excel.send_excel_file(REQUEST, xls, filename) return sco_excel.send_excel_file(REQUEST, xls, filename)
@ -399,7 +508,7 @@ def do_placement(REQUEST):
return t 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""" """Dialogue placement etudiants: choix methode et localisation"""
evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluations.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals: if not evals:
@ -415,298 +524,170 @@ def placement_eval_selectetuds(evaluation_id, REQUEST=None):
formid = "placementfile" formid = "placementfile"
if not REQUEST.form.get("%s-submitted" % formid, False): if not REQUEST.form.get("%s-submitted" % formid, False):
# not submitted, choix groupe # not submitted, choix groupe
r = do_placement_selectetuds(REQUEST) r = do_placement_selectetuds()
if r: 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( H.append(
"""<h3>Explications</h3> """<h3>Explications</h3> <ul> <li>Choisir le format du fichier résultat :</li> <ul> <li>le format pdf
<ul> consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li> <li>le format xls
<li>Choisir le format du fichier résultat :</li> produit un classeur avec deux onglets</li> <ul> <li>le premier onglet donne une vue de la salle avec la
<ul> localisation des étudiants et peut servir de feuille d'émargement;</li> <li>le second onglet est un tableau
<li>le format pdf consiste en un tableau précisant pour chaque étudiant la localisation de sa table;</li> similaire à celui du fichier pdf;</li> </ul> </ul> <li>préciser les surveillants et la localisation (bâtiment
<li>le format xls produit un classeur avec deux onglets</li> et salle) et indiquer le nombre de colonnes;</li> <li>deux types de placements sont possibles :</li> <ul>
<ul> <li>continue suppose que les tables ont toutes un numéro unique;</li> <li>coordonnées localise chaque table
<li>le premier onglet donne une vue de la salle avec la localisation des étudiants et peut servir de feuille d'émargement;</li> via un numéro de colonne et un numéro de ligne (ou rangée).</li> </ul> </ul> """
<li>le second onglet est un tableau similaire à celui du fichier pdf;</li>
</ul>
</ul>
<li>préciser les surveillants et la localisation (bâtiment et salle) et indiquer le nombre de colonnes;</li>
<li>deux types de placements sont possibles :</li>
<ul>
<li>continue suppose que les tables ont toutes un numéro unique;</li>
<li>coordonnées localise chaque table via un numéro de colonne et un numéro de ligne (ou rangée).</li>
</ul>
</ul>
"""
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
def Excel_feuille_placement( def _one_header(ws, numbering, styles):
E, description, listetud, columns, space, maxlines, building, room, numbering cells = []
):
"""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))
if numbering == "coordinate": if numbering == "coordinate":
ws1.col(0).width = 4000 cells.append(ws.make_cell("Nom", styles["2bi"]))
ws1.col(1).width = 4500 cells.append(ws.make_cell("Prénom", styles["2bi"]))
ws1.col(2).width = 1500 cells.append(ws.make_cell("Colonne", styles["2bi"]))
ws1.col(3).width = 1500 cells.append(ws.make_cell("Ligne", styles["2bi"]))
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
else: else:
ws1.col(0).width = 4000 cells.append(ws.make_cell("Nom", styles["2bi"]))
ws1.col(1).width = 4500 cells.append(ws.make_cell("Prénom", styles["2bi"]))
ws1.col(2).width = 3000 cells.append(ws.make_cell("Place", styles["2bi"]))
return cells
ws1.col(3).width = 500
ws1.col(4).width = 4000 def _headers(ws, numbering, styles, nb_listes):
ws1.col(5).width = 4500 cells = []
ws1.col(6).width = 3000 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
font0 = Font() styles = {
font0.name = "Arial" "titres": sco_excel.excel_make_style(font_name="Arial", bold=True, size=12),
font0.bold = True "1t": ws0.excel_make_composite_style(
font0.height = 12 * 0x14 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() def _init_lines(maxlines):
font1i.name = "Arial" return [
font1i.height = 10 * 0x14 [] for _ in range(maxlines)
font1i.italic = True ] # 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() def _write_lines(ws, lines):
font2bi.name = "Arial" for line in lines:
font2bi.height = 8 * 0x14 ws.append_row(line)
font2bi.bold = True
font2bi.italic = True
font2 = Font()
font2.name = "Arial"
font2.height = 10 * 0x14
style_titres = XFStyle() def _titres(ws, description, evaluation, building, room, styles):
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
dt = time.strftime("%d/%m/%Y a %Hh%M") dt = time.strftime("%d/%m/%Y a %Hh%M")
ws0.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres) ws.append_single_cell_row(
ws1.write(li, 0, "Feuille placement etudiants éditée le %s" % dt, style_titres) "Feuille placement etudiants éditée le %s" % dt, styles["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,
) )
ws1.write( for line, desceval in enumerate(description):
li, if line in [1, 4, 7]:
0, ws.append_blank_row()
"Date : %s - Horaire : %s à %s" % (E["jour"], E["heure_debut"], E["heure_fin"]), ws.append_single_cell_row(desceval[0], styles["titres"])
style_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 # entetes colonnes - feuille0
cells = [ws0.make_cell()]
for col in range(nbcolumns): for col in range(nbcolumns):
ws0.write(li, col + 1, "colonne %s" % (col + 1), style2b) cells.append(ws0.make_cell("colonne %s" % (col + 1), styles["2b"]))
# entetes colonnes - feuille1 ws0.append_row(cells)
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)
# etudiants # etudiants
line = 1 line = 1
col = 1 col = 1
@ -730,78 +711,188 @@ def Excel_feuille_placement(
placementetud.append(linetud) placementetud.append(linetud)
# etudiants - feuille0 # etudiants - feuille0
line = 0 place = 1
li0 = li for rang, linetud in enumerate(placementetud, start=1):
for linetud in placementetud: # Chaque rang est affiché sur 3 lignes xlsx (notées A, B, C)
li0 += 1 # ligne A: le nom, ligne B: le prénom, ligne C: un espace ou la place
line += 1 cells_a = [ws0.make_cell(rang, styles["2b"])]
ws0.write(li0, 0, line, style2b) cells_b = [ws0.make_cell("", styles["2b"])]
col = 1 cells_c = [ws0.make_cell("", styles["2b"])]
row = 14 # premieère ligne de signature
for etudid in linetud: for etudid in linetud:
ws0.write(li0, col, (etudid[0]).decode(scu.SCO_ENCODING), style1t) cells_a.append(ws0.make_cell(etudid[0], styles["1t"]))
ws0.write(li0 + 1, col, (etudid[1]).decode(scu.SCO_ENCODING), style1m) cells_b.append(ws0.make_cell(etudid[1], styles["1m"]))
ws0.row(li0 + 2).height = space
if numbering == "coordinate": if numbering == "coordinate":
ws0.write(li0 + 2, col, " ", style1bb) cell_c = ws0.make_cell("", styles["1bb"])
else: else:
ws0.write( cell_c = ws0.make_cell("place %s" % place, styles["1bb"])
li0 + 2, col, "place %s" % (col + (line - 1) * nbcolumns), style1bb cells_c.append(cell_c)
) ws0.set_row_dimension_height(row, space / 25)
# ws0.write(li+3,col, ' ', style1bm ) row += 3
# ws0.write(li+4,col, ' ', style1bb ) place = place + 1
if col == nbcolumns: if col == nbcolumns:
col = 0 ws0.append_row(cells_a)
li0 += 2 ws0.append_row(cells_b)
col += 1 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": def _compute_ordretud(listetud, nbcolumns, numbering):
coloffset = 5 orderetud = []
else: line = 1
coloffset = 4 col = 1
line = 0 for etudid in listetud:
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)
if numbering == "coordinate": if numbering == "coordinate":
ws1.write(li1, col + 2, etudid[2], style2m2) orderetud.append((etudid[0], etudid[1], col, line))
ws1.write(li1, col + 3, etudid[3], style2r)
else: 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 line = 0
li1 = li if col >= maxlistes: # fin de page
nbcol = nbcol + 1 _write_lines(ws, lines)
col = col + coloffset lines = _init_lines(maxlines)
if nbcol == 2:
li = li + maxlines + 2
li1 = li
nbcol = 0
col = 0 col = 0
if numbering == "coordinate": ws.append_blank_row()
ws1.write(li, 0, "Nom", style2bi) nb_etu_restant -= maxlistes * maxlines
ws1.write(li, 1, "Prénom", style2bi) nb_listes = min(
ws1.write(li, 2, "Colonne", style2bi) maxlistes, nb_etu_restant // maxlines + 1
ws1.write(li, 3, "Ligne", style2bi) ) # 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) def _excel_feuille_placement(
ws1.write(li, 5, "Prénom", style2bi) evaluation,
ws1.write(li, 6, "Place", style2bi) description,
return wb.savetostr() 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()

View File

@ -0,0 +1,56 @@
{% import 'bootstrap/wtf.html' as wtf %}
{% macro render_field(field) %}
<tr>
<td class="wtf-field">{{ field.label }}</td>
<td class="wtf-field">{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</td>
</tr>
{% endmacro %}
<div class="saisienote_etape1 form_placement">
<form method=post>
{{ form.evaluation_id }}
<table class="tf">
<tbody>
{{ 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) }}
<!--
{% for partition in form.groups_tree %}
<tr>
{% if partition == 'Tous' %}
<td rowspan="{{ form.groups_tree_length }}">Groupes</td>
{% endif %}
<td>{{ partition }}</td>
<td>
{% for groupe in form.groups_tree[partition] %}
{{ groupe }}{{ form[form.groups_tree[partition][groupe]] }}
{% endfor %}
</td>
</tr>
{% endfor %}
-->
{% endif %}
{{ render_field(form.file_format) }}
</tbody>
</table>
<p>
<input id="gr_submit" type=submit value="Ok">
<input id="gr_cancel" type=submit value="Annuler">
</script>
</form>
</div>

View File

@ -1611,6 +1611,7 @@ sco_publish(
"/placement_eval_selectetuds", "/placement_eval_selectetuds",
sco_placement.placement_eval_selectetuds, sco_placement.placement_eval_selectetuds,
Permission.ScoEnsView, Permission.ScoEnsView,
methods=["GET", "POST"],
) )
sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView) sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView)