forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into table
This commit is contained in:
commit
8d4cf28e50
@ -88,6 +88,10 @@ def internal_server_error(exc):
|
|||||||
# note that we set the 500 status explicitly
|
# note that we set the 500 status explicitly
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
# Invalide tous les caches
|
||||||
|
log("internal_server_error: clearing caches")
|
||||||
|
clear_scodoc_cache()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
render_template(
|
render_template(
|
||||||
"error_500.j2",
|
"error_500.j2",
|
||||||
|
63
app/forms/formsemestre/change_formation.py
Normal file
63
app/forms/formsemestre/change_formation.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# ScoDoc
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Formulaire changement formation
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import RadioField, SubmitField, validators
|
||||||
|
|
||||||
|
from app.models import Formation
|
||||||
|
|
||||||
|
|
||||||
|
class FormSemestreChangeFormationForm(FlaskForm):
|
||||||
|
"Formulaire changement formation d'un formsemestre"
|
||||||
|
# consrtuit dynamiquement ci-dessous
|
||||||
|
|
||||||
|
|
||||||
|
def gen_formsemestre_change_formation_form(
|
||||||
|
formations: list[Formation],
|
||||||
|
) -> FormSemestreChangeFormationForm:
|
||||||
|
"Create our dynamical form"
|
||||||
|
# see https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
|
||||||
|
class F(FormSemestreChangeFormationForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
setattr(
|
||||||
|
F,
|
||||||
|
"radio_but",
|
||||||
|
RadioField(
|
||||||
|
"Label",
|
||||||
|
choices=[
|
||||||
|
(formation.id, formation.get_titre_version())
|
||||||
|
for formation in formations
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
setattr(F, "submit", SubmitField("Changer la formation"))
|
||||||
|
setattr(F, "cancel", SubmitField("Annuler"))
|
||||||
|
return F()
|
@ -221,6 +221,10 @@ class Module(db.Model):
|
|||||||
"""returns { ue_id : coef }"""
|
"""returns { ue_id : coef }"""
|
||||||
return {p.ue.id: p.coef for p in self.ue_coefs}
|
return {p.ue.id: p.coef for p in self.ue_coefs}
|
||||||
|
|
||||||
|
def get_ue_coef_dict_acronyme(self):
|
||||||
|
"""returns { ue_acronyme : coef }"""
|
||||||
|
return {p.ue.acronyme: p.coef for p in self.ue_coefs}
|
||||||
|
|
||||||
def delete_ue_coef(self, ue):
|
def delete_ue_coef(self, ue):
|
||||||
"""delete coef"""
|
"""delete coef"""
|
||||||
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
if self.formation.has_locked_sems(self.ue.semestre_idx):
|
||||||
|
333
app/scodoc/sco_formation_versions.py
Normal file
333
app/scodoc/sco_formation_versions.py
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
# -*- 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:
|
||||||
|
(
|
||||||
|
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 = UniteEns.query.get(ue_id)
|
||||||
|
new_ue = UniteEns.query.get(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 = Module.query.get(module_id)
|
||||||
|
new_mod = Module.query.get(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 = 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,
|
||||||
|
):
|
||||||
|
poids.ue_id = ues_old2new[poids.ue_id]
|
||||||
|
db.session.add(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:
|
||||||
|
validation.ue_id = ues_old2new[validation.ue_id]
|
||||||
|
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()
|
@ -27,11 +27,10 @@
|
|||||||
|
|
||||||
"""Import / Export de formations
|
"""Import / Export de formations
|
||||||
"""
|
"""
|
||||||
from operator import itemgetter
|
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import flash, g, url_for, request
|
from flask import flash, g, url_for
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
@ -52,7 +51,6 @@ from app.scodoc import codes_cursus
|
|||||||
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
|
||||||
from app.scodoc import sco_formsemestre
|
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_tag_module
|
from app.scodoc import sco_tag_module
|
||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
@ -95,18 +93,19 @@ def formation_list(formation_id=None, args={}): ### XXX obsolete, à supprimer
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def formation_export(
|
def formation_export_dict(
|
||||||
formation_id,
|
formation: Formation,
|
||||||
export_ids=False,
|
export_ids=False,
|
||||||
export_tags=True,
|
export_tags=True,
|
||||||
export_external_ues=False,
|
export_external_ues=False,
|
||||||
export_codes_apo=True,
|
export_codes_apo=True,
|
||||||
format=None,
|
ac_as_list=False,
|
||||||
):
|
ue_reference_style="id",
|
||||||
"""Get a formation, with UE, matieres, modules
|
) -> dict:
|
||||||
in desired format
|
"""Get a formation, with UE, matieres, modules...
|
||||||
|
as a deep dict.
|
||||||
|
ac_as_list spécifie le format des Appentissages Critiques.
|
||||||
"""
|
"""
|
||||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
|
||||||
f_dict = formation.to_dict(with_refcomp_attrs=True)
|
f_dict = formation.to_dict(with_refcomp_attrs=True)
|
||||||
if not export_ids:
|
if not export_ids:
|
||||||
del f_dict["id"]
|
del f_dict["id"]
|
||||||
@ -131,7 +130,8 @@ def formation_export(
|
|||||||
# Et le parcour:
|
# Et le parcour:
|
||||||
if ue.parcour:
|
if ue.parcour:
|
||||||
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
|
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
|
||||||
ue_dict["reference"] = ue.id # pour les coefficients
|
# pour les coefficients:
|
||||||
|
ue_dict["reference"] = ue.id if ue_reference_style == "id" else ue.acronyme
|
||||||
if not export_ids:
|
if not export_ids:
|
||||||
for id_id in (
|
for id_id in (
|
||||||
"id",
|
"id",
|
||||||
@ -165,19 +165,28 @@ def formation_export(
|
|||||||
if tags:
|
if tags:
|
||||||
mod["tags"] = [{"name": x} for x in tags]
|
mod["tags"] = [{"name": x} for x in tags]
|
||||||
#
|
#
|
||||||
module = Module.query.get(module_id)
|
module: Module = Module.query.get(module_id)
|
||||||
if module.is_apc():
|
if module.is_apc():
|
||||||
# Exporte les coefficients
|
# Exporte les coefficients
|
||||||
|
if ue_reference_style == "id":
|
||||||
mod["coefficients"] = [
|
mod["coefficients"] = [
|
||||||
{"ue_reference": str(ue_id), "coef": str(coef)}
|
{"ue_reference": str(ue_id), "coef": str(coef)}
|
||||||
for (ue_id, coef) in module.get_ue_coef_dict().items()
|
for (ue_id, coef) in module.get_ue_coef_dict().items()
|
||||||
]
|
]
|
||||||
|
else:
|
||||||
|
mod["coefficients"] = [
|
||||||
|
{"ue_reference": ue_acronyme, "coef": str(coef)}
|
||||||
|
for (
|
||||||
|
ue_acronyme,
|
||||||
|
coef,
|
||||||
|
) in module.get_ue_coef_dict_acronyme().items()
|
||||||
|
]
|
||||||
# Et les parcours
|
# Et les parcours
|
||||||
mod["parcours"] = [
|
mod["parcours"] = [
|
||||||
p.to_dict(with_annees=False) for p in module.parcours
|
p.to_dict(with_annees=False) for p in module.parcours
|
||||||
]
|
]
|
||||||
# Et les AC
|
# Et les AC
|
||||||
if format == "xml":
|
if ac_as_list:
|
||||||
# XML préfère une liste
|
# XML préfère une liste
|
||||||
mod["app_critiques"] = [
|
mod["app_critiques"] = [
|
||||||
x.to_dict(with_code=True) for x in module.app_critiques
|
x.to_dict(with_code=True) for x in module.app_critiques
|
||||||
@ -196,7 +205,29 @@ def formation_export(
|
|||||||
del mod["code_apogee"]
|
del mod["code_apogee"]
|
||||||
if mod["ects"] is None:
|
if mod["ects"] is None:
|
||||||
del mod["ects"]
|
del mod["ects"]
|
||||||
|
return f_dict
|
||||||
|
|
||||||
|
|
||||||
|
def formation_export(
|
||||||
|
formation_id,
|
||||||
|
export_ids=False,
|
||||||
|
export_tags=True,
|
||||||
|
export_external_ues=False,
|
||||||
|
export_codes_apo=True,
|
||||||
|
format=None,
|
||||||
|
) -> flask.Response:
|
||||||
|
"""Get a formation, with UE, matieres, modules
|
||||||
|
in desired format
|
||||||
|
"""
|
||||||
|
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||||
|
f_dict = formation_export_dict(
|
||||||
|
formation,
|
||||||
|
export_ids=export_ids,
|
||||||
|
export_tags=export_tags,
|
||||||
|
export_external_ues=export_external_ues,
|
||||||
|
export_codes_apo=export_codes_apo,
|
||||||
|
ac_as_list=format == "xml",
|
||||||
|
)
|
||||||
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
|
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
|
||||||
return scu.sendResult(
|
return scu.sendResult(
|
||||||
f_dict,
|
f_dict,
|
||||||
@ -597,5 +628,4 @@ def formation_create_new_version(formation_id, redirect=True):
|
|||||||
msg="Nouvelle version !",
|
msg="Nouvelle version !",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
return new_id, modules_old2new, ues_old2new
|
return new_id, modules_old2new, ues_old2new
|
||||||
|
@ -39,7 +39,6 @@ from app.models import (
|
|||||||
Module,
|
Module,
|
||||||
ModuleImpl,
|
ModuleImpl,
|
||||||
Evaluation,
|
Evaluation,
|
||||||
EvaluationUEPoids,
|
|
||||||
UniteEns,
|
UniteEns,
|
||||||
ScolarFormSemestreValidation,
|
ScolarFormSemestreValidation,
|
||||||
ScolarAutorisationInscription,
|
ScolarAutorisationInscription,
|
||||||
@ -64,14 +63,11 @@ from app.scodoc import codes_cursus
|
|||||||
from app.scodoc import sco_compute_moy
|
from app.scodoc import sco_compute_moy
|
||||||
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
|
||||||
from app.scodoc import sco_etud
|
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_formations
|
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_groups_copy
|
from app.scodoc import sco_groups_copy
|
||||||
from app.scodoc import sco_modalites
|
from app.scodoc import sco_modalites
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_cursus_dut
|
|
||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
from app.scodoc import sco_portal_apogee
|
from app.scodoc import sco_portal_apogee
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
@ -1200,7 +1196,7 @@ def do_formsemestre_clone(
|
|||||||
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
|
||||||
New dates, responsable_id
|
New dates, responsable_id
|
||||||
"""
|
"""
|
||||||
log(f"cloning orig_formsemestre_id")
|
log(f"do_formsemestre_clone: {orig_formsemestre_id}")
|
||||||
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
|
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
|
||||||
orig_formsemestre_id
|
orig_formsemestre_id
|
||||||
)
|
)
|
||||||
@ -1291,207 +1287,6 @@ def do_formsemestre_clone(
|
|||||||
return formsemestre_id
|
return formsemestre_id
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
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 noiuvelle version
|
|
||||||
"""
|
|
||||||
if formsemestre_id is not None:
|
|
||||||
formsemestre_id = int(formsemestre_id)
|
|
||||||
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>",
|
|
||||||
OK="Créer une nouvelle version et y associer ces semestres",
|
|
||||||
dest_url="",
|
|
||||||
cancel_url=cancel_url,
|
|
||||||
parameters={"formation_id": formation_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 semestre 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 !")
|
|
||||||
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
# New formation:
|
|
||||||
(
|
|
||||||
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 = UniteEns.query.get(ue_id)
|
|
||||||
new_ue = UniteEns.query.get(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 = Module.query.get(module_id)
|
|
||||||
new_mod = Module.query.get(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:
|
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
||||||
sem["formation_id"] = formation_id
|
|
||||||
sco_formsemestre.do_formsemestre_edit(sem, cnx=cnx, html_quote=False)
|
|
||||||
_reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
|
|
||||||
|
|
||||||
cnx.commit()
|
|
||||||
return formation_id
|
|
||||||
|
|
||||||
|
|
||||||
def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new):
|
|
||||||
"""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:
|
|
||||||
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
|
|
||||||
for mod in modimpls:
|
|
||||||
mod["module_id"] = modules_old2new[mod["module_id"]]
|
|
||||||
sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id)
|
|
||||||
# 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,
|
|
||||||
):
|
|
||||||
poids.ue_id = ues_old2new[poids.ue_id]
|
|
||||||
db.session.add(poids)
|
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# update decisions:
|
|
||||||
events = sco_etud.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
|
|
||||||
for e in events:
|
|
||||||
if e["ue_id"]:
|
|
||||||
e["ue_id"] = ues_old2new[e["ue_id"]]
|
|
||||||
sco_etud.scolar_events_edit(cnx, e)
|
|
||||||
validations = sco_cursus_dut.scolar_formsemestre_validation_list(
|
|
||||||
cnx, args={"formsemestre_id": formsemestre_id}
|
|
||||||
)
|
|
||||||
for e in validations:
|
|
||||||
if e["ue_id"]:
|
|
||||||
e["ue_id"] = ues_old2new[e["ue_id"]]
|
|
||||||
# log('e=%s' % e )
|
|
||||||
sco_cursus_dut.scolar_formsemestre_validation_edit(cnx, e)
|
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_delete(formsemestre_id):
|
def formsemestre_delete(formsemestre_id):
|
||||||
"""Delete a formsemestre (affiche avertissements)"""
|
"""Delete a formsemestre (affiche avertissements)"""
|
||||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
@ -349,7 +349,7 @@ SCO_USER_MANUAL = "https://scodoc.org/GuideUtilisateur"
|
|||||||
SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces"
|
SCO_ANNONCES_WEBSITE = "https://listes.univ-paris13.fr/mailman/listinfo/scodoc-annonces"
|
||||||
SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr"
|
SCO_DEVEL_LIST = "scodoc-devel@listes.univ-paris13.fr"
|
||||||
SCO_USERS_LIST = "notes@listes.univ-paris13.fr"
|
SCO_USERS_LIST = "notes@listes.univ-paris13.fr"
|
||||||
SCO_LISTS_URL = "https://scodoc.org/ListesDeDiffusion/"
|
SCO_LISTS_URL = "https://scodoc.org/Contact"
|
||||||
SCO_DISCORD_ASSISTANCE = "https://discord.gg/ybw6ugtFsZ"
|
SCO_DISCORD_ASSISTANCE = "https://discord.gg/ybw6ugtFsZ"
|
||||||
|
|
||||||
# Mails avec exceptions (erreurs) anormales envoyés à cette adresse:
|
# Mails avec exceptions (erreurs) anormales envoyés à cette adresse:
|
||||||
|
@ -16,7 +16,7 @@ function is_valid_note(v) {
|
|||||||
var note_min = parseFloat($("#eval_note_min").text());
|
var note_min = parseFloat($("#eval_note_min").text());
|
||||||
var note_max = parseFloat($("#eval_note_max").text());
|
var note_max = parseFloat($("#eval_note_max").text());
|
||||||
|
|
||||||
if (!v.match("^-?[0-9.]*$")) {
|
if (!v.match("^-?[0-9]*.?[0-9]*$")) {
|
||||||
return (v == "ABS") || (v == "EXC") || (v == "SUPR") || (v == "ATT") || (v == "DEM");
|
return (v == "ABS") || (v == "EXC") || (v == "SUPR") || (v == "ATT") || (v == "DEM");
|
||||||
} else {
|
} else {
|
||||||
var x = parseFloat(v);
|
var x = parseFloat(v);
|
||||||
@ -51,9 +51,9 @@ function save_note(elem, v, etudid) {
|
|||||||
'comment': document.getElementById('formnotes_comment').value
|
'comment': document.getElementById('formnotes_comment').value
|
||||||
},
|
},
|
||||||
function (result) {
|
function (result) {
|
||||||
|
if (result['nbchanged'] > 0) {
|
||||||
sco_message("enregistré");
|
sco_message("enregistré");
|
||||||
elem.className = "note_saved";
|
elem.className = "note_saved";
|
||||||
if (result['nbchanged'] > 0) {
|
|
||||||
// il y avait une decision de jury ?
|
// il y avait une decision de jury ?
|
||||||
if (result.existing_decisions[0] == etudid) {
|
if (result.existing_decisions[0] == etudid) {
|
||||||
if (v != $(elem).attr('data-orig-value')) {
|
if (v != $(elem).attr('data-orig-value')) {
|
||||||
@ -66,8 +66,12 @@ function save_note(elem, v, etudid) {
|
|||||||
if (result['history_menu']) {
|
if (result['history_menu']) {
|
||||||
$("#hist_" + etudid).html(result['history_menu']);
|
$("#hist_" + etudid).html(result['history_menu']);
|
||||||
}
|
}
|
||||||
|
$(elem).attr('data-last-saved-value', v);
|
||||||
|
} else {
|
||||||
|
$('#sco_msg').html("").show();
|
||||||
|
sco_message("valeur non enregistrée");
|
||||||
}
|
}
|
||||||
$(elem).attr('data-last-saved-value', v)
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,9 @@
|
|||||||
}}">le canal Discord</a></b>.
|
}}">le canal Discord</a></b>.
|
||||||
</p>
|
</p>
|
||||||
{% if 'scodoc_dept' in g %}
|
{% if 'scodoc_dept' in g %}
|
||||||
<p>Pour aider les développeurs à corriger le problème, nous vous
|
<p>Pour aider à corriger le problème, nous vous
|
||||||
suggérons d'envoyer les données anonymisées sur votre configuration:
|
<b>remercions d'envoyer ce rapport d'erreur</b>
|
||||||
|
(qui contient des données anonymisées sur votre configuration):
|
||||||
<form method="POST" action="{{ url_for( 'scolar.sco_dump_and_send_db',
|
<form method="POST" action="{{ url_for( 'scolar.sco_dump_and_send_db',
|
||||||
scodoc_dept=g.scodoc_dept ) }}">
|
scodoc_dept=g.scodoc_dept ) }}">
|
||||||
<input type="hidden" name="request_url" value="{{request_url}}">
|
<input type="hidden" name="request_url" value="{{request_url}}">
|
||||||
@ -29,10 +30,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p>Vous pouvez aussi envoyer un mail à la liste "notes"
|
<p>Vous pouvez aussi contacter l'équipe de développeurs:
|
||||||
<a href="mailto:{{scu.SCO_USERS_LIST}}">{{scu.SCO_USERS_LIST}}</a>
|
|
||||||
</p>
|
|
||||||
<p>Pour plus d'informations sur les listes de diffusion
|
|
||||||
<a href="{{ scu.SCO_LISTS_URL }}">voir cette page</a>.
|
<a href="{{ scu.SCO_LISTS_URL }}">voir cette page</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
30
app/templates/formsemestre/change_formation.j2
Normal file
30
app/templates/formsemestre/change_formation.j2
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<h2>Changement de la formation du semestre</h2>
|
||||||
|
|
||||||
|
<p class="help"> On ne peut pas changer la formation d'un semestre existant car
|
||||||
|
elle défini son organisation (modules, ...), SAUF si la nouvelle formation a
|
||||||
|
<em>exactement</em> le même contenu que l'existante.
|
||||||
|
Cela peut arriver par exemple lorsqu'on crée une nouvelle version (pas encore modifiée)
|
||||||
|
et que l'on a oublié d'y rattacher un semestre.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{% if formations %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="fontred">Aucune formation ne peut se substituer à celle de ce semestre.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -105,4 +105,13 @@ class ScoData:
|
|||||||
self.prefs = sco_preferences.SemPreferences(formsemestre_id)
|
self.prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||||
|
|
||||||
|
|
||||||
from app.views import scodoc, notes, scolar, absences, users, pn_modules, refcomp
|
from app.views import (
|
||||||
|
absences,
|
||||||
|
notes_formsemestre,
|
||||||
|
notes,
|
||||||
|
pn_modules,
|
||||||
|
refcomp,
|
||||||
|
scodoc,
|
||||||
|
scolar,
|
||||||
|
users,
|
||||||
|
)
|
||||||
|
@ -109,6 +109,7 @@ from app.scodoc import sco_evaluation_recap
|
|||||||
from app.scodoc import sco_export_results
|
from app.scodoc import sco_export_results
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
from app.scodoc import sco_formation_recap
|
from app.scodoc import sco_formation_recap
|
||||||
|
from app.scodoc import sco_formation_versions
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_formsemestre_custommenu
|
from app.scodoc import sco_formsemestre_custommenu
|
||||||
from app.scodoc import sco_formsemestre_edit
|
from app.scodoc import sco_formsemestre_edit
|
||||||
@ -188,7 +189,7 @@ sco_publish(
|
|||||||
)
|
)
|
||||||
sco_publish(
|
sco_publish(
|
||||||
"/formsemestre_associate_new_version",
|
"/formsemestre_associate_new_version",
|
||||||
sco_formsemestre_edit.formsemestre_associate_new_version,
|
sco_formation_versions.formsemestre_associate_new_version,
|
||||||
Permission.ScoChangeFormation,
|
Permission.ScoChangeFormation,
|
||||||
methods=["GET", "POST"],
|
methods=["GET", "POST"],
|
||||||
)
|
)
|
||||||
|
107
app/views/notes_formsemestre.py
Normal file
107
app/views/notes_formsemestre.py
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# ScoDoc
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Vues "modernes" des formsemestre
|
||||||
|
Emmanuel Viennet, 2023
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import flash, redirect, render_template, url_for
|
||||||
|
from flask import g, request
|
||||||
|
|
||||||
|
from app.decorators import (
|
||||||
|
scodoc,
|
||||||
|
permission_required,
|
||||||
|
)
|
||||||
|
from app.forms.formsemestre import change_formation
|
||||||
|
from app.models import Formation, FormSemestre
|
||||||
|
from app.scodoc import sco_formations, sco_formation_versions
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.views import notes_bp as bp
|
||||||
|
from app.views import ScoData
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route(
|
||||||
|
"/formsemestre_change_formation/<int:formsemestre_id>", methods=["GET", "POST"]
|
||||||
|
)
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoImplement)
|
||||||
|
def formsemestre_change_formation(formsemestre_id: int):
|
||||||
|
"""Propose de changer un formsemestre de formation.
|
||||||
|
Cette opération est bien sûr impossible... sauf si les deux formations sont identiques.
|
||||||
|
Par exemple, on vient de créer une formation, et on a oublié d'y associé un formsemestre
|
||||||
|
existant.
|
||||||
|
"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
formation_dict = sco_formations.formation_export_dict(
|
||||||
|
formsemestre.formation, export_external_ues=True, ue_reference_style="acronyme"
|
||||||
|
)
|
||||||
|
formations = [
|
||||||
|
formation
|
||||||
|
for formation in Formation.query.filter_by(
|
||||||
|
dept_id=formsemestre.dept_id, acronyme=formsemestre.formation.acronyme
|
||||||
|
)
|
||||||
|
if formation.id != formsemestre.formation.id
|
||||||
|
and sco_formation_versions.formations_are_equals(
|
||||||
|
formation, formation2_dict=formation_dict
|
||||||
|
)
|
||||||
|
]
|
||||||
|
form = change_formation.gen_formsemestre_change_formation_form(formations)
|
||||||
|
if request.method == "POST" and form.validate:
|
||||||
|
if not form.cancel.data:
|
||||||
|
new_formation_id = form.radio_but.data
|
||||||
|
if new_formation_id is None: # pas de choix radio
|
||||||
|
flash("Pas de formation sélectionnée !")
|
||||||
|
return render_template(
|
||||||
|
"formsemestre/change_formation.j2",
|
||||||
|
form=form,
|
||||||
|
formations=formations,
|
||||||
|
formsemestre=formsemestre,
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
new_formation: Formation = Formation.query.filter_by(
|
||||||
|
dept_id=g.scodoc_dept_id, formation_id=new_formation_id
|
||||||
|
).first_or_404()
|
||||||
|
sco_formation_versions.formsemestre_change_formation(
|
||||||
|
formsemestre, new_formation
|
||||||
|
)
|
||||||
|
flash("Formation du semestre modifiée")
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# GET
|
||||||
|
return render_template(
|
||||||
|
"formsemestre/change_formation.j2",
|
||||||
|
form=form,
|
||||||
|
formations=formations,
|
||||||
|
formsemestre=formsemestre,
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
)
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.4.68"
|
SCOVERSION = "9.4.70"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import pytest
|
|||||||
from tests.unit import yaml_setup, call_view
|
from tests.unit import yaml_setup, call_view
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db
|
|
||||||
from app.models import Formation, FormSemestre
|
from app.models import Formation, FormSemestre
|
||||||
from app.scodoc import (
|
from app.scodoc import (
|
||||||
sco_archives,
|
sco_archives,
|
||||||
@ -18,6 +17,7 @@ from app.scodoc import (
|
|||||||
sco_evaluations,
|
sco_evaluations,
|
||||||
sco_evaluation_check_abs,
|
sco_evaluation_check_abs,
|
||||||
sco_evaluation_recap,
|
sco_evaluation_recap,
|
||||||
|
sco_formation_versions,
|
||||||
sco_formsemestre_edit,
|
sco_formsemestre_edit,
|
||||||
sco_formsemestre_inscriptions,
|
sco_formsemestre_inscriptions,
|
||||||
sco_formsemestre_status,
|
sco_formsemestre_status,
|
||||||
@ -52,7 +52,7 @@ def test_formsemestres_associate_new_version(test_client):
|
|||||||
assert {s.semestre_id for s in formsemestres} == {1}
|
assert {s.semestre_id for s in formsemestres} == {1}
|
||||||
# Les rattache à une nouvelle version de la formation:
|
# Les rattache à une nouvelle version de la formation:
|
||||||
formsemestre_ids = [s.id for s in formsemestres]
|
formsemestre_ids = [s.id for s in formsemestres]
|
||||||
sco_formsemestre_edit.do_formsemestres_associate_new_version(
|
sco_formation_versions.do_formsemestres_associate_new_version(
|
||||||
formation.id, formsemestre_ids
|
formation.id, formsemestre_ids
|
||||||
)
|
)
|
||||||
new_formation: Formation = Formation.query.filter_by(
|
new_formation: Formation = Formation.query.filter_by(
|
||||||
@ -107,7 +107,7 @@ def test_formsemestre_misc_views(test_client):
|
|||||||
assert isinstance(ans, (str, Response)) # ici str
|
assert isinstance(ans, (str, Response)) # ici str
|
||||||
# Juste la page dialogue avant opération::
|
# Juste la page dialogue avant opération::
|
||||||
ans = sco_formsemestre_edit.formsemestre_clone(formsemestre.id)
|
ans = sco_formsemestre_edit.formsemestre_clone(formsemestre.id)
|
||||||
ans = sco_formsemestre_edit.formsemestre_associate_new_version(
|
ans = sco_formation_versions.formsemestre_associate_new_version(
|
||||||
formsemestre.formation_id, formsemestre.id
|
formsemestre.formation_id, formsemestre.id
|
||||||
)
|
)
|
||||||
ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)
|
ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)
|
||||||
|
Loading…
Reference in New Issue
Block a user