Renommage dans UI et code des anciens 'Parcours' ScoDoc en 'Cursus'

This commit is contained in:
Emmanuel Viennet 2023-02-12 13:36:47 +01:00 committed by iziram
parent 8e7509b035
commit 07f478d6ea
20 changed files with 2671 additions and 50 deletions

View File

@ -650,13 +650,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
à poursuivre après le semestre courant. à poursuivre après le semestre courant.
""" """
# La poursuite d'études dans un semestre pair dune même année # La poursuite d'études dans un semestre pair dune même année
# est de droit pour tout étudiant. # est de droit pour tout étudiant:
# Pas de redoublements directs de S_impair vers S_impair if (self.formsemestre.semestre_id % 2) and sco_codes.CursusBUT.NB_SEM:
# (pourront être traités manuellement) ids.add(self.formsemestre.semestre_id + 1)
if (
self.formsemestre.semestre_id % 2
) and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM:
return {self.formsemestre.semestre_id + 1}
# La poursuite détudes dans un semestre impair est possible si # La poursuite détudes dans un semestre impair est possible si
# et seulement si létudiant a obtenu : # et seulement si létudiant a obtenu :
# - la moyenne à plus de la moitié des regroupements cohérents dUE ; # - la moyenne à plus de la moitié des regroupements cohérents dUE ;
@ -670,7 +667,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if ( if (
self.jury_annuel self.jury_annuel
and code in sco_codes.BUT_CODES_PASSAGE and code in sco_codes.BUT_CODES_PASSAGE
and self.formsemestre.semestre_id < sco_codes.CursusBUT.NB_SEM and self.formsemestre_pair.semestre_id < sco_codes.CursusBUT.NB_SEM
): ):
ids.add(self.formsemestre.semestre_id + 1) ids.add(self.formsemestre.semestre_id + 1)

View File

@ -43,7 +43,7 @@ from app.models import (
UniteEns, UniteEns,
) )
from app.comp import moy_mod from app.comp import moy_mod
from app.scodoc import sco_codes_parcours from app.scodoc import codes_cursus
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType

View File

@ -5,7 +5,8 @@ from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules from app.models.but_refcomp import ApcParcours, app_critiques_modules, parcours_modules
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType

View File

@ -65,8 +65,10 @@ def comp_nom_semestre_dans_parcours(sem):
"""Le nom a afficher pour titrer un semestre """Le nom a afficher pour titrer un semestre
par exemple: "semestre 2 FI 2015" par exemple: "semestre 2 FI 2015"
""" """
formation: Formation = Formation.query.get_or_404(sem["formation_id"]) from app.scodoc import sco_formations
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
return "%s %s %s %s" % ( return "%s %s %s %s" % (
parcours.SESSION_NAME, # eg "semestre" parcours.SESSION_NAME, # eg "semestre"
sem["semestre_id"], # eg 2 sem["semestre_id"], # eg 2

View File

@ -824,8 +824,7 @@ FORMATION_CURSUS_DESCRS = [p[1].__doc__ for p in _tp] # intitulés (eg pour men
FORMATION_CURSUS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_CURSUS) FORMATION_CURSUS_TYPES = [p[0] for p in _tp] # codes numeriques (TYPE_CURSUS)
def get_cursus_from_code(code_cursus: int) -> TypeCursus: def get_cursus_from_code(code_cursus):
"renvoie le cursus de code indiqué"
cursus = SCO_CURSUS.get(code_cursus) cursus = SCO_CURSUS.get(code_cursus)
if cursus is None: if cursus is None:
log(f"Warning: invalid code_cursus: {code_cursus}") log(f"Warning: invalid code_cursus: {code_cursus}")

1356
app/scodoc/notes_table.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,8 @@ from app.models import Formation
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_bac from app.scodoc import sco_bac
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_cache
from app.scodoc import sco_formations
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict from app.scodoc import sco_pv_dict
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -64,7 +66,9 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
semlist = [dpv["formsemestre"] for dpv in dpv_by_sem.values() if dpv] semlist = [dpv["formsemestre"] for dpv in dpv_by_sem.values() if dpv]
semlist_parcours = [] semlist_parcours = []
for sem in semlist: for sem in semlist:
sem["formation"] = Formation.query.get_or_404(sem["formation_id"]).to_dict() sem["formation"] = sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)[0]
sem["parcours"] = codes_cursus.get_cursus_from_code( sem["parcours"] = codes_cursus.get_cursus_from_code(
sem["formation"]["type_parcours"] sem["formation"]["type_parcours"]
) )

View File

