# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2023 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 # ############################################################################## """ScoDoc: génération feuille émargement et placement Contribution J.-M. Place 2021 basée sur une idée de M. Salomon, UFC / IUT DE BELFORT-MONTBÉLIARD, 2016 """ import random import time from copy import copy import wtforms.validators from flask import request, render_template from flask_login import current_user from flask_wtf import FlaskForm from openpyxl.styles import PatternFill, Alignment, Border, Side, Font from wtforms import ( StringField, SubmitField, SelectField, RadioField, HiddenField, SelectMultipleField, ) import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import ScoValueError from app.scodoc import html_sco_header, sco_preferences from app.scodoc import sco_edit_module from app.scodoc import sco_evaluations from app.scodoc import sco_evaluation_db from app.scodoc import sco_excel from app.scodoc.sco_excel import ScoExcelBook, COLORS from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl from app.scodoc import sco_permissions_check from app.scodoc.gen_tables import GenTable from app.scodoc import sco_etud import sco_version _ = lambda x: x # sans babel _l = _ COORD = "Coordonnées" SEQ = "Continue" TOUS = "Tous" def _get_group_info(evaluation_id): # groupes groups = sco_groups.do_evaluation_listegroupes(evaluation_id, include_default=True) has_groups = False groups_tree = {} for group in groups: partition = group["partition_name"] or TOUS group_id = group["group_id"] group_name = group["group_name"] or TOUS if partition not in groups_tree: groups_tree[partition] = {} groups_tree[partition][group_name] = group_id if partition != TOUS: has_groups = True else: has_groups = False nb_groups = sum([len(groups_tree[p]) for p in groups_tree]) return groups_tree, has_groups, nb_groups class PlacementForm(FlaskForm): """Formulaire pour placement des étudiants en Salle""" 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=[]) batiment = StringField("Batiment") salle = StringField("Salle") nb_rangs = SelectField( "nb de places en largeur", coerce=int, choices=[3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], description="largeur de la salle, en nombre de places", ) etiquetage = RadioField( "Numérotation", choices=[SEQ, COORD], validators=[ wtforms.validators.DataRequired("indiquez le style de numérotation"), ], ) groups = SelectMultipleField( "Groupe(s)", validators=[], ) submit = SubmitField("OK") def __init__(self, formdata=None, data=None): super().__init__(formdata=formdata, data=data) self.groups_tree = {} self.has_groups = None self.nb_groups = None self.tous_id = None self.set_evaluation_infos(data["evaluation_id"]) def set_evaluation_infos(self, evaluation_id): """Initialise les données du formulaire avec les données de l'évaluation.""" eval_data = sco_evaluation_db.get_evaluation_dict( {"evaluation_id": evaluation_id} ) if not eval_data: raise ScoValueError("invalid evaluation_id") self.groups_tree, self.has_groups, self.nb_groups = _get_group_info( evaluation_id ) choices = [] for partition in self.groups_tree: for groupe in self.groups_tree[partition]: if ( groupe == TOUS ): # Affichage et valeur spécifique pour le groupe TOUS self.tous_id = str(self.groups_tree[partition][groupe]) choices.append((TOUS, TOUS)) else: groupe_id = str(self.groups_tree[partition][groupe]) choices.append((groupe_id, "%s (%s)" % (str(groupe), partition))) self.groups.choices = choices # self.groups.default = [TOUS] # Ne fonctionnne pas... (ni dans la déclaration de PlaceForm.groups) # la réponse [] est de toute façon transposée en [ self.tous_id ] lors du traitement (cas du groupe unique) class _DistributeurContinu: """Distribue les places selon un ordre numérique.""" def __init__(self): self.position = 1 def suivant(self): """Retounre la désignation de la place suivante""" 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): """Retounre la désignation de la place suivante""" retour = (self.index, self.rang) self.rang += 1 if self.rang > self.nb_rangs: self.rang = 1 self.index += 1 return retour def placement_eval_selectetuds(evaluation_id): """Creation de l'écran de placement""" form = PlacementForm( request.form, data={"evaluation_id": int(evaluation_id), "groups": TOUS}, ) if form.validate_on_submit(): runner = PlacementRunner(form) if not runner.check_placement(): return ( """
(vérifiez que le semestre n'est pas verrouillé et que vous avez l'autorisation d'effectuer cette opération)
""" % runner.__dict__ ) return runner.exec_placement() # calcul et generation du fichier htmls = [ html_sco_header.sco_header(), sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), "" + footer
class PlacementRunner:
"""Execution de l'action définie par le formulaire"""
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
if len(form["groups"].data) == 0:
self.groups_ids = [form.tous_id]
else: # On remplace le mot-clé TOUS le l'identiant de ce groupe
self.groups_ids = [
gid if gid != TOUS else form.tous_id for gid in form["groups"].data
]
self.eval_data = sco_evaluation_db.get_evaluation_dict(
{"evaluation_id": self.evaluation_id}
)[0]
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.moduleimpl_list(
moduleimpl_id=self.moduleimpl_id
)[0]
self.module_data = sco_edit_module.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.module_data["code"] or "?",
ndb.DateDMYtoISO(self.eval_data["jour"]),
)
if self.eval_data["description"]:
self.evaltitre = self.eval_data["description"]
else:
self.evaltitre = "évaluation du %s" % self.eval_data["jour"]
self.desceval = [ # une liste de chaines: description de l'evaluation
"%s" % self.sem["titreannee"],
"Module : %s - %s"
% (self.module_data["code"] or "?", self.module_data["abbrev"] or ""),
"Surveillants : %s" % self.surveillants,
"Batiment : %(batiment)s - Salle : %(salle)s" % self.__dict__,
"Controle : %s (coef. %g)"
% (self.evaltitre, self.eval_data["coefficient"]),
]
self.styles = None
self.plan = None
self.listetud = None
def check_placement(self):
"""Vérifie que l'utilisateur courant a le droit d'édition sur les notes"""
# Check access (admin, respformation, and responsable_id)
return sco_permissions_check.can_edit_notes(
self.current_user, self.moduleimpl_id
)
def exec_placement(self):
"""Excéute l'action liée au formulaire"""
self._repartition()
if self.file_format == "xls":
return self._production_xls()
return self._production_pdf()
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):
get_all_students = None in [
g["group_name"] for g in self.groups
] # tous les etudiants
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
self.evaluation_id,
self.groups,
getallstudents=get_all_students,
include_demdef=True,
)
listetud = [] # liste de couples (nom,prenom)
for etudid, etat in etudid_etats:
# infos identite etudiant (xxx sous-optimal: 1/select par etudiant)
ident = sco_etud.etudident_list(ndb.GetDBConnexion(), {"etudid": etudid})[0]
if 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 = "placement_%s_%s" % (self.evalname, self.gr_title_filename)
xls = self._excel_feuille_placement()
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
def _production_pdf(self):
pdf_title = "
".join(self.desceval)
pdf_title += (
"\nDate : %(jour)s - Horaire : %(heure_debut)s à %(heure_fin)s"
% self.eval_data
)
filename = "placement_%(evalname)s_%(gr_title_filename)s" % self.__dict__
titles = {
"nom": "Nom",
"prenom": "Prenom",
"colonne": "Colonne",
"ligne": "Ligne",
"place": "Place",
}
if self.etiquetage == COORD:
columns_ids = ["nom", "prenom", "colonne", "ligne"]
else:
columns_ids = ["nom", "prenom", "place"]
rows = []
for etud in sorted(self.plan, key=lambda item: item[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(
self.moduleimpl_data["formsemestre_id"]
),
)
return tab.make_page(fmt="pdf", with_html_headers=False)
def _one_header(self, worksheet):
cells = [
worksheet.make_cell("Nom", self.styles["2bi"]),
worksheet.make_cell("Prénom", self.styles["2bi"]),
]
if self.etiquetage == COORD:
cells.append(worksheet.make_cell("Colonne", self.styles["2bi"]))
cells.append(worksheet.make_cell("Ligne", self.styles["2bi"]))
else:
cells.append(worksheet.make_cell("Place", self.styles["2bi"]))
return cells
def _headers(self, worksheet, nb_listes):
cells = []
for _ in range(nb_listes):
cells += self._one_header(worksheet)
cells.append(worksheet.make_cell(""))
worksheet.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 _titres(self, worksheet):
datetime = time.strftime("%d/%m/%Y a %Hh%M")
worksheet.append_single_cell_row(
"Feuille placement etudiants éditée le %s" % datetime, self.styles["titres"]
)
for line, desceval in enumerate(self.desceval):
if line in [1, 4, 7]:
worksheet.append_blank_row()
worksheet.append_single_cell_row(desceval, self.styles["titres"])
worksheet.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
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
rang += 1
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
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 _feuille1(self, worksheet, 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
worksheet.set_column_dimension_width(value=widths)
nb_etu_restant = len(self.listetud)
self._titres(worksheet)
nb_listes = min(
maxlistes, nb_etu_restant // maxlines + 1
) # nombre de colonnes dans la page
self._headers(worksheet, nb_listes)
# construction liste alphabétique
# Affichage
lines = [[] for _ in range(maxlines)]
lineno = 0
col = 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[lineno].append(worksheet.make_cell())
lines[lineno].append(worksheet.make_cell(etud[0][0], self.styles["2l"]))
lines[lineno].append(worksheet.make_cell(etud[0][1], self.styles["2m1"]))
if self.etiquetage == COORD:
lines[lineno].append(
worksheet.make_cell(etud[1][1], self.styles["2m2"])
)
lines[lineno].append(worksheet.make_cell(etud[1][0], self.styles["2r"]))
else:
lines[lineno].append(worksheet.make_cell(etud[1], self.styles["2r"]))
lineno = lineno + 1
if lineno >= maxlines: # fin de liste
col = col + 1
lineno = 0
if col >= maxlistes: # fin de page
for line_cells in lines:
worksheet.append_row(line_cells)
lines = [[] for _ in range(maxlines)]
col = 0
worksheet.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(worksheet, nb_listes)
for line_cells in lines:
worksheet.append_row(line_cells)
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)
"""
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
workbook = ScoExcelBook()
sheet_name_0 = "Emargement"
ws0 = workbook.create_sheet(sheet_name_0)
# 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
)
sheet_name_1 = "Positions"
ws1 = workbook.create_sheet(sheet_name_1)
self._make_styles(ws0, ws1)
self._feuille0(ws0, space)
self._feuille1(ws1, maxlines)
return workbook.generate()