Merge pull request 'scodoc9_placement_PR' (#126) from jmplace/ScoDoc-Lille:scodoc9_placement_PR into master

Reviewed-on: https://scodoc.org/git/viennet/ScoDoc/pulls/126
This commit is contained in:
Emmanuel Viennet 2021-09-18 13:43:11 +02:00
commit 11ba73d264
14 changed files with 1748 additions and 1808 deletions

1
.gitignore vendored
View File

@ -170,3 +170,4 @@ Thumbs.db
*.code-workspace
copy

View File

@ -482,9 +482,9 @@ class GenTable(object):
ses.append_blank_row() # empty line
ses.append_single_cell_row(self.origin, style_base)
if wb is None:
return ses.generate_standalone()
return ses.generate()
else:
ses.generate_embeded()
ses.generate()
def text(self):
"raw text representation of the table"

View File

@ -269,9 +269,14 @@ def etudarchive_generate_excel_sample(group_id=None, REQUEST=None):
],
extra_cols=["fichier_a_charger"],
)
return sco_excel.send_excel_file(
REQUEST, data, "ImportFichiersEtudiants" + scu.XLSX_SUFFIX
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)
def etudarchive_import_files_form(group_id, REQUEST=None):

View File

@ -35,11 +35,11 @@ from enum import Enum
from tempfile import NamedTemporaryFile
import openpyxl.utils.datetime
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
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL
from openpyxl.comments import Comment
import app.scodoc.sco_utils as scu
from app.scodoc import notesdb
@ -59,24 +59,9 @@ 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)
# font, border, number_format, fill,...
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
def xldate_as_datetime(xldate, datemode=0):
@ -86,6 +71,12 @@ def xldate_as_datetime(xldate, datemode=0):
return openpyxl.utils.datetime.from_ISO8601(xldate)
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:
"""Permet la génération d'un classeur xlsx composé de plusieurs feuilles.
usage:
@ -98,13 +89,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
@ -112,12 +106,12 @@ 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)
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
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()
@ -125,6 +119,7 @@ class ScoExcelBook:
def excel_make_style(
bold=False,
italic=False,
outline=False,
color: COLORS = COLORS.BLACK,
bgcolor: COLORS = None,
halign=None,
@ -145,7 +140,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)
@ -182,41 +184,93 @@ class ScoExcelSheet:
"""
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.
"""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 wb is None:
self.wb = Workbook()
self.ws = self.wb.active
self.ws.title = self.sheet_name
else:
self.wb = None
self.ws = wb
# 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.
@ -232,8 +286,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:
@ -272,73 +330,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 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)
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
@ -377,7 +393,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):
@ -538,7 +554,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):
@ -758,4 +774,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()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -324,5 +324,12 @@ 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()
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}")

View File

@ -829,7 +829,10 @@ 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)
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):

View File

@ -486,7 +486,10 @@ 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)
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):

View File

@ -1092,13 +1092,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 {

View File

@ -0,0 +1,77 @@
{% import 'bootstrap/wtf.html' as wtf %}
{% macro render_field(field) %}
<tr>
<td class="wtf-field">{{ field.label }}</td>
<td class="wtf-field">{{ field()|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 }}
{{ form.csrf_token }}
<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) }}
<!-- Tentative de recréer le choix des groupes sous forme de cases à cocher // demande à créer des champs wtf dynamiquement
{% for partition in form.groups_tree %}
<tr>
{% if partition == 'Tous' %}
<td rowspan="{{ form.nb_groups }}">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>
<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>il est possible de choisir un ou plusieurs groupes (shift/ctrl click) ou de choisir 'tous'.</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>
</div>

View File

@ -1645,8 +1645,8 @@ 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)
# --- Saisie des notes
sco_publish(

View File

@ -1937,7 +1937,10 @@ 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)
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 +1958,10 @@ 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
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)

View File

@ -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,10 @@ 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
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"])