@ -451,24 +451,20 @@ def formation_list_table() -> GenTable:
editable = current_user.has_permission(Permission.ScoChangeFormation) editable = current_user.has_permission(Permission.ScoChangeFormation)
# Traduit/ajoute des champs à afficher: # Traduit/ajoute des champs à afficher:
rows = [] for f in formations:
for formation in formations: try:
acronyme_no_spaces = formation.acronyme.lower().replace(" ", "-") f["parcours_name"] = codes_cursus.get_cursus_from_code(
row = { f["type_parcours"]
"acronyme": formation.acronyme, ).NAME
"parcours_name": codes_cursus.get_cursus_from_code( except:
formation.type_parcours f["parcours_name"] = ""
).NAME, f["_titre_target"] = url_for(
"titre": formation.titre, "notes.ue_table",
"_titre_target": url_for( scodoc_dept=g.scodoc_dept,
"notes.ue_table", formation_id=str(f["formation_id"]),
scodoc_dept=g.scodoc_dept, )
formation_id=formation.id, f["_titre_link_class"] = "stdlink"
), f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
"_titre_link_class": "stdlink",
"_titre_id": f"""titre-{acronyme_no_spaces}""",
"version": formation.version or 0,
}
# Ajoute les semestres associés à chaque formation: # Ajoute les semestres associés à chaque formation:
row["formsemestres"] = formation.formsemestres.order_by( row["formsemestres"] = formation.formsemestres.order_by(
FormSemestre.date_debut FormSemestre.date_debut

View File

@ -38,7 +38,7 @@ import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.models import Departement from app.models import Departement
from app.models import Formation, FormSemestre from app.models import FormSemestre
from app.scodoc import sco_cache, codes_cursus, sco_formations, sco_preferences from app.scodoc import sco_cache, codes_cursus, sco_formations, sco_preferences
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.codes_cursus import NO_SEMESTRE_ID from app.scodoc.codes_cursus import NO_SEMESTRE_ID
@ -145,8 +145,13 @@ def _formsemestre_enrich(sem):
# imports ici pour eviter refs circulaires # imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit from app.scodoc import sco_formsemestre_edit
formation: Formation = Formation.query.get_or_404(sem["formation_id"]) formations = sco_formations.formation_list(
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) args={"formation_id": sem["formation_id"]}
)
if not formations:
raise ScoValueError("pas de formation pour ce semestre !")
F = formations[0]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
# 'S1', 'S2', ... ou '' pour les monosemestres # 'S1', 'S2', ... ou '' pour les monosemestres
if sem["semestre_id"] != NO_SEMESTRE_ID: if sem["semestre_id"] != NO_SEMESTRE_ID:
sem["sem_id_txt"] = "S%s" % sem["semestre_id"] sem["sem_id_txt"] = "S%s" % sem["semestre_id"]

View File

@ -55,6 +55,7 @@ from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_etud
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT

View File

@ -540,6 +540,58 @@ def formsemestre_page_title(formsemestre_id=None):
) )
def fill_formsemestre(sem):
"""Add some useful fields to help display formsemestres"""
sem["notes_url"] = scu.NotesURL()
formsemestre_id = sem["formsemestre_id"]
if not sem["etat"]:
sem[
"locklink"
] = f"""<a href="{url_for('notes.formsemestre_change_lock',
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id )
}">{scu.icontag("lock_img", border="0", title="Semestre verrouillé")}</a>"""
else:
sem["locklink"] = ""
if sco_preferences.get_preference("bul_display_publication", formsemestre_id):
if sem["bul_hide_xml"]:
eyeicon = scu.icontag("hide_img", border="0", title="Bulletins NON publiés")
else:
eyeicon = scu.icontag("eye_img", border="0", title="Bulletins publiés")
sem[
"eyelink"
] = f"""<a href="{
url_for('notes.formsemestre_change_publication_bul',
scodoc_dept=g.scodoc_dept,formsemestre_id=formsemestre_id)
}">{eyeicon}</a>"""
else:
sem["eyelink"] = ""
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
sem["formation"] = F
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
if sem["semestre_id"] != -1:
sem["num_sem"] = f""", {parcours.SESSION_NAME} {sem["semestre_id"]}"""
else:
sem["num_sem"] = "" # formation sans semestres
if sem["modalite"]:
sem["modalitestr"] = f""" en {sem["modalite"]}"""
else:
sem["modalitestr"] = ""
sem["etape_apo_str"] = "Code étape Apogée: " + (
sco_formsemestre.formsemestre_etape_apo_str(sem) or "Pas de code étape"
)
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
sem["nbinscrits"] = len(inscrits)
uresps = [
sco_users.user_info(responsable_id) for responsable_id in sem["responsables"]
]
sem["resp"] = ", ".join([u["prenomnom"] for u in uresps])
sem["nomcomplet"] = ", ".join([u["nomcomplet"] for u in uresps])
# Description du semestre sous forme de table exportable # Description du semestre sous forme de table exportable
def formsemestre_description_table( def formsemestre_description_table(
formsemestre_id: int, with_evals=False, with_parcours=False formsemestre_id: int, with_evals=False, with_parcours=False
@ -552,7 +604,10 @@ def formsemestre_description_table(
).first_or_404() ).first_or_404()
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id) use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
parcours = codes_cursus.get_cursus_from_code(formsemestre.formation.type_parcours) F = sco_formations.formation_list(args={"formation_id": formsemestre.formation_id})[
0
]
parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
# --- Colonnes à proposer: # --- Colonnes à proposer:
columns_ids = ["UE", "Code", "Module"] columns_ids = ["UE", "Code", "Module"]
if with_parcours: if with_parcours:

View File

@ -795,7 +795,8 @@ def groups_table(
) )
m["parcours"] = Se.get_cursus_descr() m["parcours"] = Se.get_cursus_descr()
m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud) m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud)
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
L = [[m.get(k, "") for k in keys] for m in groups_infos.members]
title = "etudiants_%s" % groups_infos.groups_filename title = "etudiants_%s" % groups_infos.groups_filename
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title) xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
filename = title filename = title

View File

@ -261,8 +261,8 @@ def list_source_sems(sem, delai=None) -> list[dict]:
if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID: if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID:
continue continue
# #
formation: Formation = Formation.query.get_or_404(s["formation_id"]) F = sco_formations.formation_list(args={"formation_id": s["formation_id"]})[0]
parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) parcours = codes_cursus.get_cursus_from_code(F["type_parcours"])
if not parcours.ALLOW_SEM_SKIP: if not parcours.ALLOW_SEM_SKIP:
if s["semestre_id"] < (sem["semestre_id"] - 1): if s["semestre_id"] < (sem["semestre_id"] - 1):
continue continue

View File

