# -*- 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 # ############################################################################## """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, ) ) return flask.redirect( url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) ) 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 new_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()