reprise sco96

This commit is contained in:
Jean-Marie Place 2023-07-28 09:00:21 +02:00
parent f3ceaff307
commit 22c2fe0f3b
4 changed files with 2043 additions and 0 deletions

445
app/but/prepajury_but.py Normal file
View File

@ -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)

522
app/but/prepajury_desc.py Normal file
View File

@ -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

410
app/but/prepajury_xl.py Normal file
View File

@ -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()

View File

@ -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]
)