@ -44,7 +44,9 @@ import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_pv_dict from app.scodoc import sco_cursus
from app.scodoc import sco_cursus_dut
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
@ -58,6 +60,57 @@ from app.scodoc.sco_pdf import PDFLOCK
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
def _descr_decisions_ues(nt, etudid, decisions_ue, decision_sem) -> list[dict]:
"""Liste des UE validées dans ce semestre (incluant les UE capitalisées)"""
if not decisions_ue:
return []
uelist = []
# Les UE validées dans ce semestre:
for ue_id in decisions_ue.keys():
try:
if decisions_ue[ue_id] and (
codes_cursus.code_ue_validant(decisions_ue[ue_id]["code"])
or (
(not nt.is_apc)
and (
# XXX ceci devrait dépendre du parcours et non pas être une option ! #sco8
decision_sem
and scu.CONFIG.CAPITALIZE_ALL_UES
and codes_cursus.code_semestre_validant(decision_sem["code"])
)
)
):
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0]
uelist.append(ue)
except:
log(
f"Exception in descr_decisions_ues: ue_id={ue_id} decisions_ue={decisions_ue}"
)
# Les UE capitalisées dans d'autres semestres:
if etudid in nt.validations.ue_capitalisees.index:
for ue_id in nt.validations.ue_capitalisees.loc[[etudid]]["ue_id"]:
try:
uelist.append(nt.get_etud_ue_status(etudid, ue_id)["ue"])
except (KeyError, TypeError):
pass
uelist.sort(key=itemgetter("numero"))
return uelist
def _descr_decision_sem(etat, decision_sem):
"résumé textuel de la décision de semestre"
if etat == "D":
decision = "Démission"
else:
if decision_sem:
cod = decision_sem["code"]
decision = codes_cursus.CODES_EXPL.get(cod, "") # + ' (%s)' % cod
else:
decision = ""
return decision
def _descr_decision_sem_abbrev(etat, decision_sem): def _descr_decision_sem_abbrev(etat, decision_sem):
"résumé textuel tres court (code) de la décision de semestre" "résumé textuel tres court (code) de la décision de semestre"
if etat == "D": if etat == "D":
@ -70,6 +123,232 @@ def _descr_decision_sem_abbrev(etat, decision_sem):
return decision return decision
def descr_autorisations(autorisations: list[ScolarAutorisationInscription]) -> str:
"résumé textuel des autorisations d'inscription (-> 'S1, S3' )"
return ", ".join([f"S{a.semestre_id}" for a in autorisations])
def _comp_ects_by_ue_code(nt, decision_ues):
"""Calcul somme des ECTS validés dans ce semestre (sans les UE capitalisées)
decision_ues est le resultat de nt.get_etud_decision_ues
Chaque resultat est un dict: { ue_code : ects }
"""
if not decision_ues:
return {}
ects_by_ue_code = {}
for ue_id in decision_ues:
d = decision_ues[ue_id]
ue = UniteEns.query.get(ue_id)
ects_by_ue_code[ue.ue_code] = d["ects"]
return ects_by_ue_code
def _comp_ects_capitalises_by_ue_code(nt: NotesTableCompat, etudid: int):
"""Calcul somme des ECTS des UE capitalisees"""
ues = nt.get_ues_stat_dict()
ects_by_ue_code = {}
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status and ue_status["is_capitalized"]:
ects_val = float(ue_status["ue"]["ects"] or 0.0)
ects_by_ue_code[ue["ue_code"]] = ects_val
return ects_by_ue_code
def _sum_ects_dicts(s, t):
"""Somme deux dictionnaires { ue_code : ects },
quand une UE de même code apparait deux fois, prend celle avec le plus d'ECTS.
"""
sum_ects = sum(s.values()) + sum(t.values())
for ue_code in set(s).intersection(set(t)):
sum_ects -= min(s[ue_code], t[ue_code])
return sum_ects
def dict_pvjury(
formsemestre_id,
etudids=None,
with_prev=False,
with_parcours_decisions=False,
):
"""Données pour édition jury
etudids == None => tous les inscrits, sinon donne la liste des ids
Si with_prev: ajoute infos sur code jury semestre precedent
Si with_parcours_decisions: ajoute infos sur code decision jury de tous les semestre du parcours
Résultat:
{
'date' : date de la decision la plus recente,
'formsemestre' : sem,
'is_apc' : bool,
'formation' : { 'acronyme' :, 'titre': ... }
'decisions' : { [ { 'identite' : {'nom' :, 'prenom':, ...,},
'etat' : I ou D ou DEF
'decision_sem' : {'code':, 'code_prev': },
'decisions_ue' : { ue_id : { 'code' : ADM|CMP|AJ, 'event_date' :,
'acronyme', 'numero': } },
'autorisations' : [ { 'semestre_id' : { ... } } ],
'validation_parcours' : True si parcours validé (diplome obtenu)
'prev_code' : code (calculé slt si with_prev),
'mention' : mention (en fct moy gen),
'sum_ects' : total ECTS acquis dans ce semestre (incluant les UE capitalisées)
'sum_ects_capitalises' : somme des ECTS des UE capitalisees
}
]
},
'decisions_dict' : { etudid : decision (comme ci-dessus) },
}
"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if etudids is None:
etudids = nt.get_etudids()
if not etudids:
return {}
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
max_date = "0000-01-01"
has_prev = False # vrai si au moins un etudiant a un code prev
semestre_non_terminal = False # True si au moins un etudiant a un devenir
decisions = []
D = {} # même chose que decisions, mais { etudid : dec }
for etudid in etudids:
# etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud: Identite = Identite.query.get(etudid)
Se = sco_cursus.get_situation_etud_cursus(
etud.to_dict_scodoc7(), formsemestre_id
)
semestre_non_terminal = semestre_non_terminal or Se.semestre_non_terminal
d = {}
d["identite"] = nt.identdict[etudid]
d["etat"] = nt.get_etud_etat(
etudid
) # I|D|DEF (inscription ou démission ou défaillant)
d["decision_sem"] = nt.get_etud_decision_sem(etudid)
d["decisions_ue"] = nt.get_etud_decision_ues(etudid)
if formsemestre.formation.is_apc():
d.update(but_validations.dict_decision_jury(etud, formsemestre))
d["last_formsemestre_id"] = Se.get_semestres()[
-1
] # id du dernier semestre (chronologiquement) dans lequel il a été inscrit
ects_capitalises_by_ue_code = _comp_ects_capitalises_by_ue_code(nt, etudid)
d["sum_ects_capitalises"] = sum(ects_capitalises_by_ue_code.values())
ects_by_ue_code = _comp_ects_by_ue_code(nt, d["decisions_ue"])
d["sum_ects"] = _sum_ects_dicts(ects_capitalises_by_ue_code, ects_by_ue_code)
if d["decision_sem"] and codes_cursus.code_semestre_validant(
d["decision_sem"]["code"]
):
d["mention"] = scu.get_mention(nt.get_etud_moy_gen(etudid))
else:
d["mention"] = ""
# Versions "en français": (avec les UE capitalisées d'ailleurs)
dec_ue_list = _descr_decisions_ues(
nt, etudid, d["decisions_ue"], d["decision_sem"]
)
d["decisions_ue_nb"] = len(
dec_ue_list
) # avec les UE capitalisées, donc des éventuels doublons
# Mais sur la description (eg sur les bulletins), on ne veut pas
# afficher ces doublons: on uniquifie sur ue_code
_codes = set()
ue_uniq = []
for ue in dec_ue_list:
if ue["ue_code"] not in _codes:
ue_uniq.append(ue)
_codes.add(ue["ue_code"])
d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq])
if nt.is_apc:
d["decision_sem_descr"] = "" # pas de validation de semestre en BUT
else:
d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"])
autorisations = ScolarAutorisationInscription.query.filter_by(
etudid=etudid, origin_formsemestre_id=formsemestre_id
).all()
d["autorisations"] = [a.to_dict() for a in autorisations]
d["autorisations_descr"] = descr_autorisations(autorisations)
d["validation_parcours"] = Se.parcours_validated()
d["parcours"] = Se.get_cursus_descr(filter_futur=True)
if with_parcours_decisions:
d["parcours_decisions"] = Se.get_parcours_decisions()
# Observations sur les compensations:
compensators = sco_cursus_dut.scolar_formsemestre_validation_list(
cnx, args={"compense_formsemestre_id": formsemestre_id, "etudid": etudid}
)
obs = []
for compensator in compensators:
# nb: il ne devrait y en avoir qu'un !
csem = sco_formsemestre.get_formsemestre(compensator["formsemestre_id"])
obs.append(
"%s compensé par %s (%s)"
% (sem["sem_id_txt"], csem["sem_id_txt"], csem["anneescolaire"])
)
if d["decision_sem"] and d["decision_sem"]["compense_formsemestre_id"]:
compensed = sco_formsemestre.get_formsemestre(
d["decision_sem"]["compense_formsemestre_id"]
)
obs.append(
f"""{sem["sem_id_txt"]} compense {compensed["sem_id_txt"]} ({compensed["anneescolaire"]})"""
)
d["observation"] = ", ".join(obs)
# Cherche la date de decision (sem ou UE) la plus récente:
if d["decision_sem"]:
date = ndb.DateDMYtoISO(d["decision_sem"]["event_date"])
if date and date > max_date: # decision plus recente
max_date = date
if d["decisions_ue"]:
for dec_ue in d["decisions_ue"].values():
if dec_ue:
date = ndb.DateDMYtoISO(dec_ue["event_date"])
if date and date > max_date: # decision plus recente
max_date = date
# Code semestre precedent
if with_prev: # optionnel car un peu long...
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not info:
continue # should not occur
etud = info[0]
if Se.prev and Se.prev_decision:
d["prev_decision_sem"] = Se.prev_decision
d["prev_code"] = Se.prev_decision["code"]
d["prev_code_descr"] = _descr_decision_sem(
scu.INSCRIT, Se.prev_decision
)
d["prev"] = Se.prev
has_prev = True
else:
d["prev_decision_sem"] = None
d["prev_code"] = ""
d["prev_code_descr"] = ""
d["Se"] = Se
decisions.append(d)
D[etudid] = d
return {
"date": ndb.DateISOtoDMY(max_date),
"formsemestre": sem,
"is_apc": nt.is_apc,
"has_prev": has_prev,
"semestre_non_terminal": semestre_non_terminal,
"formation": sco_formations.formation_list(
args={"formation_id": sem["formation_id"]}
)[0],
"decisions": decisions,
"decisions_dict": D,
}
def pvjury_table( def pvjury_table(
dpv, dpv,
only_diplome=False, only_diplome=False,

925
app/scodoc/sco_pvpdf.py Normal file
View File

@ -0,0 +1,925 @@
# -*- 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
#
##############################################################################
"""Edition des PV de jury
"""
import io
import re
from PIL import Image as PILImage
from PIL import UnidentifiedImageError
import reportlab
from reportlab.lib.units import cm, mm
from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_JUSTIFY
from reportlab.platypus import Paragraph, Spacer, Frame, PageBreak
from reportlab.platypus import Table, TableStyle, Image
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.lib.pagesizes import A4, landscape
from reportlab.lib import styles
from reportlab.lib.colors import Color
from flask import g
from app.models import FormSemestre, Identite
import app.scodoc.sco_utils as scu
from app.scodoc import sco_bulletins_pdf
from app.scodoc import codes_cursus
from app.scodoc import sco_etud
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_cursus_dut import SituationEtudCursus
from app.scodoc.sco_pdf import SU
import sco_version
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
LOGO_FOOTER_WIDTH = LOGO_FOOTER_HEIGHT * scu.CONFIG.LOGO_FOOTER_ASPECT
LOGO_HEADER_ASPECT = scu.CONFIG.LOGO_HEADER_ASPECT # XXX logo IUTV (A AUTOMATISER)
LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm
LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
"Add footer on page"
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
foot = Frame(
0.1 * mm,
0.2 * cm,
width - 1 * mm,
2 * cm,
leftPadding=0,
rightPadding=0,
topPadding=0,
bottomPadding=0,
id="monfooter",
showBoundary=0,
)
left_foot_style = reportlab.lib.styles.ParagraphStyle({})
left_foot_style.fontName = preferences["SCOLAR_FONT"]
left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
left_foot_style.leftIndent = 0
left_foot_style.firstLineIndent = 0
left_foot_style.alignment = TA_RIGHT
right_foot_style = reportlab.lib.styles.ParagraphStyle({})
right_foot_style.fontName = preferences["SCOLAR_FONT"]
right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
right_foot_style.alignment = TA_RIGHT
p = sco_pdf.make_paras(
f"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
preferences["INSTITUTION_ADDRESS"]}</para>""",
left_foot_style,
)
np = Paragraph(f'<para fontSize="14">{doc.page}</para>', right_foot_style)
tabstyle = TableStyle(
[
("LEFTPADDING", (0, 0), (-1, -1), 0),
("RIGHTPADDING", (0, 0), (-1, -1), 0),
("ALIGN", (0, 0), (-1, -1), "RIGHT"),
# ('INNERGRID', (0,0), (-1,-1), 0.25, black),#debug
# ('LINEABOVE', (0,0), (-1,0), 0.5, black),
("VALIGN", (1, 0), (1, 0), "MIDDLE"),
("RIGHTPADDING", (-1, 0), (-1, 0), 1 * cm),
]
)
elems = [p]
if logo:
elems.append(logo)
colWidths = [None, LOGO_FOOTER_WIDTH + 2 * mm]
if with_page_numbers:
elems.append(np)
colWidths.append(2 * cm)
else:
elems.append("")
colWidths.append(8 * mm) # force marge droite
tab = Table([elems], style=tabstyle, colWidths=colWidths)
canvas.saveState() # is it necessary ?
foot.addFromList([tab], canvas)
canvas.restoreState()
def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
"Ajoute au canvas le frame avec le logo"
if only_on_first_page and int(doc.page) > 1:
return
height = doc.pagesize[1]
head = Frame(
-22 * mm,
height - 13 * mm - LOGO_HEADER_HEIGHT,
10 * cm,
LOGO_HEADER_HEIGHT + 2 * mm,
leftPadding=0,
rightPadding=0,
topPadding=0,
bottomPadding=0,
id="monheader",
showBoundary=0,
)
if logo:
canvas.saveState() # is it necessary ?
head.addFromList([logo], canvas)
canvas.restoreState()
class CourrierIndividuelTemplate(PageTemplate):
"""Template pour courrier avisant des decisions de jury (1 page par étudiant)"""
def __init__(
self,
document,
pagesbookmarks=None,
author=None,
title=None,
subject=None,
margins=(0, 0, 0, 0), # additional margins in mm (left,top,right, bottom)
preferences=None, # dictionnary with preferences, required
force_header=False,
force_footer=False, # always add a footer (whatever the preferences, use for PV)
template_name="CourrierJuryTemplate",
):
"""Initialise our page template."""
self.pagesbookmarks = pagesbookmarks or {}
self.pdfmeta_author = author
self.pdfmeta_title = title
self.pdfmeta_subject = subject
self.preferences = preferences
self.force_header = force_header
self.force_footer = force_footer
self.with_footer = (
self.force_footer or self.preferences["PV_LETTER_WITH_HEADER"]
)
self.with_header = (
self.force_header or self.preferences["PV_LETTER_WITH_FOOTER"]
)
self.with_page_background = self.preferences["PV_LETTER_WITH_BACKGROUND"]
self.with_page_numbers = False
self.header_only_on_first_page = False
# Our doc is made of a single frame
left, top, right, bottom = margins # marge additionnelle en mm
# marges du Frame principal
self.bot_p = 2 * cm
self.left_p = 2.5 * cm
self.right_p = 2.5 * cm
self.top_p = 0 * cm
# log("margins=%s" % str(margins))
content = Frame(
self.left_p + left * mm,
self.bot_p + bottom * mm,
document.pagesize[0] - self.right_p - self.left_p - left * mm - right * mm,
document.pagesize[1] - self.top_p - self.bot_p - top * mm - bottom * mm,
)
PageTemplate.__init__(self, template_name, [content])
self.background_image_filename = None
self.logo_footer = None
self.logo_header = None
# Search logos in dept specific dir, then in global scu.CONFIG dir
if template_name == "PVJuryTemplate":
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:
self.background_image_filename = background.filepath
footer = find_logo(logoname="footer", dept_id=g.scodoc_dept_id)
if footer is not None:
self.logo_footer = Image(
footer.filepath,
height=LOGO_FOOTER_HEIGHT,
width=LOGO_FOOTER_WIDTH,
)
header = find_logo(logoname="header", dept_id=g.scodoc_dept_id)
if header is not None:
self.logo_header = Image(
header.filepath,
height=LOGO_HEADER_HEIGHT,
width=LOGO_HEADER_WIDTH,
)
def beforeDrawPage(self, canv, doc):
"""Draws a logo and an contribution message on each page."""
# ---- Add some meta data and bookmarks
if self.pdfmeta_author:
canv.setAuthor(SU(self.pdfmeta_author))
if self.pdfmeta_title:
canv.setTitle(SU(self.pdfmeta_title))
if self.pdfmeta_subject:
canv.setSubject(SU(self.pdfmeta_subject))
bm = self.pagesbookmarks.get(doc.page, None)
if bm != None:
key = bm
txt = SU(bm)
canv.bookmarkPage(key)
canv.addOutlineEntry(txt, bm)
# ---- Background image
if self.background_image_filename and self.with_page_background:
canv.drawImage(
self.background_image_filename, 0, 0, doc.pagesize[0], doc.pagesize[1]
)
# ---- Header/Footer
if self.with_header:
page_header(
canv,
doc,
self.logo_header,
self.preferences,
self.header_only_on_first_page,
)
if self.with_footer:
page_footer(
canv,
doc,
self.logo_footer,
self.preferences,
with_page_numbers=self.with_page_numbers,
)
class PVTemplate(CourrierIndividuelTemplate):
"""Template pour les pages des PV de jury"""
def __init__(
self,
document,
author=None,
title=None,
subject=None,
margins=None, # additional margins in mm (left,top,right, bottom)
preferences=None, # dictionnary with preferences, required
):
if margins is None:
margins = (
preferences["pv_left_margin"],
preferences["pv_top_margin"],
preferences["pv_right_margin"],
preferences["pv_bottom_margin"],
)
CourrierIndividuelTemplate.__init__(
self,
document,
author=author,
title=title,
subject=subject,
margins=margins,
preferences=preferences,
force_header=True,
force_footer=True,
template_name="PVJuryTemplate",
)
self.with_page_numbers = True
self.header_only_on_first_page = True
self.with_header = self.preferences["PV_WITH_HEADER"]
self.with_footer = self.preferences["PV_WITH_FOOTER"]
self.with_page_background = self.preferences["PV_WITH_BACKGROUND"]
def afterDrawPage(self, canv, doc):
"""Called after all flowables have been drawn on a page"""
pass
def beforeDrawPage(self, canv, doc):
"""Called before any flowables are drawn on a page"""
# If the page number is even, force a page break
CourrierIndividuelTemplate.beforeDrawPage(self, canv, doc)
# Note: on cherche un moyen de generer un saut de page double
# (redémarrer sur page impaire, nouvelle feuille en recto/verso). Pas trouvé en Platypus.
#
# if self.__pageNum % 2 == 0:
# canvas.showPage()
# # Increment pageNum again since we've added a blank page
# self.__pageNum += 1
def _simulate_br(paragraph_txt: str, para="<para>") -> str:
"""Reportlab bug turnaround (could be removed in a future version).
p is a string with Reportlab intra-paragraph XML tags.
Replaces <br> (currently ignored by Reportlab) by </para><para>
Also replaces <br> by <br/>
"""
return ("</para>" + para).join(
re.split(r"<.*?br.*?/>", paragraph_txt.replace("<br>", "<br/>"))
)
def _make_signature_image(signature, leftindent, formsemestre_id) -> Table:
"crée un paragraphe avec l'image signature"
# cree une image PIL pour avoir la taille (W,H)
f = io.BytesIO(signature)
img = PILImage.open(f)
width, height = img.size
pdfheight = (
1.0
* sco_preferences.get_preference("pv_sig_image_height", formsemestre_id)
* mm
)
f.seek(0, 0)
style = styles.ParagraphStyle({})
style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
) # vertical space
style.leftIndent = leftindent
return Table(
[("", Image(f, width=width * pdfheight / float(height), height=pdfheight))],
colWidths=(9 * cm, 7 * cm),
)
def pdf_lettres_individuelles(
formsemestre_id,
etudids=None,
date_jury="",
date_commission="",
signature=None,
):
"""Document PDF avec les lettres d'avis pour les etudiants mentionnés
(tous ceux du semestre, ou la liste indiquée par etudids)
Renvoie pdf data ou chaine vide si aucun etudiant avec décision de jury.
"""
from app.scodoc import sco_pvjury
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=etudids, with_prev=True)
if not dpv:
return ""
# Ajoute infos sur etudiants
etuds = [x["identite"] for x in dpv["decisions"]]
sco_etud.fill_etuds_info(etuds)
#
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
prefs = sco_preferences.SemPreferences(formsemestre_id)
params = {
"date_jury": date_jury,
"date_commission": date_commission,
"titre_formation": dpv["formation"]["titre_officiel"],
"htab1": "8cm", # lignes à droite (entete, signature)
"htab2": "1cm",
}
# copie preferences
for name in sco_preferences.get_base_preferences().prefs_name:
params[name] = sco_preferences.get_preference(name, formsemestre_id)
bookmarks = {}
objects = [] # list of PLATYPUS objects
npages = 0
for decision in dpv["decisions"]:
if (
decision["decision_sem"]
or decision.get("decision_annee")
or decision.get("decision_rcue")
): # decision prise
etud: Identite = Identite.query.get(decision["identite"]["etudid"])
params["nomEtud"] = etud.nomprenom
bookmarks[npages + 1] = scu.suppress_accents(etud.nomprenom)
try:
objects += pdf_lettre_individuelle(
dpv["formsemestre"], decision, etud, params, signature
)
except UnidentifiedImageError as exc:
raise ScoValueError(
"Fichier image (signature ou logo ?) invalide !"
) from exc
objects.append(PageBreak())
npages += 1
if npages == 0:
return ""
# Paramètres de mise en page
margins = (
prefs["left_margin"],
prefs["top_margin"],
prefs["right_margin"],
prefs["bottom_margin"],
)
# ----- Build PDF
report = io.BytesIO() # in-memory document, no disk file
document = BaseDocTemplate(report)
document.addPageTemplates(
CourrierIndividuelTemplate(
document,
author=f"{sco_version.SCONAME} {sco_version.SCOVERSION} (E. Viennet)",
title=f"Lettres décision {formsemestre.titre_annee()}",
subject="Décision jury",
margins=margins,
pagesbookmarks=bookmarks,
preferences=prefs,
)
)
document.build(objects)
data = report.getvalue()
return data
def _descr_jury(formsemestre: FormSemestre, diplome):
if not diplome:
if formsemestre.formation.is_apc():
t = f"""BUT{(formsemestre.semestre_id+1)//2}"""
s = t
else:
t = f"""passage de Semestre {formsemestre.semestre_id} en Semestre {formsemestre.semestre_id + 1}"""
s = "passage de semestre"
else:
t = "délivrance du diplôme"
s = t
return t, s # titre long, titre court
def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=None):
"""
Renvoie une liste d'objets PLATYPUS pour intégration
dans un autre document.
"""
#
formsemestre_id = sem["formsemestre_id"]
formsemestre = FormSemestre.query.get(formsemestre_id)
Se: SituationEtudCursus = decision["Se"]
t, s = _descr_jury(
formsemestre, Se.parcours_validated() or not Se.semestre_non_terminal
)
objects = []
style = reportlab.lib.styles.ParagraphStyle({})
style.fontSize = 14
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
style.leading = 18
style.alignment = TA_LEFT
params["semestre_id"] = formsemestre.semestre_id
params["decision_sem_descr"] = decision["decision_sem_descr"]
params["type_jury"] = t # type de jury (passage ou delivrance)
params["type_jury_abbrv"] = s # idem, abbrégé
params["decisions_ue_descr"] = decision["decisions_ue_descr"]
if decision["decisions_ue_nb"] > 1:
params["decisions_ue_descr_plural"] = "s"
else:
params["decisions_ue_descr_plural"] = ""
params["INSTITUTION_CITY"] = (
sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or ""
)
if decision["prev_decision_sem"]:
params["prev_semestre_id"] = decision["prev"]["semestre_id"]
params["prev_decision_sem_txt"] = ""
params["decision_orig"] = ""
params.update(decision["identite"])
# fix domicile
if params["domicile"]:
params["domicile"] = params["domicile"].replace("\\n", "<br/>")
# UE capitalisées:
if decision["decisions_ue"] and decision["decisions_ue_descr"]:
params["decision_ue_txt"] = (
"""<b>Unité%(decisions_ue_descr_plural)s d'Enseignement %(decision_orig)s capitalisée%(decisions_ue_descr_plural)s : %(decisions_ue_descr)s</b>"""
% params
)
else:
params["decision_ue_txt"] = ""
# Mention
params["mention"] = decision["mention"]
# Informations sur compensations
if decision["observation"]:
params["observation_txt"] = (
"""<b>Observation :</b> %(observation)s.""" % decision
)
else:
params["observation_txt"] = ""
# Autorisations de passage
if decision["autorisations"] and not Se.parcours_validated():
if len(decision["autorisations"]) > 1:
s = "s"
else:
s = ""
params[
"autorisations_txt"
] = """Vous êtes autorisé%s à continuer dans le%s semestre%s : <b>%s</b>""" % (
etud.e,
s,
s,
decision["autorisations_descr"],
)
else:
params["autorisations_txt"] = ""
if decision["decision_sem"] and Se.parcours_validated():
params["diplome_txt"] = (
"""Vous avez donc obtenu le diplôme : <b>%(titre_formation)s</b>""" % params
)
else:
params["diplome_txt"] = ""
# Les fonctions ci-dessous ajoutent ou modifient des champs:
if formsemestre.formation.is_apc():
# ajout champs spécifiques PV BUT
add_apc_infos(formsemestre, params, decision)
else:
# ajout champs spécifiques PV DUT
add_classic_infos(formsemestre, params, decision)
# Corps de la lettre:
objects += sco_bulletins_pdf.process_field(
sco_preferences.get_preference("PV_LETTER_TEMPLATE", sem["formsemestre_id"]),
params,
style,
suppress_empty_pars=True,
)
# Signature:
# nota: si semestre terminal, signature par directeur IUT, sinon, signature par
# chef de département.
if Se.semestre_non_terminal:
sig = (
sco_preferences.get_preference(
"PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
else:
sig = (
sco_preferences.get_preference(
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
)
or ""
) % params
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
objects += sco_pdf.make_paras(
(
"""<para leftindent="%(htab1)s" spaceBefore="25mm">"""
+ sig
+ """</para>"""
)
% params,
style,
)
if signature:
try:
objects.append(
_make_signature_image(signature, params["htab1"], formsemestre_id)
)
except UnidentifiedImageError as exc:
raise ScoValueError("Image signature invalide !") from exc
return objects
def add_classic_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations classiques, donc avec codes semestres"""
if decision["prev_decision_sem"]:
params["prev_code_descr"] = decision["prev_code_descr"]
params[
"prev_decision_sem_txt"
] = f"""<b>Décision du semestre antérieur S{params['prev_semestre_id']} :</b> {params['prev_code_descr']}"""
# Décision semestre courant:
if formsemestre.semestre_id >= 0:
params["decision_orig"] = f"du semestre S{formsemestre.semestre_id}"
else:
params["decision_orig"] = ""
def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict):
"""Ajoute les champs pour les formations APC (BUT), donc avec codes RCUE et année"""
annee_but = (formsemestre.semestre_id + 1) // 2
params["decision_orig"] = f"année BUT{annee_but}"
if decision is None:
params["decision_sem_descr"] = ""
params["decision_ue_txt"] = ""
else:
decision_annee = decision.get("decision_annee") or {}
params["decision_sem_descr"] = decision_annee.get("code") or ""
params[
"decision_ue_txt"
] = f"""{params["decision_ue_txt"]}<br/>
<b>Niveaux de compétences:</b><br/> {decision.get("descr_decisions_rcue") or ""}
"""
# ----------------------------------------------
def pvjury_pdf(
dpv,
date_commission=None,
date_jury=None,
numeroArrete=None,
VDICode=None,
showTitle=False,
pv_title=None,
with_paragraph_nom=False,
anonymous=False,
):
"""Doc PDF récapitulant les décisions de jury
(tableau en format paysage)
dpv: result of dict_pvjury
"""
if not dpv:
return {}
sem = dpv["formsemestre"]
formsemestre_id = sem["formsemestre_id"]
objects = _pvjury_pdf_type(
dpv,
only_diplome=False,
date_commission=date_commission,
numeroArrete=numeroArrete,
VDICode=VDICode,
date_jury=date_jury,
showTitle=showTitle,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)
jury_de_diplome = not dpv["semestre_non_terminal"]
# Si Jury de passage et qu'un étudiant valide le parcours (car il a validé antérieurement le dernier semestre)
# alors on génère aussi un PV de diplome (à la suite dans le même doc PDF)
if not jury_de_diplome:
validations_parcours = [x["validation_parcours"] for x in dpv["decisions"]]
if True in validations_parcours:
# au moins un etudiant a validé son diplome:
objects.append(PageBreak())
objects += _pvjury_pdf_type(
dpv,
only_diplome=True,
date_commission=date_commission,
date_jury=date_jury,
numeroArrete=numeroArrete,
VDICode=VDICode,
showTitle=showTitle,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)
# ----- Build PDF
report = io.BytesIO() # in-memory document, no disk file
document = BaseDocTemplate(report)
document.pagesize = landscape(A4)
document.addPageTemplates(
PVTemplate(
document,
author="%s %s (E. Viennet)" % (sco_version.SCONAME, sco_version.SCOVERSION),
title=SU("PV du jury de %s" % sem["titre_num"]),
subject="PV jury",
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
)
document.build(objects)
data = report.getvalue()
return data
def _pvjury_pdf_type(
dpv,
only_diplome=False,
date_commission=None,
date_jury=None,
numeroArrete=None,
VDICode=None,
showTitle=False,
pv_title=None,
anonymous=False,
with_paragraph_nom=False,
):
"""Doc PDF récapitulant les décisions de jury pour un type de jury (passage ou delivrance)
dpv: result of dict_pvjury
"""
from app.scodoc import sco_pvjury
# Jury de diplome si sem. terminal OU que l'on demande les diplomés d'un semestre antérieur
diplome = (not dpv["semestre_non_terminal"]) or only_diplome
sem = dpv["formsemestre"]
formsemestre_id = sem["formsemestre_id"]
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
titre_jury, _ = _descr_jury(formsemestre, diplome)
titre_diplome = pv_title or dpv["formation"]["titre_officiel"]
objects = []
style = reportlab.lib.styles.ParagraphStyle({})
style.fontSize = 12
style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
style.leading = 18
style.alignment = TA_JUSTIFY
indent = 1 * cm
bulletStyle = reportlab.lib.styles.ParagraphStyle({})
bulletStyle.fontSize = 12
bulletStyle.fontName = sco_preferences.get_preference(
"PV_FONTNAME", formsemestre_id
)
bulletStyle.leading = 12
bulletStyle.alignment = TA_JUSTIFY
bulletStyle.firstLineIndent = 0
bulletStyle.leftIndent = indent
bulletStyle.bulletIndent = indent
bulletStyle.bulletFontName = "Times-Roman"
bulletStyle.bulletFontSize = 11
bulletStyle.spaceBefore = 5 * mm
bulletStyle.spaceAfter = 5 * mm
objects += [Spacer(0, 5 * mm)]
objects += sco_pdf.make_paras(
"""
<para align="center"><b>Procès-verbal de %s du département %s - Session unique %s</b></para>
"""
% (
titre_jury,
sco_preferences.get_preference("DeptName", formsemestre_id) or "(sans nom)",
sem["anneescolaire"],
),
style,
)
objects += sco_pdf.make_paras(
"""
<para align="center"><b><i>%s</i></b></para>
"""
% titre_diplome,
style,
)
if showTitle:
objects += sco_pdf.make_paras(
"""<para align="center"><b>Semestre: %s</b></para>""" % sem["titre"], style
)
if sco_preferences.get_preference("PV_TITLE_WITH_VDI", formsemestre_id):
objects += sco_pdf.make_paras(
"""<para align="center">VDI et Code: %s</para>""" % (VDICode or ""), style
)
if date_jury:
objects += sco_pdf.make_paras(
"""<para align="center">Jury tenu le %s</para>""" % date_jury, style
)
objects += sco_pdf.make_paras(
"<para>"
+ (sco_preferences.get_preference("PV_INTRO", formsemestre_id) or "")
% {
"Decnum": numeroArrete,
"VDICode": VDICode,
"UnivName": sco_preferences.get_preference("UnivName", formsemestre_id),
"Type": titre_jury,
"Date": date_commission, # deprecated
"date_commission": date_commission,
}
+ "</para>",
bulletStyle,
)
objects += sco_pdf.make_paras(
"""<para>Le jury propose les décisions suivantes :</para>""", style
)
objects += [Spacer(0, 4 * mm)]
lines, titles, columns_ids = sco_pvjury.pvjury_table(
dpv,
only_diplome=only_diplome,
anonymous=anonymous,
with_paragraph_nom=with_paragraph_nom,
)
# convert to lists of tuples:
columns_ids = ["etudid"] + columns_ids
lines = [[line.get(x, "") for x in columns_ids] for line in lines]
titles = [titles.get(x, "") for x in columns_ids]
# Make a new cell style and put all cells in paragraphs
cell_style = styles.ParagraphStyle({})
cell_style.fontSize = sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
)
cell_style.fontName = sco_preferences.get_preference("PV_FONTNAME", formsemestre_id)
cell_style.leading = 1.0 * sco_preferences.get_preference(
"SCOLAR_FONT_SIZE", formsemestre_id
) # vertical space
LINEWIDTH = 0.5
table_style = [
(
"FONTNAME",
(0, 0),
(-1, 0),
sco_preferences.get_preference("PV_FONTNAME", formsemestre_id),
),
("LINEBELOW", (0, 0), (-1, 0), LINEWIDTH, Color(0, 0, 0)),
("GRID", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]
titles = ["<para><b>%s</b></para>" % x for x in titles]
def _format_pv_cell(x):
"""convert string to paragraph"""
if isinstance(x, str):
return Paragraph(SU(x), cell_style)
else:
return x
Pt = [[_format_pv_cell(x) for x in line[1:]] for line in ([titles] + lines)]
widths = [6 * cm, 2.8 * cm, 2.8 * cm, None, None, None, None]
if dpv["has_prev"]:
widths[2:2] = [2.8 * cm]
if sco_preferences.get_preference("bul_show_mention", formsemestre_id):
widths += [None]
objects.append(Table(Pt, repeatRows=1, colWidths=widths, style=table_style))
# Signature du directeur
objects += sco_pdf.make_paras(
"""<para spaceBefore="10mm" align="right">
%s, %s</para>"""
% (
sco_preferences.get_preference("DirectorName", formsemestre_id) or "",
sco_preferences.get_preference("DirectorTitle", formsemestre_id) or "",
),
style,
)
# Légende des codes
codes = list(codes_cursus.CODES_EXPL.keys())
codes.sort()
objects += sco_pdf.make_paras(
"""<para spaceBefore="15mm" fontSize="14">
<b>Codes utilisés :</b></para>""",
style,
)
L = []
for code in codes:
L.append((code, codes_cursus.CODES_EXPL[code]))
TableStyle2 = [
(
"FONTNAME",
(0, 0),
(-1, 0),
sco_preferences.get_preference("PV_FONTNAME", formsemestre_id),
),
("LINEBELOW", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
("LINEABOVE", (0, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
("LINEBEFORE", (0, 0), (0, -1), LINEWIDTH, Color(0, 0, 0)),
("LINEAFTER", (-1, 0), (-1, -1), LINEWIDTH, Color(0, 0, 0)),
]
objects.append(
Table(
[[Paragraph(SU(x), cell_style) for x in line] for line in L],
colWidths=(2 * cm, None),
style=TableStyle2,
)
)
return objects

View File

@ -1573,8 +1573,7 @@ def formsemestre_graph_cursus(
allkeys=False, # unused allkeys=False, # unused
): ):
"""Graphe suivi cohortes""" """Graphe suivi cohortes"""
annee_bac = str(annee_bac or "") annee_bac = str(annee_bac)
annee_admission = str(annee_admission or "")
# log("formsemestre_graph_cursus") # log("formsemestre_graph_cursus")
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if format == "pdf": if format == "pdf":

View File

@ -63,10 +63,10 @@ class TableRecap(tb.Table):
self.include_evaluations = include_evaluations self.include_evaluations = include_evaluations
self.mode_jury = mode_jury self.mode_jury = mode_jury
self.read_only = read_only # utilisé seulement dans sous-classes self.read_only = read_only # utilisé seulement dans sous-classes
parcours = res.formsemestre.formation.get_parcours() cursus = res.formsemestre.formation.get_cursus()
self.barre_moy = parcours.BARRE_MOY - scu.NOTES_TOLERANCE self.barre_moy = cursus.BARRE_MOY - scu.NOTES_TOLERANCE
self.barre_valid_ue = parcours.NOTES_BARRE_VALID_UE self.barre_valid_ue = cursus.NOTES_BARRE_VALID_UE
self.barre_warning_ue = parcours.BARRE_UE_DISPLAY_WARNING self.barre_warning_ue = cursus.BARRE_UE_DISPLAY_WARNING
self.cache_nomcomplet = {} # cache uid : nomcomplet self.cache_nomcomplet = {} # cache uid : nomcomplet
if convert_values: if convert_values:
self.fmt_note = scu.fmt_note self.fmt_note = scu.fmt_note

View File

@ -21,7 +21,7 @@
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}">{{formsemestre.etuds_inscriptions|length}} inscrits</a></span><span class="lock"> }}">{{formsemestre.etuds_inscriptions|length}} inscrits</a></span><span class="lock">
{%-if not formsemestre.etat -%} {%-if not formsemestre.etat -%}
<a href="{{ url_for( 'notes.formsemestre_flip_lock', <a href="{{ url_for( 'notes.formsemestre_change_lock',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )}}">{{ scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id )}}">{{
scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe
}}</a> }}</a>

View File

@ -49,7 +49,7 @@
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>, <span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
{% endif %} {% endif %}
- parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun', - parcours <b>{{ mod.get_cursus()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
true)|safe true)|safe
}}</b> }}</b>
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %} {% if mod.heures_cours or mod.heures_td or mod.heures_tp %}

View File

@ -19,6 +19,7 @@ from app.auth.models import User
from app.models import Departement, Formation, FormationModalite, Matiere from app.models import Departement, Formation, FormationModalite, Matiere
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
@ -153,7 +154,7 @@ class ScoFake(object):
acronyme="test", acronyme="test",
titre="Formation test", titre="Formation test",
titre_officiel="Le titre officiel de la formation test", titre_officiel="Le titre officiel de la formation test",
type_parcours: int = codes_cursus.CursusDUT.TYPE_CURSUS, type_parcours=codes_cursus.CursusDUT.TYPE_CURSUS,
formation_code=None, formation_code=None,
code_specialite=None, code_specialite=None,
) -> int: ) -> int: