339 lines
13 KiB
Python
339 lines
13 KiB
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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Association de nouvelles versions de formation à des formsemestre
|
|
"""
|
|
import flask
|
|
from flask import url_for, flash
|
|
from flask import g, request
|
|
|
|
from app import db
|
|
from app.models import (
|
|
Module,
|
|
ModuleImpl,
|
|
Evaluation,
|
|
EvaluationUEPoids,
|
|
ScolarEvent,
|
|
ScolarFormSemestreValidation,
|
|
UniteEns,
|
|
)
|
|
|
|
from app.models.formations import Formation
|
|
from app.models.formsemestre import FormSemestre
|
|
import app.scodoc.sco_utils as scu
|
|
|
|
from app import log
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
|
from app.scodoc import sco_formations
|
|
|
|
|
|
def formsemestre_associate_new_version(
|
|
formation_id: int,
|
|
formsemestre_id: int = None,
|
|
other_formsemestre_ids: list[int] = None,
|
|
):
|
|
"""Formulaire nouvelle version formation et association d'un ou plusieurs formsemestre.
|
|
formation_id: la formation à dupliquer
|
|
formsemestre_id: optionnel, formsemestre de départ, qui sera associé à la nouvelle version
|
|
"""
|
|
formsemestre_id = int(formsemestre_id) if formsemestre_id else None
|
|
formation: Formation = Formation.query.get_or_404(formation_id)
|
|
other_formsemestre_ids = {int(x) for x in (other_formsemestre_ids or [])}
|
|
if request.method == "GET":
|
|
# dresse la liste des semestres non verrouillés de la même formation
|
|
other_formsemestres: list[FormSemestre] = formation.formsemestres.filter_by(
|
|
etat=True
|
|
)
|
|
|
|
H = []
|
|
for other_formsemestre in other_formsemestres:
|
|
checked = (
|
|
'checked="checked"'
|
|
if (
|
|
other_formsemestre.id == formsemestre_id
|
|
or other_formsemestre.id in other_formsemestre_ids
|
|
)
|
|
else ""
|
|
)
|
|
disabled = (
|
|
'disabled="1"' if other_formsemestre.id == formsemestre_id else ""
|
|
)
|
|
|
|
H.append(
|
|
f"""<div><input type="checkbox" name="other_formsemestre_ids:list"
|
|
value="{other_formsemestre.id}" {checked} {disabled}
|
|
><a class="stdlink" href="{
|
|
url_for("notes.formsemestre_status",
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=other_formsemestre.id)
|
|
}">{other_formsemestre.titre_mois()}</a></input></div>"""
|
|
)
|
|
if formsemestre_id is None:
|
|
cancel_url = url_for(
|
|
"notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
|
|
)
|
|
else:
|
|
cancel_url = url_for(
|
|
"notes.formsemestre_status",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
)
|
|
|
|
return scu.confirm_dialog(
|
|
(
|
|
"""<h2>Associer à une nouvelle version de formation non verrouillée ?</h2>"""
|
|
if formsemestre_id
|
|
else """<h2>Créer une nouvelle version de la formation ?</h2>"""
|
|
)
|
|
+ f"""<p><b>Formation: </b><a class="stdlink" href="{
|
|
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
|
}">{formation.titre} version {formation.version}</a></p>
|
|
|
|
<p class="help">Le programme pédagogique ("formation") va être dupliqué
|
|
pour que vous puissiez le modifier sans affecter les semestres déjà terminés.
|
|
</p>
|
|
<p class="help">Veillez à ne pas abuser de cette possibilité, car créer
|
|
trop de versions de formations va vous compliquer la gestion
|
|
(à vous de garder trace des différences et à ne pas vous
|
|
tromper par la suite...).
|
|
</p>
|
|
<div class="othersemlist">
|
|
<p>Si vous voulez associer des semestres à la nouvelle
|
|
version, cochez-les maintenant <br>
|
|
(<b>attention : vous ne pourrez pas le faire plus tard car on ne peut pas
|
|
changer la formation d'un semestre !</b>):
|
|
</p>
|
|
{ "".join(H) }
|
|
<p>Les données (étudiants, notes...) de ces semestres seront inchangées.</p>
|
|
</div>
|
|
"""
|
|
+ (
|
|
f"""
|
|
<div class="othersemlist">
|
|
Vous pouvez aussi essayer d'<a class="stdlink" href="{url_for(
|
|
"notes.formsemestre_change_formation",
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id
|
|
)}
|
|
">associer ce semestre à une autre formation identique</a>.
|
|
</div>
|
|
"""
|
|
if formsemestre_id is not None
|
|
else ""
|
|
),
|
|
OK="Créer une nouvelle version et y associer ces semestres",
|
|
dest_url="",
|
|
cancel_url=cancel_url,
|
|
parameters={
|
|
"formation_id": formation_id,
|
|
"formsemestre_id": formsemestre_id,
|
|
},
|
|
)
|
|
elif request.method == "POST":
|
|
if formsemestre_id is not None: # pas dans le form car checkbox disabled
|
|
other_formsemestre_ids |= {formsemestre_id}
|
|
new_formation_id = do_formsemestres_associate_new_version(
|
|
formation_id, other_formsemestre_ids
|
|
)
|
|
flash(
|
|
"Nouvelle version de la formation créée"
|
|
+ (" et semestres associés." if other_formsemestre_ids else ".")
|
|
)
|
|
if formsemestre_id is None:
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.ue_table",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formation_id=new_formation_id,
|
|
)
|
|
)
|
|
else:
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.formsemestre_status",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
)
|
|
)
|
|
else:
|
|
raise ScoValueError("Méthode invalide")
|
|
|
|
|
|
def do_formsemestres_associate_new_version(
|
|
formation_id: int, formsemestre_ids: list[int]
|
|
) -> int:
|
|
"""Crée une nouvelle version de la formation du semestre, et y rattache les semestres.
|
|
Tous les moduleimpl sont ré-associés à la nouvelle formation, ainsi que les decisions de jury
|
|
si elles existent (codes d'UE validées).
|
|
Les semestre doivent tous appartenir à la meme version de la formation.
|
|
renvoie l'id de la nouvelle formation.
|
|
"""
|
|
log(f"do_formsemestres_associate_new_version {formation_id} {formsemestre_ids}")
|
|
|
|
# Check: tous les semestres de la formation
|
|
formsemestres = [FormSemestre.query.get_or_404(i) for i in formsemestre_ids]
|
|
if not all(
|
|
[formsemestre.formation_id == formation_id for formsemestre in formsemestres]
|
|
):
|
|
raise ScoValueError("les semestres ne sont pas tous de la même formation !")
|
|
|
|
# New formation:
|
|
(
|
|
new_formation_id,
|
|
modules_old2new,
|
|
ues_old2new,
|
|
) = sco_formations.formation_create_new_version(formation_id, redirect=False)
|
|
# Log new ues:
|
|
for ue_id in ues_old2new:
|
|
ue = db.session.get(UniteEns, ue_id)
|
|
new_ue = db.session.get(UniteEns, ues_old2new[ue_id])
|
|
assert ue.semestre_idx == new_ue.semestre_idx
|
|
log(f"{ue} -> {new_ue}")
|
|
# Log new modules
|
|
for module_id in modules_old2new:
|
|
mod = db.session.get(Module, module_id)
|
|
new_mod = db.session.get(Module, modules_old2new[module_id])
|
|
assert mod.semestre_id == new_mod.semestre_id
|
|
log(f"{mod} -> {new_mod}")
|
|
# re-associate
|
|
for formsemestre_id in formsemestre_ids:
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
formsemestre.formation_id = new_formation_id
|
|
db.session.add(formsemestre)
|
|
_reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)
|
|
|
|
db.session.commit()
|
|
return formation_id
|
|
|
|
|
|
def _reassociate_moduleimpls(
|
|
formsemestre: FormSemestre,
|
|
ues_old2new: dict[int, int],
|
|
modules_old2new: dict[int, int],
|
|
):
|
|
"""Associe les moduleimpls d'un semestre existant à un autre programme
|
|
et met à jour les décisions de jury (validations d'UE).
|
|
"""
|
|
# re-associate moduleimpls to new modules:
|
|
for modimpl in formsemestre.modimpls:
|
|
modimpl.module_id = modules_old2new[modimpl.module_id]
|
|
db.session.add(modimpl)
|
|
# Update poids des évaluations
|
|
# les poids associent les évaluations aux UE (qui ont changé d'id)
|
|
for poids in EvaluationUEPoids.query.filter(
|
|
EvaluationUEPoids.evaluation_id == Evaluation.id,
|
|
Evaluation.moduleimpl_id == ModuleImpl.id,
|
|
ModuleImpl.formsemestre_id == formsemestre.id,
|
|
):
|
|
if poids.ue_id in ues_old2new:
|
|
poids.ue_id = ues_old2new[poids.ue_id]
|
|
db.session.add(poids)
|
|
else:
|
|
# poids vers une UE qui n'est pas ou plus dans notre formation
|
|
db.session.delete(poids)
|
|
|
|
# update decisions:
|
|
for event in ScolarEvent.query.filter_by(formsemestre_id=formsemestre.id):
|
|
if event.ue_id is not None:
|
|
event.ue_id = ues_old2new[event.ue_id]
|
|
db.session.add(event)
|
|
|
|
for validation in ScolarFormSemestreValidation.query.filter_by(
|
|
formsemestre_id=formsemestre.id
|
|
):
|
|
if (validation.ue_id is not None) and validation.ue_id in ues_old2new:
|
|
validation.ue_id = ues_old2new[validation.ue_id]
|
|
# si l'UE n'est pas ou plus dans notre formation, laisse.
|
|
db.session.add(validation)
|
|
|
|
db.session.commit()
|
|
|
|
|
|
def formations_are_equals(
|
|
formation1: Formation, formation2: Formation = None, formation2_dict: dict = None
|
|
) -> bool:
|
|
"""True if the two formations are exactly the same, except for their versions.
|
|
Can specify either formation2 or its dict repr.
|
|
"""
|
|
fd1 = sco_formations.formation_export_dict(
|
|
formation1, export_external_ues=True, ue_reference_style="acronyme"
|
|
)
|
|
if formation2_dict is None:
|
|
if formation2 is None:
|
|
raise ValueError("must specify formation2 or formation2_dict")
|
|
formation2_dict = sco_formations.formation_export_dict(
|
|
formation2, export_external_ues=True, ue_reference_style="acronyme"
|
|
)
|
|
del fd1["version"]
|
|
if "version" in formation2_dict:
|
|
del formation2_dict["version"]
|
|
return fd1 == formation2_dict
|
|
|
|
|
|
def formsemestre_change_formation(formsemestre: FormSemestre, new_formation: Formation):
|
|
"""Change la formation d'un semestre. La nouvelle formation doit avoir exactement
|
|
le même contenu que l'actuelle, à l'exception du numéro de version.
|
|
"""
|
|
if not formations_are_equals(formsemestre.formation, new_formation):
|
|
raise ScoValueError(
|
|
"formsemestre_change_formation: les deux formations diffèrent"
|
|
)
|
|
log(
|
|
f"formsemestre_change_formation: formsemestre {formsemestre} to formation {new_formation}"
|
|
)
|
|
# Il faut ré-associer tous les modimpls et les UEs
|
|
modules_old2new = {}
|
|
for modimpl in formsemestre.modimpls:
|
|
old_module: Module = modimpl.module
|
|
new_module: Module = (
|
|
Module.query.filter_by(
|
|
formation_id=new_formation.id,
|
|
code=old_module.code,
|
|
titre=old_module.titre,
|
|
)
|
|
.join(UniteEns)
|
|
.filter_by(acronyme=old_module.ue.acronyme)
|
|
.first()
|
|
)
|
|
if new_module is None:
|
|
raise ValueError(
|
|
f"formsemestre_change_formation: erreur sur module {old_module}"
|
|
)
|
|
modules_old2new[old_module.id] = new_module.id
|
|
|
|
ues_old2new = {}
|
|
for old_ue in formsemestre.formation.ues:
|
|
new_ue: UniteEns = UniteEns.query.filter_by(
|
|
formation_id=new_formation.id, acronyme=old_ue.acronyme, titre=old_ue.titre
|
|
).first()
|
|
if new_ue is None:
|
|
raise ValueError(f"formsemestre_change_formation: erreur sur UE {old_ue}")
|
|
ues_old2new[old_ue.id] = new_ue.id
|
|
|
|
formsemestre.formation = new_formation
|
|
db.session.add(formsemestre)
|
|
_reassociate_moduleimpls(formsemestre, ues_old2new, modules_old2new)
|
|
|
|
db.session.commit()
|