From 22c2fe0f3b27b95e8e925656fda34d22f963232c Mon Sep 17 00:00:00 2001 From: jmpla Date: Fri, 28 Jul 2023 09:00:21 +0200 Subject: [PATCH] reprise sco96 --- app/but/prepajury_but.py | 445 ++++++++++++++++++++++ app/but/prepajury_desc.py | 522 ++++++++++++++++++++++++++ app/but/prepajury_xl.py | 410 ++++++++++++++++++++ app/but/prepajury_xl_format.py | 666 +++++++++++++++++++++++++++++++++ 4 files changed, 2043 insertions(+) create mode 100644 app/but/prepajury_but.py create mode 100644 app/but/prepajury_desc.py create mode 100644 app/but/prepajury_xl.py create mode 100644 app/but/prepajury_xl_format.py diff --git a/app/but/prepajury_but.py b/app/but/prepajury_but.py new file mode 100644 index 000000000..5f5ce1650 --- /dev/null +++ b/app/but/prepajury_but.py @@ -0,0 +1,445 @@ +# -*- 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 +# +############################################################################## + +"""Feuille excel pour préparation des jurys classiques (non BUT) +""" +import time + +from flask import abort + +from app.but import jury_but +from app.but.cursus_but import EtudCursusBUT +from app.but.prepajury_desc import ParcoursDesc, FormsemestreDesc +from app.models import ( + FormSemestre, + ApcParcours, + Formation, +) +import app.scodoc.sco_utils as scu +from app.but.prepajury_xl import ScoExcelBook + + +class Element: + def __init__(self, etudiant): + self.etudiant = etudiant + self.note = None + self.resultat = None + self.format = 0 + + def set_note(self, note): + self.note = note + + def set_res(self, res): + self.resultat = res + + def get_res(self): + return self.resultat + + def get_note(self): + return self.note + + +class ElementUE(Element): + def __init__(self, etudiant): + super().__init__(etudiant) + self.status = None + + def set_status(self, status): + self.status = status + + +class ElementNiveau(Element): + def __init__(self, etudiant, competence_id): + super().__init__(etudiant) + self.competence_id = competence_id + self.validation = None + self.ues = {} + + def get_elem(self, periode=None): + if periode is None: + return self + return self.ues.get(periode, None) + + def compute(self, rcue): + self.set_note(rcue.moy_rcue) + + +class ElementFormsemestre(Element): + def __init__(self, etudiant, formsemestre_desc: FormsemestreDesc = None): + super().__init__(etudiant) + self.formsemestre_desc = formsemestre_desc + self.formsemestre_id = formsemestre_desc.formsemestre_id + self.deca = None + self.ues = {} + + def get_elem(self, competence_id=None): + if competence_id is None: + return self + return self.ues.get(competence_id, None) + + +class ElementAnnee(Element): + def __init__(self, etudiant): + super().__init__(etudiant) + self.formsemestres = {} + self.niveaux = {} + self.last = None + self.deca = None + + def get_elem(self, competence_id=None, periode=None): + if competence_id is None and periode is None: + return self + elif competence_id is None: + return self.formsemestres.get(periode, None) + elif competence_id in self.niveaux: + return self.niveaux[competence_id].get_elem(periode) + else: + return None + + def set_periode(self, periode, formsemestre_desc): + self.formsemestres[periode] = formsemestre_desc + + def set_niveau(self, competence_id, elem_niveau): + self.niveaux[competence_id] = elem_niveau + + def add_validation(self, validation): + competence_id = validation.ue1.niveau_competence.competence_id + self.niveaux[competence_id].set_res(validation.code) + + def create_structure(self): + self.last = self.formsemestres.get(2, self.formsemestres.get(1, None)) + if self.last is not None: + self.deca = jury_but.DecisionsProposeesAnnee( + self.etudiant.ident, self.last.formsemestre_desc.formsemestre + ) + for niveau in self.deca.niveaux_competences: + competence_id = niveau.competence_id + elem_niveau = ElementNiveau(self.etudiant, competence_id) + self.niveaux[competence_id] = elem_niveau + for ue in self.deca.ues_impair: + competence_id = ue.niveau_competence.competence_id + periode = 1 + elem_ue = ElementUE(self.etudiant) + self.niveaux[competence_id].ues[periode] = elem_ue + self.formsemestres[periode].ues[competence_id] = elem_ue + for ue in self.deca.ues_pair: + competence_id = ue.niveau_competence.competence_id + periode = 2 + elem_ue = ElementUE(self.etudiant) + self.niveaux[competence_id].ues[periode] = elem_ue + self.formsemestres[periode].ues[competence_id] = elem_ue + + def compute(self): + if self.last is not None: + self.set_res(self.deca.code_valide) + self.set_note( + self.last.formsemestre_desc.get_resultats().get_etud_moy_gen( + self.etudiant.ident.id + ) + ) + + +class EtudiantJury: + """ + Structure: + ident + formation + parcour + cursus + inscriptions* + current_formsemestre + absences_tot + absences_just + Annees* + nb_rcues + note + resultat + niveaux* + note + resultat + ues* + note + resultat + DUT + resultat + BUT + resultat + """ + + annee_periode = { + 1: ("BUT1", 1), + 2: ("BUT1", 2), + 3: ("BUT2", 1), + 4: ("BUT2", 2), + 5: ("BUT3", 1), + 6: ("BUT3", 2), + } + + def __init__(self, ident, contexte: "_Compilation"): + self.ident = ident + self.contexte = contexte + self.formation = contexte.formation + self.current_formsemestre = contexte.formsemestre + self.current_formsemestre_id = contexte.formsemestre.formsemestre_id + self.current_formsemestre_desc = contexte.get_semestredesc( + self.current_formsemestre_id + ) + self.nbabs, self.nbabsjust = self.current_formsemestre.get_abs_count(ident.id) + self.cursus: EtudCursusBUT = EtudCursusBUT(ident, self.formation) + self.parcour = self.cursus.inscriptions[-1].parcour + # donnes propres à l étudiant (à remplir par fill_in) + self.history = [] # description historique de l etudiant + self.formsemestres = [] # liste historique des formsemestres + self.formsemestre_by_semestre = ( + {} + ) # semetre_id -> dernier FormSemestreDesc pour chaque semestre + self.formsemestre_by_annee = ( + {} + ) # annee -> dernier FormSemestreDesc pour chaque semestre + self.annees = {} + self.DUT = None # Résultat au DUT + self.BUT = None # Résultat au BUT + + def get_elem(self, annee=None, competence_id=None, periode=None): + if annee is None: + return self + if annee in self.annees: + return self.annees[annee].get_elem(competence_id, periode) + return None + + def get_desc(self, annee=None, competence_id=None, periode=None): + return self.parcour.get_desc(annee, competence_id, periode) + + def compute_history(self): + # calcul historique + for inscription in sorted( + self.cursus.inscriptions, key=lambda x: x.formsemestre.date_debut + ): + formsemestre = inscription.formsemestre + semestre_id = None + # if formsemestre.formation == self.formation: + if formsemestre.formation.is_apc(): + formsemestre_id = formsemestre.formsemestre_id + formsemestre_desc = self.contexte.get_semestredesc(formsemestre_id) + semestre_id = formsemestre.semestre_id + self.formsemestres.append(formsemestre) + annee, periode = EtudiantJury.annee_periode[semestre_id] + self.formsemestre_by_semestre[semestre_id] = formsemestre_desc + self.formsemestre_by_annee[annee] = formsemestre_desc + etat = inscription.etat + Sx = f"S{semestre_id}" + if etat != "I": + Sx += " (Dem)" if etat == "D" else f"({etat})" + self.history.append(Sx) + + def create_structure(self): + for annee, formsemestre_desc in self.formsemestre_by_annee.items(): + elem_annee = ElementAnnee(self) + self.annees[annee] = elem_annee + + for semestre_id, formsemestre_desc in self.formsemestre_by_semestre.items(): + annee, periode = EtudiantJury.annee_periode[semestre_id] + elem_formsemestre = ElementFormsemestre(self.ident, formsemestre_desc) + self.annees[annee].set_periode(periode, elem_formsemestre) + + for annee in self.annees: + self.annees[annee].create_structure() + + def compute(self): + for annee in self.annees: + self.annees[annee].compute() + for ( + competence_id, + validations, + ) in self.cursus.validation_par_competence_et_annee.items(): + for annee, validation in validations.items(): + self.annees[annee].add_validation(validation) + + def fill_in(self): + """ + Creation des donnees propres à l'étudiant + """ + self.compute_history() + # self.create_structure() + # self.compute() + # for ( + # competence_id, + # validations_par_competence, + # ) in self.cursus.validation_par_competence_et_annee.items(): + # for annee, validation in validations_par_competence.items(): + # elem_annee = self.annees[annee] + # elem_formsemestre = elem_annee.formsemestres.get( + # periode, ElementFormsemestre(self, formsemestre_desc) + # ) + # resultats = formsemestre_desc.get_resultats() + # ues = resultats.etud_ues(self.ident.etudid) + # for ue in ues: + # niveau_id = ue.niveau_competence_id + # competence_id = ue.niveau_competence.competence_id + # status = resultats.get_etud_ue_status(self.ident.etudid, ue.id) + # if competence_id not in self.annees[annee].niveaux: + # elem_niveau = ElementNiveau(self, validation) + # self.annees[annee].niveaux[competence_id] = elem_niveau + # else: + # elem_niveau = self.annees[annee].niveaux[competence_id] + # elem_ue = ElementUE(self, status) + # elem_niveau.ues[periode] = elem_ue + # + # + # + # for ( + # competence_id, + # validation, + # ) in self.cursus.validation_par_competence_et_annee.items(): + # self.elements_par_competence_annee_et_periode[competence_id] = {} + # for annee in validation: + # self.elements_par_competence_annee_et_periode[competence_id][ + # annee + # ] = [] + # self.deca_by_semestre[semestre_idx] = jury_but.DecisionsProposeesAnnee( + # self.ident, self.formsemestre_by_semestre[semestre_idx] + # ) + # # niveau.add_ue_status(semestre_idx, status) + # if annee in self.deca: + # for rcue in self.deca[annee].rcues_annee: + # ue_id1 = rcue.ue_1.id + # self.notes_ues[ue_id1] = rcue.moy_ue_1 + # ue_id2 = rcue.ue_2.id + # self.notes_ues[ue_id2] = rcue.moy_ue_2 + # rcue_id = rcue.ue_1.niveau_competence_id + # self.notes_rcues[rcue_id] = rcue.moy_rcue + + def get_note(self, annee, competence_id=None, periode=None): + elem: Element = self.get_elem(annee, competence_id, periode) + if elem: + return elem.get_note() + return "-" + + def get_res(self, annee, competence_id=None, periode=None): + elem: Element = self.get_elem(annee, competence_id, periode) + if elem: + return elem.get_res() + return "-" + + def get_data(self): + result = [ + self.ident.id, + self.ident.code_nip, + self.ident.civilite, + self.ident.nom, + self.ident.prenom, + self.ident.etat_civil_pv(with_paragraph=False), + self.parcour.code, + ", ".join(self.history), + ] + return result + + +class _Compilation: + """ + structure: + semestres: formsemestre_id -> FormSemestreDesc + parcours: parcour_code -> ParcoursDesc + formation + """ + + def __init__(self, formsemestre: FormSemestre): + self.semestres = {} + self.parcours = {} + formsemestre_id = formsemestre.formsemestre_id + self.formation: Formation = formsemestre.formation + self.formsemestre = formsemestre + self.add_semestre(formsemestre_id, formsemestre) + self.current_semestre = self.semestres[formsemestre_id] + # inventaire des semestres et parcours + for ident in ( + self.semestres[formsemestre_id].get_resultats().get_inscrits(order_by="moy") + ): + etudiant: EtudiantJury = EtudiantJury( + ident, self + ) # initialise etudiant.cursus et etudiant.parcour + for inscription in etudiant.cursus.inscriptions: + formsemestre = inscription.formsemestre + if ( + formsemestre.formation.referentiel_competence + == self.formation.referentiel_competence + ): + self.add_semestre(formsemestre.formsemestre_id, formsemestre) + scodocParcour = etudiant.parcour + if scodocParcour is None: + parcourCode = "TC" + else: + parcourCode = scodocParcour.code + if parcourCode in self.parcours: + parcoursDesc = self.parcours[parcourCode] + else: + parcoursDesc = ParcoursDesc(self.formation, scodocParcour) + self.parcours[parcourCode] = parcoursDesc + parcoursDesc.add_etudiant(etudiant) + etudiant.fill_in() + + def get_semestredesc(self, formsemestre_id): + return self.semestres.get(formsemestre_id, None) + + def add_semestre(self, formsemestre_id, formsemestre=None): + if formsemestre_id not in self.semestres: + self.semestres[formsemestre_id] = FormsemestreDesc( + formsemestre_id, formsemestre + ) + + def add_parcours(self, scodoc_parcour: ApcParcours, etudiant: EtudiantJury): + parcour_code = scodoc_parcour.get("code", "TC") + if parcour_code not in self.parcours: + self.parcours[parcour_code] = ParcoursDesc(self.formation, scodoc_parcour) + self.parcours[parcour_code].add(etudiant) + + def computes_decision(self): + pass + + def make_excel(self, filename: str): + workbook = ScoExcelBook() + for parcoursCode, parcours in self.parcours.items(): + parcours.generate(workbook) + + mime, suffix = scu.get_mime_suffix("xlsx") + xls = workbook.generate() + return scu.send_file(xls, filename=filename, mime=mime, suffix=suffix) + + +def feuille_preparation_jury_but(formsemestre_id): + if not isinstance(formsemestre_id, int): + abort(404) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + # res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + """Feuille excel pour préparation des jurys adaptée pour le BUT.""" + # breakpoint() + compilation = _Compilation(formsemestre) + # compilation.computes_decision() + filename = scu.sanitize_filename( + f"""{'jury'}-{formsemestre.titre_num()}-{time.strftime("%Y-%m-%d")}""" + ) + return compilation.make_excel(filename) diff --git a/app/but/prepajury_desc.py b/app/but/prepajury_desc.py new file mode 100644 index 000000000..bb87c126c --- /dev/null +++ b/app/but/prepajury_desc.py @@ -0,0 +1,522 @@ +import openpyxl +from openpyxl.worksheet.worksheet import Worksheet + +from app.but.prepajury_xl_format import ( + SCO_COLORS, + FMT, + SCO_FONTSIZE, + SCO_VALIGN, + SCO_HALIGN, + SCO_BORDERTHICKNESS, + Sco_Style, + HAIR_BLACK, + SCO_NUMBER_FORMAT, +) +from app.comp import res_sem +from app.models import ApcCompetence, ApcParcours, FormSemestre +from app.but.prepajury_xl import ( + ScoExcelBook, + ScoExcelSheet, + base_signature, + Frame_Engine, + Merge_Engine, +) + + +UNUSED = "XXX" +liste_annees = ["BUT1", "BUT2", "BUT3"] +header_colors = { + "BUT1": { + "BUT": SCO_COLORS.BUT1, + "RCUE": SCO_COLORS.RCUE1, + "UE": SCO_COLORS.UE1, + }, + "BUT2": { + "BUT": SCO_COLORS.BUT2, + "RCUE": SCO_COLORS.RCUE2, + "UE": SCO_COLORS.UE2, + }, + "BUT3": { + "BUT": SCO_COLORS.BUT3, + "RCUE": SCO_COLORS.RCUE3, + "UE": SCO_COLORS.UE3, + }, +} + + +def periode(semestre_idx): + return 1 + (semestre_idx + 1) % 2 + + +class UeDesc: + def __init__(self, scodocUe, competence_id, periode): + self.fromScodoc = scodocUe + self.competence_id = competence_id + self.periode = periode + + +class NiveauDesc: + def __init__(self, scodocNiveau): + self.fromScodoc = scodocNiveau + self.ues = {1: None, 2: None} + for scodocUe in scodocNiveau.ues: + ue_desc = UeDesc( + scodocUe, scodocNiveau.competence_id, periode(scodocUe.semestre_idx) + ) + self.ues[periode(scodocUe.semestre_idx)] = scodocUe + if not scodocUe.is_external: + self.ues[periode(scodocUe.semestre_idx)] = scodocUe + + def get_desc(self, periode=None): + if periode is None: + return self + return self.ues.get(periode, None) + + def generate_data( + self, ws: ScoExcelSheet, row: int, etudiant: "EtudiantJury", column: int + ) -> int: + for periode in [1, 2]: + ue = self.ues[periode] + if ue is None: + ws.set_cell(row, column, UNUSED) + ws.set_cell(row, column + 1, UNUSED) + else: + ws.set_cell( + row, + column, + etudiant.get_note( + self.fromScodoc.annee, self.fromScodoc.competence_id, periode + ), + base_signature, + ) + ws.set_cell( + row, + column + 1, + etudiant.get_res( + self.fromScodoc.annee, self.fromScodoc.competence_id, periode + ), + ) + column += 2 + ws.set_cell( + row, + column, + etudiant.get_note(self.fromScodoc.annee, self.fromScodoc.competence_id), + base_signature, + ) + ws.set_cell( + row, + column + 1, + etudiant.get_res(self.fromScodoc.annee, self.fromScodoc.competence_id), + ) + return column + 2 + + @staticmethod + def generate_blank_data(ws: ScoExcelSheet, row: int, column: int) -> int: + ws.set_cell(row, column + 0, "UN1") + ws.set_cell(row, column + 1, "UR1") + ws.set_cell(row, column + 2, "UN2") + ws.set_cell(row, column + 3, "UR2") + ws.set_cell(row, column + 4, "R1") + ws.set_cell(row, column + 5, "R2") + column += 6 + return column + + @staticmethod + def generate_blank_header(ws: ScoExcelSheet, column: int, annee: str): + rcue_signature = FMT.FILL_BGCOLOR.write( + value=header_colors[annee]["RCUE"].value, + signature=base_signature, + ) + ue_signature = FMT.FILL_BGCOLOR.write( + value=header_colors[annee]["UE"].value, + signature=base_signature, + ) + merge = ws.get_merge_engine(start_row=2, start_column=column) + frame = ws.get_frame_engine( + start_row=2, start_column=column, thickness=SCO_BORDERTHICKNESS.BORDER_THIN + ) + ws.set_cell(2, column, UNUSED, from_signature=rcue_signature) + for ue in ["UE1", "UE2"]: + frame_ue = ws.get_frame_engine( + start_row=3, + start_column=column, + thickness=SCO_BORDERTHICKNESS.BORDER_THIN, + color=SCO_COLORS.GREY, + ) + merge_ue = ws.get_merge_engine(start_row=3, start_column=column) + ws.set_cell(3, column, UNUSED, ue_signature) + ws.set_cell(4, column, "Note", ue_signature) + ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True) + column += 1 + ws.set_cell(4, column, "Rés.", ue_signature) + ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True) + column += 1 + merge_ue.close(end_row=3, end_column=column - 1) + frame_ue.close(end_row=4, end_column=column - 1) + merge_rcue = ws.get_merge_engine(start_row=3, start_column=column) + ws.set_cell(3, column, UNUSED, rcue_signature) + ws.set_cell(4, column, UNUSED, rcue_signature) + ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True) + column += 1 + ws.set_cell(4, column, UNUSED, rcue_signature) + ws.set_column_dimension_hidden(ScoExcelSheet.i2col(column - 1), True) + column += 1 + merge_rcue.close(end_row=3, end_column=column - 1) + frame.close(end_row=4, end_column=column - 1) + merge.close(end_row=2, end_column=column - 1) + return column + + def generate_header(self, ws: ScoExcelSheet, column: int): + rcue_signature = FMT.FILL_BGCOLOR.write( + value=header_colors[self.fromScodoc.annee]["RCUE"].value, + signature=base_signature, + ) + ue_signature = FMT.FILL_BGCOLOR.write( + value=header_colors[self.fromScodoc.annee]["UE"].value, + signature=base_signature, + ) + merge = ws.get_merge_engine(start_row=2, start_column=column) + frame = ws.get_frame_engine( + start_row=2, start_column=column, thickness=SCO_BORDERTHICKNESS.BORDER_THIN + ) + ws.set_cell( + 2, column, self.fromScodoc.competence.titre, from_signature=rcue_signature + ) + for periode in [1, 2]: + ue = self.ues[periode] + frame_ue = ws.get_frame_engine( + start_row=3, + start_column=column, + thickness=SCO_BORDERTHICKNESS.BORDER_THIN, + color=SCO_COLORS.GREY, + ) + merge_ue = ws.get_merge_engine(start_row=3, start_column=column) + if ue is None: + ws.set_cell(3, column, "XXX", ue_signature) + else: + ws.set_cell(3, column, ue.acronyme, ue_signature) + ws.set_cell(4, column, "Note", ue_signature) + column += 1 + ws.set_cell(4, column, "Rés.", ue_signature) + column += 1 + merge_ue.close(end_row=3, end_column=column - 1) + frame_ue.close(end_row=4, end_column=column - 1) + merge_rcue = ws.get_merge_engine(start_row=3, start_column=column) + ws.set_cell(3, column, "Competence", rcue_signature) + ws.set_cell(4, column, "Note", rcue_signature) + column += 1 + ws.set_cell(4, column, "Rés.", rcue_signature) + column += 1 + merge_rcue.close(end_row=3, end_column=column - 1) + frame.close(end_row=4, end_column=column - 1) + merge.close(end_row=2, end_column=column - 1) + return column + + def get_ues(self, etudiant): + """get list of candidates UEs for Niveau""" + ues = [None, None] + for inscription in etudiant.cursus.inscriptions: + formation_id = inscription.formsemestre.formation_id + semestre_idx = inscription.formsemestre.semestre_id + if semestre_idx in self.ues: + # identifier les ues cocernées + ues[periode(semestre_idx)] = inscription.formsemestre + return ues + + +class CompetenceDesc: + def __init__(self, scodocCompetence): + self.fromScodoc: ApcCompetence = scodocCompetence + self.niveaux = {} + for scodocNiveau in scodocCompetence.niveaux.all(): + self.niveaux[scodocNiveau.id] = NiveauDesc(scodocNiveau) + + def getNiveauDesc(self, niveau_id): + return self.niveaux[niveau_id] + + def getNiveaux(self, codeAnnee): + niveaux = [] + for niveau_id, niveauDesc in self.niveaux.items(): + if codeAnnee == niveauDesc.fromScodoc.annee: + niveaux.append(niveauDesc) + return niveaux + + +class FormsemestreDesc: + def __init__(self, formsemestre_id, formsemestre=None): + self.formsemestre_id = formsemestre_id + self.formsemestre = formsemestre + self.resultats = None + + def get_desc(self, competence_id=None): + if competence_id is None: + return self + return self.ues.get(competence_id, None) + + def get_formsemestre(self): + if self.formsemestre is None: + self.formsemestre = FormSemestre.get(self.formsemestre_id) + return self.formsemestre + + def get_resultats(self): + if self.resultats is None: + self.resultats = res_sem.load_formsemestre_results(self.get_formsemestre()) + return self.resultats + + +class AnneeDesc: + def __init__(self, codeAnnee): + self.codeAnnee = codeAnnee + self.niveaux = {} + + def get_desc(self, competence_id=None, periode=None): + if competence_id is None and periode is None: + return self + elif competence_id is None: + return self.formsemestres.get(periode, None) + elif competence_id in self.niveaux: + return self.niveaux[competence_id].get_desc(periode) + else: + return None + + def addNiveau(self, niveaux): + for niveau in niveaux: + competence_id = niveau.fromScodoc.competence_id + self.niveaux[competence_id] = niveau + + def generate_blank_niveau(self, ws: ScoExcelSheet, column: int): + return column + + def generate_data( + self, ws: ScoExcelSheet, row: int, etudiant: "EtudiantJury", column: int + ) -> int: + for niveau in self.niveaux.values(): + column = niveau.generate_data(ws, row, etudiant, column) + for i in range(len(self.niveaux), 6): + column = NiveauDesc.generate_blank_data(ws, row, column) + ws.set_cell(row, column + 0, "A1") + ws.set_cell(row, column + 1, etudiant.get_note(self.codeAnnee), base_signature) + ws.set_cell(row, column + 2, etudiant.get_res(self.codeAnnee)) + column += 3 + if self.codeAnnee == "BUT2": + # ws.set_cell(row, column, etudiant.getDipl("BUT2")) + column += 1 + if self.codeAnnee == "BUT3": + # ws.set_cell(row, column, etudiant.getDipl("BUT3")) + column += 1 + return column + + def generate_header(self, ws: ScoExcelSheet, column: int): + start = column + but_signature = FMT.FILL_BGCOLOR.write( + signature=base_signature, value=header_colors[self.codeAnnee]["BUT"].value + ) + merge = ws.get_merge_engine(start_row=1, start_column=column) + frame = ws.get_frame_engine( + start_row=1, start_column=column, thickness=SCO_BORDERTHICKNESS.BORDER_THICK + ) + ws.set_cell( + 1, + column, + text=self.codeAnnee, + from_signature=but_signature, + composition=[ + (FMT.BORDER_LEFT_COLOR, SCO_COLORS.BLACK.value), + (FMT.BORDER_LEFT_STYLE, SCO_BORDERTHICKNESS.BORDER_MEDIUM.value), + ], + ) + for niveau in self.niveaux.values(): + column = niveau.generate_header(ws, column) + for i in range(len(self.niveaux), 6): + column = NiveauDesc.generate_blank_header(ws, column, self.codeAnnee) + merge_annee = ws.get_merge_engine(start_row=2, start_column=column) + ws.set_cell(2, column, "Année", from_signature=but_signature) + # cell_format(ws.cell(2, column), but_signature, [(FMT.FONT_BOLD, True)]) + ws.set_cell(3, column, "Nb", from_signature=but_signature) + ws.set_cell(4, column, "RCUE", from_signature=but_signature) + column += 1 + ws.set_cell(3, column, from_signature=but_signature) + ws.set_cell(3, column, "Moy.", from_signature=but_signature) + ws.set_cell(4, column, f"Sem", from_signature=but_signature) + column += 1 + ws.set_cell(3, column, from_signature=but_signature) + ws.set_cell(4, column, "Rés.", from_signature=but_signature) + column += 1 + merge_annee.close(end_row=2, end_column=column - 1) + if self.codeAnnee == "BUT2": + ws.set_cell(2, column, "DUT", from_signature=but_signature) + ws.set_cell(3, column, from_signature=but_signature) + ws.set_cell(4, column, "Rés.", from_signature=but_signature) + column += 1 + if self.codeAnnee == "BUT3": + ws.set_cell(2, column, "BUT", from_signature=but_signature) + ws.set_cell(3, column, from_signature=but_signature) + ws.set_cell(4, column, "Rés.", from_signature=but_signature) + column += 1 + frame.close(end_row=4, end_column=column - 1) + merge.close(end_row=1, end_column=column - 1) + return column + + +class ParcoursDesc: + signature_header = FMT.compose( + [ + (FMT.FILL_BGCOLOR, SCO_COLORS.LIGHT_YELLOW.value), + (FMT.NUMBER_FORMAT, SCO_NUMBER_FORMAT.NUMBER_GENERAL.value), + # (FMT.FONT_BOLD, True), + # (FMT.FONT_SIZE, SCO_FONTSIZE.FONTSIZE_13.value), + # (FMT.ALIGNMENT_VALIGN, SCO_VALIGN.VALIGN_CENTER.value), + # (FMT.ALIGNMENT_HALIGN, SCO_HALIGN.HALIGN_CENTER.value), + # (FMT.BORDER_RIGHT, HAIR_BLACK), + # (FMT.BORDER_LEFT, HAIR_BLACK), + # (FMT.BORDER_TOP, HAIR_BLACK), + # (FMT.BORDER_BOTTOM, HAIR_BLACK), + ], + base_signature, + ) + + def __init__(self, formation, scodocParcour: ApcParcours = None): + self.fromScodoc: ApcParcours = scodocParcour # None pour le tronc commun 'TC' + self.etudiants = [] + self.competences = {} + self.annees = {} + if scodocParcour is None: + for ( + scodocCompetence + ) in formation.referentiel_competence.get_competences_tronc_commun(): + self.competences[scodocCompetence.id] = CompetenceDesc(scodocCompetence) + else: + query = formation.query_competences_parcour(scodocParcour) + if not query is None: + for scodocCompetence in query.all(): + self.competences[scodocCompetence.id] = CompetenceDesc( + scodocCompetence + ) + for codeAnnee in liste_annees: + annee_desc = AnneeDesc(codeAnnee) + for competence_id, competence in self.competences.items(): + annee_desc.addNiveau(competence.getNiveaux(codeAnnee)) + self.annees[codeAnnee] = annee_desc + + def get_desc(self, annee=None, competence_id=None, periode=None): + if annee is None: + return self + if annee in self.annees: + return self.annees[annee].get_desc(competence_id, periode) + return None + + def add_etudiant(self, etudiant): + if not etudiant in self.etudiants: + self.etudiants.append(etudiant) + + def getNiveauDesc(self, competence_id, niveau_id): + return self.competences[competence_id].getNiveauDesc(niveau_id) + + def getData(self): + data = [] + for etudiant in self.etudiants: + data.append(etudiant.get_data()) + return data + + def append_title_column(self, worksheet, cells, val1, val2, val3, val4): + cells1, cells2, cells3, cells4 = cells + cells1.append(worksheet.make_cell(val1)) + cells2.append(worksheet.make_cell(val2)) + cells3.append(worksheet.make_cell(val3)) + cells4.append(worksheet.make_cell(val4)) + + def handle_description( + self, ws: ScoExcelSheet, description: tuple, row: int, column: int + ) -> int: + title, content_list = description + frame_thickness, frame_color = [ + (SCO_BORDERTHICKNESS.BORDER_THICK, SCO_COLORS.BLACK), + (SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK), + (SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK), + (SCO_BORDERTHICKNESS.NONE, SCO_COLORS.NONE), + ][row - 1] + frame = ws.get_frame_engine( + start_row=row, + start_column=column, + thickness=frame_thickness, + color=frame_color, + ) + ws.set_cell( + row=row, + column=column, + text=title, + from_signature=self.signature_header, + ) + merge_h = ws.get_merge_engine(start_row=row, start_column=column) + merge_v = ws.get_merge_engine(start_row=row, start_column=column) + if content_list is None: + merge_v.close(end_row=4) + column += 1 + else: + for content in content_list: + column = self.handle_description(ws, content, row + 1, column) + merge_h.close(end_column=column - 1) + frame.close(4, column - 1) + return column + + def generate_etudiant_header(self, ws: ScoExcelSheet) -> int: + titles = ( + "ETUDIANT", + [ + ("id", None), + ("nip", None), + ("Civ", None), + ("nom", None), + ("prenom", None), + ("parcours", None), + ("cursus", None), + ( + "absences", + [ + ("Tot.", None), + ("Non", [("Just.", None)]), + ], + ), + ], + ) + column = self.handle_description(ws, titles, 1, 1) + return column + + def generate_header(self, ws: ScoExcelSheet): + column: int = self.generate_etudiant_header(ws) + for codeAnnee in liste_annees: + column = self.annees[codeAnnee].generate_header(ws, column) + + def generate(self, workbook: ScoExcelBook): + if self.fromScodoc: + sheet_name = self.fromScodoc.code + else: + sheet_name = "TC" + worksheet: ScoExcelSheet = workbook.create_sheet(sheet_name) + self.generate_header(worksheet) + self.generate_etudiants(worksheet) + + def generate_data( + self, ws: ScoExcelSheet, row: int, column: int, etudiant: "EtudiantJury" + ): + for codeAnnee in liste_annees: + column = self.annees[codeAnnee].generate_data(ws, row, etudiant, column) + + def generate_etudiants(self, ws: ScoExcelSheet): + ligne = 5 + for etudiant in self.etudiants: + ws.set_cell(ligne, 1, etudiant.ident.id) + ws.set_cell(ligne, 2, etudiant.ident.code_nip) + ws.set_cell(ligne, 3, etudiant.ident.civilite) + ws.set_cell(ligne, 4, etudiant.ident.nom) + ws.set_cell(ligne, 5, etudiant.ident.prenom) + if etudiant.parcour: + ws.set_cell(ligne, 6, etudiant.parcour.code) + else: + ws.set_cell(ligne, 6, "-") + cursus = ", ".join(etudiant.history) + ws.set_cell(ligne, 7, cursus) + ws.set_cell(ligne, 8, etudiant.nbabs) + ws.set_cell(ligne, 9, etudiant.nbabs - etudiant.nbabsjust) + # self.generate_data(ws, ligne, 10, etudiant) + ligne = ligne + 1 diff --git a/app/but/prepajury_xl.py b/app/but/prepajury_xl.py new file mode 100644 index 000000000..007022ae4 --- /dev/null +++ b/app/but/prepajury_xl.py @@ -0,0 +1,410 @@ +# -*- 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 +# +############################################################################## + +from __future__ import annotations + +from collections import defaultdict + +from openpyxl.cell import WriteOnlyCell +from openpyxl.worksheet.worksheet import Worksheet + +from app.but.prepajury_xl_format import ( + Sco_Style, + FMT, + SCO_FONTNAME, + SCO_FONTSIZE, + SCO_HALIGN, + SCO_VALIGN, + SCO_NUMBER_FORMAT, + SCO_BORDERTHICKNESS, + SCO_COLORS, + fmt_atomics, +) + +""" Excel file handling +""" +import datetime +from tempfile import NamedTemporaryFile + +import openpyxl.utils.datetime +from openpyxl.styles.numbers import FORMAT_DATE_DDMMYY +from openpyxl.comments import Comment +from openpyxl import Workbook + + +import app.scodoc.sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError + + +# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante: +# font, border, number_format, fill,... +# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles) + +base_signature = ( + FMT.FONT_NAME.set(SCO_FONTNAME.FONTNAME_CALIBRI) + + FMT.FONT_SIZE.set(SCO_FONTSIZE.FONTSIZE_13) + + FMT.ALIGNMENT_HALIGN.set(SCO_HALIGN.HALIGN_CENTER) + + FMT.ALIGNMENT_VALIGN.set(SCO_VALIGN.VALIGN_CENTER) + + FMT.NUMBER_FORMAT.set(SCO_NUMBER_FORMAT.NUMBER_GENERAL) +) + + +class Sco_Cell: + def __init__(self, text: str = "", signature: int = 0): + self.text = text + self.signature = signature + + def alter(self, signature: int): + for fmt in fmt_atomics: + value: int = fmt.composante.read(signature) + if value > 0: + self.signature = fmt.write(value, self.signature) + + def build(self, ws: Worksheet, row: int, column: int): + cell = ws.cell(row, column) + cell.value = self.text + FMT.ALL.apply(cell=cell, signature=self.signature) + + +class Frame_Engine: + def __init__( + self, + ws: ScoExcelSheet, + start_row: int = 1, + start_column: int = 1, + thickness: SCO_BORDERTHICKNESS = SCO_BORDERTHICKNESS.NONE, + color: SCO_COLORS = SCO_COLORS.BLACK, + ): + self.start_row: int = start_row + self.start_column: int = start_column + self.ws: ScoExcelSheet = ws + self.border_style = FMT.BORDER_LEFT.make_zero_based_constant([thickness, color]) + + def close(self, end_row: int, end_column: int): + left_signature: int = FMT.BORDER_LEFT.write(self.border_style) + right_signature: int = FMT.BORDER_RIGHT.write(self.border_style) + top_signature: int = FMT.BORDER_TOP.write(self.border_style) + bottom_signature: int = FMT.BORDER_BOTTOM.write(self.border_style) + for row in range(self.start_row, end_row + 1): + self.ws.cells[row][self.start_column].alter(left_signature) + self.ws.cells[row][end_column].alter(right_signature) + for column in range(self.start_column, end_column + 1): + self.ws.cells[self.start_row][column].alter(top_signature) + self.ws.cells[end_row][column].alter(bottom_signature) + + +class Merge_Engine: + def __init__(self, start_row: int = 1, start_column: int = 1): + self.start_row: int = start_row + self.start_column: int = start_column + self.end_row: int = None + self.end_column: int = None + self.closed: bool = False + + def close(self, end_row=None, end_column=None): + if end_row is None: + self.end_row = self.start_row + 1 + else: + self.end_row = end_row + 1 + if end_column is None: + self.end_column = self.start_column + 1 + else: + self.end_column = end_column + 1 + self.closed = True + + def write(self, ws: Worksheet): + if self.closed: + if (self.end_row - self.start_row > 0) and ( + self.end_column - self.start_column > 0 + ): + ws.merge_cells( + start_row=self.start_row, + start_column=self.start_column, + end_row=self.end_row - 1, + end_column=self.end_column - 1, + ) + + def __repr__(self): + return f"( {self.start_row}:{self.start_column}-{self.end_row}:{self.end_column})[{self.closed}]" + + +def xldate_as_datetime(xldate, datemode=0): + """Conversion d'une date Excel en datetime python + Deux formats de chaîne acceptés: + * JJ/MM/YYYY (chaîne naïve) + * Date ISO (valeur de type date lue dans la feuille) + Peut lever une ValueError + """ + try: + return datetime.datetime.strptime(xldate, "%d/%m/%Y") + except: + return openpyxl.utils.datetime.from_ISO8601(xldate) + + +def adjust_sheetname(sheet_name): + """Renvoie un nom convenable pour une feuille excel: < 31 cars, sans caractères spéciaux + Le / n'est pas autorisé par exemple. + Voir https://xlsxwriter.readthedocs.io/workbook.html#add_worksheet + """ + sheet_name = scu.make_filename(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: + wb = ScoExcelBook() + ws0 = wb.create_sheet('sheet name 0') + ws1 = wb.create_sheet('sheet name 1') + ... + steam = wb.generate() + """ + + def __init__(self): + self.sheets = [] # list of sheets + self.wb = Workbook() + + def create_sheet(self, sheet_name="feuille", default_signature: int = 0): + """Crée une nouvelle feuille dans ce classeur + sheet_name -- le nom de la feuille + default_style -- le style par défaut + """ + sheet_name = adjust_sheetname(sheet_name) + ws = self.wb.create_sheet(sheet_name) + sheet = ScoExcelSheet(sheet_name, default_signature=default_signature, ws=ws) + self.sheets.append(sheet) + return sheet + + def generate(self): + """génération d'un stream binaire représentant la totalité du classeur. + retourne le flux + """ + sheet: Worksheet = self.wb.get_sheet_by_name("Sheet") + self.wb.remove_sheet(sheet) + + for sheet in self.sheets: + sheet.prepare() + # construction d'un flux + # (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream) + with NamedTemporaryFile() as tmp: + self.wb.save(tmp.name) + tmp.seek(0) + return tmp.read() + + +class ScoExcelSheet: + """Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook. + En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations + est imposé: + * instructions globales (largeur/maquage des colonnes et ligne, ...) + * construction et ajout des cellules et ligne selon le sens de lecture (occidental) + ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..) + * pour finir appel de la méthode de génération + """ + + def __init__( + self, + sheet_name: str = "feuille", + default_signature: int = 0, + ws: Worksheet = 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 elle crée son propre wb), sinon c'est la worksheet + créée par le workbook propriétaire un workbook est créé 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 = adjust_sheetname(sheet_name) + self.default_signature = default_signature + self.merges: list[Merge_Engine] = [] + 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.cells = defaultdict(lambda: defaultdict(Sco_Cell)) + self.column_dimensions = {} + self.row_dimensions = {} + + def get_frame_engine( + self, + start_row: int, + start_column: int, + thickness: SCO_BORDERTHICKNESS = SCO_BORDERTHICKNESS.NONE, + color: SCO_COLORS = SCO_COLORS.NONE, + ): + return Frame_Engine( + ws=self, + start_row=start_row, + start_column=start_column, + thickness=thickness, + color=color, + ) + + def get_merge_engine(self, start_row: int, start_column: int): + merge_engine: Merge_Engine = Merge_Engine( + start_row=start_row, start_column=start_column + ) + self.merges.append(merge_engine) + return merge_engine + + @staticmethod + def i2col(idx): + if idx < 26: # one letter key + return chr(idx + 65) + else: # two letters AA..ZZ + first = idx // 26 + second = idx % 26 + return "" + chr(first + 64) + chr(second + 65) + + def set_cell( + self, + row: int, + column: int, + text: str = "", + from_signature: int = 0, + composition: list = [], + ): + cell: Sco_Cell = self.cells[row][column] + cell.text = text + cell.alter(FMT.compose(composition, from_signature)) + + 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) + """ + 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 isinstance(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_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.row_dimensions[cle].hidden = value + + def set_column_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 + + # def make_cell(self, value: any = None, style: Sco_Style = None, comment=None): + # """Construit une cellule. + # value -- contenu de la cellule (texte, numérique, booléen ou date) + # style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié + # """ + # # adaptation des valeurs si nécessaire + # if value is None: + # value = "" + # elif value is True: + # value = 1 + # elif value is False: + # value = 0 + # elif isinstance(value, datetime.datetime): + # value = value.replace( + # tzinfo=None + # ) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones) + # + # # création de la cellule + # cell = WriteOnlyCell(self.ws, value) + # + # if style is not None: + # style.apply(cell) + # + # if not comment is None: + # cell.comment = Comment(comment, "scodoc") + # lines = comment.splitlines() + # cell.comment.width = 7 * max([len(line) for line in lines]) if lines else 7 + # cell.comment.height = 20 * len(lines) if lines else 20 + # + # # test datatype to overwrite datetime format + # if isinstance(value, datetime.date): + # cell.data_type = "d" + # cell.number_format = FORMAT_DATE_DDMMYY + # elif isinstance(value, int) or isinstance(value, float): + # cell.data_type = "n" + # else: + # cell.data_type = "s" + # + # return cell + + 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 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) + for row in self.cells: + for column in self.cells[row]: + self.cells[row][column].build(self.ws, row, column) + for merge_engine in self.merges: + merge_engine.write(self.ws) + + def generate(self): + """génération d'un classeur mono-feuille""" + # 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() diff --git a/app/but/prepajury_xl_format.py b/app/but/prepajury_xl_format.py new file mode 100644 index 000000000..5579af127 --- /dev/null +++ b/app/but/prepajury_xl_format.py @@ -0,0 +1,666 @@ +import abc +from enum import Enum + +import openpyxl.styles +from openpyxl.cell import Cell +from openpyxl.styles import Side, Border, Font, PatternFill, Alignment +from openpyxl.styles.numbers import FORMAT_GENERAL, FORMAT_NUMBER_00 + +# Formatting Enums + + +class SCO_COLORS(Enum): + def __new__(cls, value, argb): + obj = object.__new__(cls) + obj._value_ = value + obj.argb = argb + return obj + + NONE = (0, None) + BLACK = (1, "FF000000") + WHITE = (2, "FFFFFFFF") + RED = (3, "FFFF0000") + BROWN = (4, "FF993300") + PURPLE = (5, "FF993366") + BLUE = (6, "FF0000FF") + ORANGE = (7, "FFFF3300") + LIGHT_YELLOW = (8, "FFFFFF99") + GREEN = (9, "FF00FF00") + GREY = (10, "FF101010") + BUT1 = (23, "FF95B3D7") + RCUE1 = (24, "FFB8CCE4") + UE1 = (25, "FFDCE6F1") + BUT2 = (26, "FFC4D79B") + RCUE2 = (27, "FFD8E4BC") + UE2 = (28, "FFEBF1DE") + BUT3 = (29, "FFFABF8F") + RCUE3 = (30, "FFFCD5B4") + UE3 = (31, "FFFDE9D9") + + +class SCO_BORDERTHICKNESS(Enum): + def __new__(cls, value, width): + obj = object.__new__(cls) + obj._value_ = value + obj.width = width + return obj + + NONE = (0, None) + BORDER_HAIR = (1, "hair") + BORDER_THIN = (2, "thin") + BORDER_MEDIUM = (3, "medium") + BORDER_THICK = (4, "thick") + + +class SCO_FONTNAME(Enum): + def __new__(cls, value, fontname): + obj = object.__new__(cls) + obj._value_ = value + obj.fontname = fontname + return obj + + NONE = (0, None) + FONTNAME_CALIBRI = (1, "Calibri") + FONTNAME_ARIAL = (2, "Arial") + FONTNAME_COURIER = (3, "Courier New") + FONTNAME_TIMES = (4, "Times New Roman") + + +class SCO_FONTSIZE(Enum): + def __new__(cls, value, fontsize): + obj = object.__new__(cls) + obj._value_ = value + obj.fontsize = fontsize + return obj + + NONE = (0, None) + FONTSIZE_9 = (1, 9.0) + FONTSIZE_10 = (2, 10.0) + FONTSIZE_11 = (2, 11.0) + FONTSIZE_13 = (4, 13.0) + + +class SCO_NUMBER_FORMAT(Enum): + def __new__(cls, value, format): + obj = object.__new__(cls) + obj._value_ = value + obj.format = format + return obj + + NONE = (0, None) + NUMBER_GENERAL = (0, FORMAT_GENERAL) + NUMBER_00 = (1, FORMAT_NUMBER_00) + NUMBER_0 = (2, "0.0") + + +class SCO_HALIGN(Enum): + def __new__(cls, value, position): + obj = object.__new__(cls) + obj._value_ = value + obj.position = position + return obj + + NONE = (0, None) + HALIGN_LEFT = (1, "left") + HALIGN_CENTER = (2, "center") + HALIGN_RIGHT = (3, "right") + + +class SCO_VALIGN(Enum): + def __new__(cls, value, position): + obj = object.__new__(cls) + obj._value_ = value + obj.position = position + return obj + + VALIGN_BOTTOM = (0, "bottom") + VALIGN_TOP = (1, "top") + VALIGN_CENTER = (2, "center") + + +# Composante (bitfield) atomique. Based on Enums +free = 0 + + +class Composante(abc.ABC): + def __init__(self, base=None, width: int = 1): + global free + if base is None: + self.base = free + free += width + else: + self.base = base + self.width = width + self.end = self.base + self.width + self.mask = ((1 << width) - 1) << self.base + + def read(self, signature: int) -> int: + return (signature & self.mask) >> self.base + + def clear(self, signature: int) -> int: + return signature & ~self.mask + + def write(self, index, signature=0) -> int: + return self.clear(signature) + (index << self.base) + + def make_zero_based_constant(self, enums: list[Enum] = None): + return 0 + + @abc.abstractmethod + def build(self, value: int): + pass + + +class Composante_boolean(Composante): + def __init__(self): + super().__init__(width=1) + + def set(self, data: bool, signature=0) -> int: + return self.write(1 if data else 0, signature) + + def build(self, signature) -> bool: + value = self.read(signature) + assert value < (1 << self.width) + return value == 1 + + +class Composante_number_format(Composante): + WIDTH: int = 2 + + def __init__(self): + assert (1 << self.WIDTH) > SCO_NUMBER_FORMAT.__len__() + super().__init__(width=self.WIDTH) + + def set(self, data: SCO_NUMBER_FORMAT, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_NUMBER_FORMAT: + value = self.read(signature) + assert value < (1 << self.width) + return SCO_NUMBER_FORMAT(value) + + +class Composante_Colors(Composante): + WIDTH: int = 5 + + def __init__(self, default: SCO_COLORS = SCO_COLORS.BLACK): + assert (1 << self.WIDTH) > SCO_COLORS.__len__() + super().__init__(width=self.WIDTH) + self.default: SCO_COLORS = default + + def set(self, data: SCO_COLORS, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_COLORS: + value = self.read(signature) + assert value < (1 << self.width) + if value == 0: + return None + try: + return SCO_COLORS(value) + except: + return self.default + + +class Composante_borderThickness(Composante): + WIDTH: int = 3 + + def __init__(self): + assert (1 << self.WIDTH) > SCO_BORDERTHICKNESS.__len__() + super().__init__(width=self.WIDTH) + + def set(self, data: SCO_BORDERTHICKNESS, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_BORDERTHICKNESS: + value = self.read(signature) + assert value < (1 << self.width) + try: + return SCO_BORDERTHICKNESS(value) + except: + return None + + +class Composante_fontname(Composante): + WIDTH: int = 3 + + def __init__(self): + assert (1 << self.WIDTH) > SCO_FONTNAME.__len__() + super().__init__(width=self.WIDTH) + + def set(self, data: SCO_FONTNAME, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_FONTNAME: + value = self.read(signature) + assert value < (1 << self.width) + try: + return SCO_FONTNAME(value) + except: + return SCO_FONTNAME.FONTNAME_CALIBRI + + +class Composante_fontsize(Composante): + WIDTH: int = 3 + + def __init__(self): + assert (1 << self.WIDTH) > SCO_FONTSIZE.__len__() + super().__init__(width=self.WIDTH) + + def set(self, data: SCO_FONTSIZE, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_FONTSIZE: + value = self.read(signature) + assert value < (1 << self.width) + return SCO_FONTSIZE(value) or None + + +class Composante_halign(Composante): + WIDTH: int = 3 + + def __init__(self): + assert (1 << self.WIDTH) > SCO_HALIGN.__len__() + super().__init__(width=self.WIDTH) + + def set(self, data: SCO_HALIGN, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_HALIGN: + value = self.read(signature) + assert value < (1 << self.width) + try: + return SCO_HALIGN(value) + except: + return SCO_HALIGN.HALIGN_LEFT + + +class Composante_valign(Composante): + WIDTH: int = 3 + + def __init__(self): + assert (1 << self.WIDTH) > SCO_VALIGN.__len__() + super().__init__(width=self.WIDTH) + + def set(self, data: SCO_VALIGN, signature=0) -> int: + return self.write(data.value, signature) + + def build(self, signature: int) -> SCO_VALIGN: + value = self.read(signature) + assert value < (1 << self.width) + try: + return SCO_VALIGN(value) + except: + return SCO_VALIGN.VALIGN_CENTER + + +# Formatting objects + + +class Sco_Fill: + def __init__(self, color: SCO_COLORS): + self.color = color + + def to_openpyxl(self): + return PatternFill( + fill_type="solid", + fgColor=None if self.color is None else self.color.argb, + ) + + +class Sco_BorderSide: + def __init__( + self, + thickness: SCO_BORDERTHICKNESS = None, + color: SCO_COLORS = SCO_COLORS.WHITE, + ): + self.thickness = thickness + self.color: SCO_COLORS = color + + def to_openpyxl(self): + return Side( + border_style=None if self.thickness is None else self.thickness.width, + color=None if self.color is None else self.color.argb, + ) + + +class Sco_Borders: + def __init__( + self, + left: Sco_BorderSide = None, + right: Sco_BorderSide = None, + top: Sco_BorderSide = None, + bottom: Sco_BorderSide = None, + ): + self.left = left + self.right = right + self.top = top + self.bottom = bottom + + def to_openpyxl(self): + return Border( + left=self.left.to_openpyxl(), + right=self.right.to_openpyxl(), + top=self.top.to_openpyxl(), + bottom=self.bottom.to_openpyxl(), + ) + + +class Sco_Alignment: + def __init__( + self, + halign: SCO_HALIGN = None, + valign: SCO_VALIGN = None, + ): + self.halign = halign + self.valign = valign + + def to_openpyxl(self): + return Alignment( + horizontal=None if self.halign is None else self.halign.position, + vertical=None if self.valign is None else self.valign.position, + ) + + +class Sco_Font: + def __init__( + self, + name: SCO_FONTNAME = SCO_FONTNAME(0), + fontsize: SCO_FONTSIZE = SCO_FONTSIZE(0), + bold: bool = None, + italic: bool = None, + outline: bool = None, + color: "SCO_COLORS" = None, + ): + self.name = name + self.bold = bold + self.italic = italic + self.outline = outline + self.color = color + self.fontsize = fontsize + + def to_openpyxl(self): + return Font( + name=None if self.name is None else self.name.fontname, + size=None if self.fontsize is None else self.fontsize.fontsize, + bold=self.bold, + italic=self.italic, + outline=self.outline, + color=None if self.color is None else self.color.argb, + ) + + +class Sco_Style: + from openpyxl.cell import Cell + + def __init__( + self, + font: Sco_Font = None, + fill: Sco_Fill = None, + alignment: Sco_Alignment = None, + borders: Sco_Borders = None, + number_format: SCO_NUMBER_FORMAT = FORMAT_GENERAL, + ): + self.font = font or None + self.fill = fill or None + self.alignment = alignment or None + self.borders = borders or None + self.number_format = number_format or SCO_NUMBER_FORMAT.NUMBER_GENERAL + + def apply(self, cell: Cell): + if self.font: + cell.font = self.font.to_openpyxl() + if self.fill and self.fill.color: + cell.fill = self.fill.to_openpyxl() + if self.alignment: + cell.alignment = self.alignment.to_openpyxl() + if self.borders: + cell.border = self.borders.to_openpyxl() + cell.number_format = ( + FORMAT_GENERAL + if self.number_format is None + or self.number_format == SCO_NUMBER_FORMAT.NONE + else self.number_format.format + ) + + +# Composantes groupant d'autres composantes et dotées d'un mécanisme de cache + + +class Composante_group(Composante): + def __init__(self, composantes: list[Composante]): + self.composantes = composantes + self.cache = {} + mini = min([comp.base for comp in composantes]) + maxi = max([comp.end for comp in composantes]) + width = sum([comp.width for comp in composantes]) + if not width == (maxi - mini): + raise Exception("Composante group non complete ou non connexe") + super().__init__(base=mini, width=width) + + def lookup_or_cache(self, signature: int): + value = self.read(signature) + assert value < (1 << self.width) + if not value in self.cache: + self.cache[value] = self.build(signature) + return self.cache[value] + + def make_zero_based_constant(self, enums: list[Enum] = None) -> int: + if enums is None: + return 0 + signature = 0 + for enum, composante in zip(enums, self.composantes): + signature += composante.write(enum.value) + return signature >> self.base + + +class Composante_fill(Composante_group): + def __init__(self, color: Composante_Colors): + super().__init__([color]) + self.color = color + + def build(self, signature: int) -> Sco_Fill: + return Sco_Fill(color=self.color.build(signature)) + + +class Composante_font(Composante_group): + def __init__( + self, + name: Composante_fontname, + fontsize: Composante_fontsize, + color: Composante_Colors, + bold: Composante_boolean, + italic: Composante_boolean, + outline: Composante_boolean, + ): + super().__init__([name, fontsize, color, bold, italic, outline]) + self.name = name + self.fontsize = fontsize + self.color = color + self.bold = bold + self.italic = italic + self.outline = outline + + def build(self, signature: int) -> Sco_Font: + return Sco_Font( + name=self.name.build(signature), + fontsize=self.fontsize.build(signature), + color=self.color.build(signature), + bold=self.bold.build(signature), + italic=self.italic.build(signature), + outline=self.outline.build(signature), + ) + + +class Composante_border(Composante_group): + def __init__(self, thick: Composante_borderThickness, color: Composante_Colors): + super().__init__([thick, color]) + self.thick = thick + self.color = color + + def build(self, signature: int) -> Sco_BorderSide: + return Sco_BorderSide( + thickness=self.thick.build(signature), + color=self.color.build(signature), + ) + + +class Composante_borders(Composante_group): + def __init__( + self, + left: Composante_border, + right: Composante_border, + top: Composante_border, + bottom: Composante_border, + ): + super().__init__([left, right, top, bottom]) + self.left = left + self.right = right + self.top = top + self.bottom = bottom + + def build(self, signature: int) -> Sco_Borders: + return Sco_Borders( + left=self.left.lookup_or_cache(signature), + right=self.right.lookup_or_cache(signature), + top=self.top.lookup_or_cache(signature), + bottom=self.bottom.lookup_or_cache(signature), + ) + + +class Composante_alignment(Composante_group): + def __init__(self, halign: Composante_halign, valign: Composante_valign): + super().__init__([halign, valign]) + self.halign = halign + self.valign = valign + + def build(self, signature: int) -> Sco_Alignment: + return Sco_Alignment( + halign=self.halign.build(signature), + valign=self.valign.build(signature), + ) + + +class Composante_all(Composante_group): + def __init__( + self, + font: Composante_font, + fill: Composante_fill, + borders: Composante_borders, + alignment: Composante_alignment, + number_format: Composante_number_format, + ): + super().__init__([font, fill, borders, alignment, number_format]) + assert self.width < 64 + self.font = font + self.fill = fill + self.borders = borders + self.alignment = alignment + self.number_format = number_format + + def build(self, signature: int) -> Sco_Style: + return Sco_Style( + fill=self.fill.lookup_or_cache(signature), + font=self.font.lookup_or_cache(signature), + borders=self.borders.lookup_or_cache(signature), + alignment=self.alignment.lookup_or_cache(signature), + number_format=self.number_format.build(signature), + ) + + def get_style(self, signature: int): + return self.lookup_or_cache(signature) + + +class FMT(Enum): + def __init__(self, composante: Composante): + self.composante = composante + + def write(self, value, signature=0) -> int: + return self.composante.write(value, signature) + + def set(self, data, signature: int = 0) -> int: + return self.composante.set(data, signature) + + def get_style(self, signature: int): + return self.composante.lookup_or_cache(signature) + + def make_zero_based_constant(self, enums: list[Enum]) -> int: + return self.composante.make_zero_based_constant(enums=enums) + + def apply(self, cell: Cell, signature: int): + self.composante.build(signature).apply(cell) + + @classmethod + def compose(cls, composition: list[("FMT", int)], signature: int = 0) -> int: + for field, value in composition: + signature = field.write(value, field.composante.clear(signature)) + return signature + + @classmethod + def style(cls, signature: int = None) -> Sco_Style: + return FMT.ALL.get_style(signature) + + FONT_NAME = Composante_fontname() + FONT_SIZE = Composante_fontsize() + FONT_COLOR = Composante_Colors() + FONT_BOLD = Composante_boolean() + FONT_ITALIC = Composante_boolean() + FONT_OUTLINE = Composante_boolean() + BORDER_LEFT_STYLE = Composante_borderThickness() + BORDER_LEFT_COLOR = Composante_Colors() + BORDER_RIGHT_STYLE = Composante_borderThickness() + BORDER_RIGHT_COLOR = Composante_Colors() + BORDER_TOP_STYLE = Composante_borderThickness() + BORDER_TOP_COLOR = Composante_Colors() + BORDER_BOTTOM_STYLE = Composante_borderThickness() + BORDER_BOTTOM_COLOR = Composante_Colors() + FILL_BGCOLOR = Composante_Colors(None) + ALIGNMENT_HALIGN = Composante_halign() + ALIGNMENT_VALIGN = Composante_valign() + NUMBER_FORMAT = Composante_number_format() + FONT = Composante_font( + FONT_NAME, FONT_SIZE, FONT_COLOR, FONT_BOLD, FONT_ITALIC, FONT_OUTLINE + ) + FILL = Composante_fill(FILL_BGCOLOR) + BORDER_LEFT = Composante_border(BORDER_LEFT_STYLE, BORDER_LEFT_COLOR) + BORDER_RIGHT = Composante_border(BORDER_RIGHT_STYLE, BORDER_RIGHT_COLOR) + BORDER_TOP = Composante_border(BORDER_TOP_STYLE, BORDER_TOP_COLOR) + BORDER_BOTTOM = Composante_border(BORDER_BOTTOM_STYLE, BORDER_BOTTOM_COLOR) + BORDERS = Composante_borders(BORDER_LEFT, BORDER_RIGHT, BORDER_TOP, BORDER_BOTTOM) + ALIGNMENT = Composante_alignment(ALIGNMENT_HALIGN, ALIGNMENT_VALIGN) + ALL = Composante_all(FONT, FILL, BORDERS, ALIGNMENT, NUMBER_FORMAT) + + +fmt_atomics = { + FMT.FONT_NAME, + FMT.FONT_SIZE, + FMT.FONT_COLOR, + FMT.FONT_BOLD, + FMT.FONT_ITALIC, + FMT.FONT_OUTLINE, + FMT.BORDER_LEFT_STYLE, + FMT.BORDER_LEFT_COLOR, + FMT.BORDER_RIGHT_STYLE, + FMT.BORDER_RIGHT_COLOR, + FMT.BORDER_TOP_STYLE, + FMT.BORDER_TOP_COLOR, + FMT.BORDER_BOTTOM_STYLE, + FMT.BORDER_BOTTOM_COLOR, + FMT.FILL_BGCOLOR, + FMT.ALIGNMENT_HALIGN, + FMT.ALIGNMENT_VALIGN, + FMT.NUMBER_FORMAT, +} + +HAIR_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( + enums=[SCO_BORDERTHICKNESS.BORDER_HAIR, SCO_COLORS.BLACK] +) +THIN_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( + enums=[SCO_BORDERTHICKNESS.BORDER_THIN, SCO_COLORS.BLACK] +) +MEDIUM_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( + enums=[SCO_BORDERTHICKNESS.BORDER_MEDIUM, SCO_COLORS.BLACK] +) +THICK_BLACK: int = FMT.BORDER_LEFT.make_zero_based_constant( + enums=[SCO_BORDERTHICKNESS.BORDER_THICK, SCO_COLORS.BLACK] +)