# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 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, ) from app.models import Evaluation, Identite, ModuleImpl import app.scodoc.sco_utils as scu from app.scodoc import sco_preferences from app.scodoc import sco_evaluations from app.scodoc import sco_excel from app.scodoc.sco_excel import ScoExcelBook, COLORS from app.scodoc import sco_groups from app.scodoc.gen_tables import GenTable import sco_version 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 has_groups = partition != TOUS nb_groups = sum(len(pg) for pg in groups_tree.values()) 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=[], description="texte libre") batiment = StringField("Bâtiment", description="texte libre") salle = StringField("Salle", description="texte libre") 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.""" _ = Evaluation.get_evaluation(evaluation_id) # check exist ? self.groups_tree, self.has_groups, self.nb_groups = _get_group_info( evaluation_id ) choices = [] for partition, groupes in self.groups_tree.items(): for groupe in groupes: if ( groupe == TOUS ): # Affichage et valeur spécifique pour le groupe TOUS self.tous_id = str(groupes[groupe]) choices.append((TOUS, TOUS)) else: groupe_id = str(self.groups_tree[partition][groupe]) choices.append((groupe_id, f"{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 ( """

Génération du placement impossible pour %s

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

Continuer

""" % runner.__dict__ ) return runner.exec_placement() # calcul et generation du fichier return render_template( "scodoc/forms/placement.j2", evaluations_description=sco_evaluations.evaluation_describe( evaluation_id=evaluation_id ), form=form, ) 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 par l'identifiant de ce groupe self.groups_ids = [ gid if gid != TOUS else form.tous_id for gid in form["groups"].data ] evl = Evaluation.get_evaluation(self.evaluation_id) self.evaluation = evl self.groups = sco_groups.listgroups(self.groups_ids) self.gr_title_filename = sco_groups.listgroups_filename(self.groups) self.current_user = current_user self.moduleimpl_id = evl.moduleimpl_id self.moduleimpl: ModuleImpl = evl.moduleimpl self.moduleimpl_data = self.moduleimpl.to_dict() mod = evl.moduleimpl.module self.module_data = mod.to_dict() self.evalname = f"""{mod.code or "?"}-{ evl.date_debut.strftime("%Y-%m-%d_%Hh%M") if evl.date_debut else ""}""" if evl.description: self.evaltitre = self.evaluation.description else: self.evaltitre = f"""évaluation{ evl.date_debut.strftime(' du ' + scu.DATEATIME_FMT) if evl.date_debut else ''}""" self.desceval = [ # une liste de chaines: description de l'evaluation evl.moduleimpl.formsemestre.titre_annee(), f"""Module : {mod.code or "?"} - {evl.moduleimpl.module.abbrev or ""}""", f"Surveillants : {self.surveillants}", f"Bâtiment : {self.batiment} - Salle : {self.salle}", f"Évaluation : {self.evaltitre} (coef. {evl.coefficient:g})", ] 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 self.moduleimpl.can_edit_notes(self.current_user) 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) -> list[tuple[str, str, int]]: # tous les etudiants ? get_all_students = None in [g["group_name"] for g in self.groups] etudid_etats = sco_groups.do_evaluation_listeetuds_groups( self.evaluation_id, self.groups, getallstudents=get_all_students, include_demdef=True, ) listetud = [] # liste de tuples (nom, prenom, etudid) for etudid, etat in etudid_etats: if etat != scu.DEMISSION: # infos identite etudiant etud = Identite.get_etud(etudid) nom = etud.nom.upper() prenom = etud.prenom.lower().capitalize() listetud.append((nom, prenom, etud.id)) 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 = f"placement_{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 += f"""\nDate : {self.evaluation.date_debut.strftime(scu.DATE_FMT) if self.evaluation.date_debut else '-' } - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin() }""" filename = f"placement_{self.evalname}_{self.gr_title_filename}" titles = { "nom": "Nom", "prenom": "Prénom", "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=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}", pdf_title=pdf_title, # pdf_shorttitle = '', preferences=sco_preferences.SemPreferences( self.moduleimpl_data["formsemestre_id"] ), table_id="placement_pdf", ) 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): date_time = time.strftime(scu.DATEATIME_FMT) worksheet.append_single_cell_row( f"Feuille placement etudiants éditée le {date_time}", 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( f"""Date : {self.evaluation.date_debut.strftime(scu.DATE_FMT) if self.evaluation.date_debut else '-' } - Horaire : {self.evaluation.heure_debut()} à {self.evaluation.heure_fin() }""", 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(f"colonne {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(f"place {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()