2024-01-25 17:17:01 +01:00
|
|
|
# -*- mode: python -*-
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
#
|
|
|
|
# Gestion scolarite IUT
|
|
|
|
#
|
|
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program; if not, write to the Free Software
|
|
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
#
|
|
|
|
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
|
|
|
#
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
##############################################################################
|
|
|
|
# Module "Avis de poursuite d'étude"
|
|
|
|
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
"""
|
|
|
|
Created on Thu Sep 8 09:36:33 2016
|
|
|
|
|
|
|
|
@author: barasc
|
|
|
|
"""
|
|
|
|
|
|
|
|
import os
|
|
|
|
import datetime
|
|
|
|
import re
|
|
|
|
import unicodedata
|
|
|
|
|
|
|
|
|
|
|
|
from flask import g
|
|
|
|
|
|
|
|
import app.scodoc.sco_utils as scu
|
2024-01-27 08:22:36 +01:00
|
|
|
|
2024-01-25 17:17:01 +01:00
|
|
|
from app.models import FormSemestre
|
|
|
|
from app.scodoc import sco_formsemestre
|
|
|
|
from app.scodoc.sco_logos import find_logo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Generated LaTeX files are encoded as:
|
|
|
|
PE_LATEX_ENCODING = "utf-8"
|
|
|
|
|
|
|
|
# /opt/scodoc/tools/doc_poursuites_etudes
|
|
|
|
REP_DEFAULT_AVIS = os.path.join(scu.SCO_TOOLS_DIR, "doc_poursuites_etudes/")
|
|
|
|
REP_LOCAL_AVIS = os.path.join(scu.SCODOC_CFG_DIR, "doc_poursuites_etudes/")
|
|
|
|
|
|
|
|
PE_DEFAULT_AVIS_LATEX_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_avis.tex"
|
|
|
|
PE_LOCAL_AVIS_LATEX_TMPL = REP_LOCAL_AVIS + "local/modeles/un_avis.tex"
|
|
|
|
PE_DEFAULT_FOOTER_TMPL = REP_DEFAULT_AVIS + "distrib/modeles/un_footer.tex"
|
|
|
|
PE_LOCAL_FOOTER_TMPL = REP_LOCAL_AVIS + "local/modeles/un_footer.tex"
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
"""
|
|
|
|
Descriptif d'un parcours classique BUT
|
|
|
|
|
|
|
|
TODO:: A améliorer si BUT en moins de 6 semestres
|
|
|
|
"""
|
|
|
|
|
|
|
|
PARCOURS = {
|
|
|
|
"S1": {
|
|
|
|
"aggregat": ["S1"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Semestre 1 (S1)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"S2": {
|
|
|
|
"aggregat": ["S2"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Semestre 2 (S2)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"1A": {
|
|
|
|
"aggregat": ["S1", "S2"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "BUT1 (S1+S2)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"S3": {
|
|
|
|
"aggregat": ["S3"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Semestre 3 (S3)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"S4": {
|
|
|
|
"aggregat": ["S4"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Semestre 4 (S4)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"2A": {
|
|
|
|
"aggregat": ["S3", "S4"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "BUT2 (S3+S4)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"3S": {
|
|
|
|
"aggregat": ["S1", "S2", "S3"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Moyenne du semestre 1 au semestre 3 (S1+S2+S3)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"4S": {
|
|
|
|
"aggregat": ["S1", "S2", "S3", "S4"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Moyenne du semestre 1 au semestre 4 (S1+S2+S3+S4)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"S5": {
|
|
|
|
"aggregat": ["S5"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Semestre 5 (S5)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"S6": {
|
|
|
|
"aggregat": ["S6"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Semestre 6 (S6)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"3A": {
|
|
|
|
"aggregat": ["S5", "S6"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "3ème année (S5+S6)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"5S": {
|
|
|
|
"aggregat": ["S1", "S2", "S3", "S4", "S5"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Moyenne du semestre 1 au semestre 5 (S1+S2+S3+S4+S5)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
"6S": {
|
|
|
|
"aggregat": ["S1", "S2", "S3", "S4", "S5", "S6"],
|
2024-02-02 06:11:21 +01:00
|
|
|
"descr": "Moyenne globale BUT (S1+S2+S3+S4+S5+S6)",
|
2024-01-25 17:17:01 +01:00
|
|
|
},
|
|
|
|
}
|
|
|
|
NBRE_SEMESTRES_DIPLOMANT = 6
|
|
|
|
AGGREGAT_DIPLOMANT = (
|
|
|
|
"6S" # aggrégat correspondant à la totalité des notes pour le diplôme
|
|
|
|
)
|
|
|
|
TOUS_LES_SEMESTRES = PARCOURS[AGGREGAT_DIPLOMANT]["aggregat"]
|
|
|
|
TOUS_LES_AGGREGATS = [cle for cle in PARCOURS.keys() if not cle.startswith("S")]
|
|
|
|
TOUS_LES_PARCOURS = list(PARCOURS.keys())
|
|
|
|
|
2024-01-25 21:54:22 +01:00
|
|
|
|
2024-01-25 17:17:01 +01:00
|
|
|
# ----------------------------------------------------------------------------------------
|
|
|
|
def calcul_age(born: datetime.date) -> int:
|
|
|
|
"""Calcule l'age connaissant la date de naissance ``born``. (L'age est calculé
|
|
|
|
à partir de l'horloge système).
|
|
|
|
|
|
|
|
Args:
|
|
|
|
born: La date de naissance
|
|
|
|
|
|
|
|
Return:
|
|
|
|
L'age (au regard de la date actuelle)
|
|
|
|
"""
|
|
|
|
if not born or not isinstance(born, datetime.date):
|
|
|
|
return None
|
|
|
|
|
|
|
|
today = datetime.date.today()
|
2024-01-25 21:54:22 +01:00
|
|
|
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
2024-01-25 17:17:01 +01:00
|
|
|
|
|
|
|
|
|
|
|
def remove_accents(input_unicode_str):
|
|
|
|
"""Supprime les accents d'une chaine unicode"""
|
|
|
|
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
|
|
|
|
only_ascii = nfkd_form.encode("ASCII", "ignore")
|
|
|
|
return only_ascii
|
|
|
|
|
|
|
|
|
|
|
|
def escape_for_latex(s):
|
|
|
|
"""Protège les caractères pour inclusion dans du source LaTeX"""
|
|
|
|
if not s:
|
|
|
|
return ""
|
|
|
|
conv = {
|
|
|
|
"&": r"\&",
|
|
|
|
"%": r"\%",
|
|
|
|
"$": r"\$",
|
|
|
|
"#": r"\#",
|
|
|
|
"_": r"\_",
|
|
|
|
"{": r"\{",
|
|
|
|
"}": r"\}",
|
|
|
|
"~": r"\textasciitilde{}",
|
|
|
|
"^": r"\^{}",
|
|
|
|
"\\": r"\textbackslash{}",
|
|
|
|
"<": r"\textless ",
|
|
|
|
">": r"\textgreater ",
|
|
|
|
}
|
|
|
|
exp = re.compile(
|
|
|
|
"|".join(
|
|
|
|
re.escape(key)
|
|
|
|
for key in sorted(list(conv.keys()), key=lambda item: -len(item))
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return exp.sub(lambda match: conv[match.group()], s)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------
|
|
|
|
def list_directory_filenames(path):
|
|
|
|
"""List of regular filenames in a directory (recursive)
|
|
|
|
Excludes files and directories begining with .
|
|
|
|
"""
|
|
|
|
R = []
|
|
|
|
for root, dirs, files in os.walk(path, topdown=True):
|
|
|
|
dirs[:] = [d for d in dirs if d[0] != "."]
|
|
|
|
R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
|
|
|
return R
|
|
|
|
|
|
|
|
|
|
|
|
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
|
|
|
|
"""Read pathname server file and add content to zip under path_in_zip"""
|
|
|
|
rooted_path_in_zip = os.path.join(ziproot, path_in_zip)
|
|
|
|
zipfile.write(filename=pathname, arcname=rooted_path_in_zip)
|
|
|
|
# data = open(pathname).read()
|
|
|
|
# zipfile.writestr(rooted_path_in_zip, data)
|
|
|
|
|
|
|
|
|
|
|
|
def add_refs_to_register(register, directory):
|
|
|
|
"""Ajoute les fichiers trouvés dans directory au registre (dictionaire) sous la forme
|
|
|
|
filename => pathname
|
|
|
|
"""
|
|
|
|
length = len(directory)
|
|
|
|
for pathname in list_directory_filenames(directory):
|
|
|
|
filename = pathname[length + 1 :]
|
|
|
|
register[filename] = pathname
|
|
|
|
|
|
|
|
|
|
|
|
def add_pe_stuff_to_zip(zipfile, ziproot):
|
|
|
|
"""Add auxiliary files to (already opened) zip
|
|
|
|
Put all local files found under config/doc_poursuites_etudes/local
|
|
|
|
and config/doc_poursuites_etudes/distrib
|
|
|
|
If a file is present in both subtrees, take the one in local.
|
|
|
|
|
|
|
|
Also copy logos
|
|
|
|
"""
|
|
|
|
register = {}
|
|
|
|
# first add standard (distrib references)
|
|
|
|
distrib_dir = os.path.join(REP_DEFAULT_AVIS, "distrib")
|
|
|
|
add_refs_to_register(register=register, directory=distrib_dir)
|
|
|
|
# then add local references (some oh them may overwrite distrib refs)
|
|
|
|
local_dir = os.path.join(REP_LOCAL_AVIS, "local")
|
|
|
|
add_refs_to_register(register=register, directory=local_dir)
|
|
|
|
# at this point register contains all refs (filename, pathname) to be saved
|
|
|
|
for filename, pathname in register.items():
|
|
|
|
add_local_file_to_zip(zipfile, ziproot, pathname, "avis/" + filename)
|
|
|
|
|
|
|
|
# Logos: (add to logos/ directory in zip)
|
|
|
|
logos_names = ["header", "footer"]
|
|
|
|
for name in logos_names:
|
|
|
|
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
|
|
|
if logo is not None:
|
|
|
|
add_local_file_to_zip(
|
|
|
|
zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ----------------------------------------------------------------------------------------
|
2024-01-25 21:54:22 +01:00
|
|
|
def get_annee_diplome_semestre(
|
|
|
|
sem_base: FormSemestre | dict, nbre_sem_formation: int = 6
|
|
|
|
) -> int:
|
2024-01-25 17:17:01 +01:00
|
|
|
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres)
|
|
|
|
et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle
|
|
|
|
sera remis le diplôme BUT des étudiants qui y sont scolarisés
|
|
|
|
(en supposant qu'il n'y ait pas de redoublement à venir).
|
|
|
|
|
|
|
|
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4, S6 pour des semestres décalés)
|
|
|
|
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'année universitaire.
|
|
|
|
|
|
|
|
Par exemple :
|
|
|
|
|
|
|
|
* S5 débutant en 2025 finissant en 2026 : diplome en 2026
|
|
|
|
* S3 debutant en 2025 et finissant en 2026 : diplome en 2027
|
|
|
|
|
|
|
|
La fonction est adaptée au cas des semestres décalés.
|
|
|
|
|
|
|
|
Par exemple :
|
|
|
|
|
|
|
|
* S5 décalé débutant en 2025 et finissant en 2025 : diplome en 2026
|
|
|
|
* S3 décalé débutant en 2025 et finissant en 2025 : diplome en 2027
|
|
|
|
|
|
|
|
Args:
|
|
|
|
sem_base: Le semestre à partir duquel est prédit l'année de diplomation, soit :
|
|
|
|
|
|
|
|
* un ``FormSemestre`` (Scodoc9)
|
|
|
|
* un dict (format compatible avec Scodoc7)
|
|
|
|
|
|
|
|
nbre_sem_formation: Le nombre de semestre prévu dans la formation (par défaut 6 pour un BUT)
|
|
|
|
"""
|
|
|
|
|
|
|
|
if isinstance(sem_base, FormSemestre):
|
|
|
|
sem_id = sem_base.semestre_id
|
|
|
|
annee_fin = sem_base.date_fin.year
|
|
|
|
annee_debut = sem_base.date_debut.year
|
|
|
|
else: # sem_base est un dictionnaire (Scodoc 7)
|
|
|
|
sem_id = sem_base["semestre_id"]
|
|
|
|
annee_fin = int(sem_base["annee_fin"])
|
|
|
|
annee_debut = int(sem_base["annee_debut"])
|
|
|
|
if (
|
|
|
|
1 <= sem_id <= nbre_sem_formation
|
|
|
|
): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
|
|
|
|
nbreSemRestant = (
|
|
|
|
nbre_sem_formation - sem_id
|
|
|
|
) # nombre de semestres restant avant diplome
|
|
|
|
nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome
|
|
|
|
# Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé
|
|
|
|
# avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
|
|
|
|
delta = annee_fin - annee_debut
|
|
|
|
decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
|
|
|
|
increment = decalage * (1 - delta)
|
|
|
|
return annee_fin + nbreAnRestant + increment
|
|
|
|
|
|
|
|
|
2024-01-25 21:54:22 +01:00
|
|
|
def get_cosemestres_diplomants(
|
|
|
|
annee_diplome: int, formation_id: int
|
|
|
|
) -> dict[int, FormSemestre]:
|
2024-01-25 17:17:01 +01:00
|
|
|
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``
|
|
|
|
et s'intégrant à la formation donnée par son ``formation_id``.
|
|
|
|
|
|
|
|
**Définition** : Un co-semestre est un semestre :
|
|
|
|
|
|
|
|
* dont l'année de diplômation prédite (sans redoublement) est la même
|
|
|
|
* dont la formation est la même (optionnel)
|
|
|
|
* qui a des étudiants inscrits
|
|
|
|
|
|
|
|
Si formation_id == None, ne prend pas en compte l'identifiant de formation
|
|
|
|
TODO:: A raccrocher à un programme
|
|
|
|
|
|
|
|
Args:
|
|
|
|
annee_diplome: L'année de diplomation
|
|
|
|
formation_id: L'identifiant de la formation
|
|
|
|
"""
|
|
|
|
tousLesSems = (
|
|
|
|
sco_formsemestre.do_formsemestre_list()
|
|
|
|
) # tous les semestres memorisés dans scodoc
|
|
|
|
|
|
|
|
if formation_id:
|
|
|
|
cosemestres_fids = {
|
|
|
|
sem["id"]
|
|
|
|
for sem in tousLesSems
|
|
|
|
if get_annee_diplome_semestre(sem) == annee_diplome
|
|
|
|
and sem["formation_id"] == formation_id
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
cosemestres_fids = {
|
|
|
|
sem["id"]
|
|
|
|
for sem in tousLesSems
|
|
|
|
if get_annee_diplome_semestre(sem) == annee_diplome
|
|
|
|
}
|
|
|
|
|
|
|
|
cosemestres = {}
|
|
|
|
for fid in cosemestres_fids:
|
|
|
|
cosem = FormSemestre.get_formsemestre(fid)
|
|
|
|
if len(cosem.etuds_inscriptions) > 0:
|
|
|
|
cosemestres[fid] = cosem
|
|
|
|
|
|
|
|
return cosemestres
|
2024-01-27 09:15:17 +01:00
|
|
|
|
|
|
|
|