diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py
index 32cf27c0..5ac78d9f 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 4f9e9a0b..e0cf5b80 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("
" + 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( - """