1
0
forked from ScoDoc/ScoDoc

Création de nouvelles versions de formations: amélioration dialogue, propose systématriquement d'embarquer des formsemestres

This commit is contained in:
Emmanuel Viennet 2023-02-23 21:19:57 +01:00
parent 6e1bffab4f
commit b728e06f27
9 changed files with 167 additions and 118 deletions

View File

@ -68,10 +68,12 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
) )
for sem in sems: for sem in sems:
H.append( H.append(
'<li><a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>' '<li><a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titremois)s</a></li>'
% sem % sem
) )
H.append('</ul><p><a href="%s">Revenir</a></p>' % scu.NotesURL()) H.append(
'</ul><p><a class="stdlink" href="%s">Revenir</a></p>' % scu.NotesURL()
)
else: else:
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(

View File

@ -118,7 +118,7 @@ def do_ue_create(args):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)""" (chaque UE doit avoir un acronyme unique dans la formation)"""
) )
if not "ue_code" in args: if (not "ue_code" in args) or (not args["ue_code"].strip()):
# évite les conflits de code # évite les conflits de code
while True: while True:
cursor = db.session.execute("select notes_newid_ucod();") cursor = db.session.execute("select notes_newid_ucod();")
@ -405,6 +405,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"explanation": """code interne (non vide). Toutes les UE partageant le même code "explanation": """code interne (non vide). Toutes les UE partageant le même code
(et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE). (et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE).
Voir liste ci-dessous.""", Voir liste ci-dessous.""",
"allow_null": False,
}, },
), ),
( (
@ -663,6 +664,13 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
ues_externes_obj = UniteEns.query.filter_by( ues_externes_obj = UniteEns.query.filter_by(
formation_id=formation_id, is_external=True formation_id=formation_id, is_external=True
) )
# liste ordonnée des formsemestres de cette formation:
formsemestres = sorted(
FormSemestre.query.filter_by(formation_id=formation_id).all(),
key=lambda s: s.sort_key(),
reverse=True,
)
if is_apc: if is_apc:
# Pour faciliter la transition des anciens programmes non APC # Pour faciliter la transition des anciens programmes non APC
for ue in ues_obj: for ue in ues_obj:
@ -901,18 +909,29 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
""" """
) )
H.append("<p><ul>") H.append("<p><ul>")
if editable: if has_perm_change:
H.append( H.append(
f""" f"""
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for('notes.formation_create_new_version', url_for('notes.formsemestre_associate_new_version',
scodoc_dept=g.scodoc_dept, formation_id=formation_id scodoc_dept=g.scodoc_dept, formation_id=formation_id
) )
}">Créer une nouvelle version (non verrouillée)</a> }">Créer une nouvelle version de la formation</a> (copie non verrouillée)
</li> </li>
""" """
) )
if not len(formsemestres):
H.append(
f"""
<li><a class="stdlink" href="{
url_for('notes.formation_delete',
scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
}">Supprimer cette formation</a> (pas encore utilisée par des semestres)
</li>
"""
)
H.append( H.append(
f""" f"""
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
@ -951,11 +970,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<h3> <a name="sems">Semestres ou sessions de cette formation</a></h3> <h3> <a name="sems">Semestres ou sessions de cette formation</a></h3>
<p><ul>""" <p><ul>"""
) )
for formsemestre in sorted( for formsemestre in formsemestres:
FormSemestre.query.filter_by(formation_id=formation_id).all(),
key=lambda s: s.sort_key(),
reverse=True,
):
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,

View File

@ -474,22 +474,23 @@ def formation_list_table() -> GenTable:
FormSemestre.date_debut FormSemestre.date_debut
).all() ).all()
row["sems_list_txt"] = ", ".join(s.session_id() for s in row["formsemestres"]) row["sems_list_txt"] = ", ".join(s.session_id() for s in row["formsemestres"])
row["_sems_list_txt_html"] = ( row["_sems_list_txt_html"] = ", ".join(
", ".join( [
f"""<a class="discretelink" href="{ f"""<a class="discretelink" href="{
url_for("notes.formsemestre_status", url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=s.id scodoc_dept=g.scodoc_dept, formsemestre_id=s.id
)}">{s.session_id()}</a> )}">{s.session_id()}</a>"""
"""
for s in row["formsemestres"] for s in row["formsemestres"]
) ]
+ f""", <a class="stdlink" id="add-semestre-{ + [
f"""<a class="stdlink" id="add-semestre-{
formation.acronyme.lower().replace(" ", "-")}" formation.acronyme.lower().replace(" ", "-")}"
href="{ url_for("notes.formsemestre_createwithmodules", href="{ url_for("notes.formsemestre_createwithmodules",
scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1 scodoc_dept=g.scodoc_dept, formation_id=formation.id, semestre_id=1
) )
}">ajouter</a> }">ajouter</a>
""" """
]
) )
if row["formsemestres"]: if row["formsemestres"]:
row["date_fin_dernier_sem"] = ( row["date_fin_dernier_sem"] = (

View File

@ -1285,80 +1285,108 @@ def do_formsemestre_clone(
def formsemestre_associate_new_version( def formsemestre_associate_new_version(
formsemestre_id, formation_id: int,
other_formsemestre_ids=[], formsemestre_id: int = None,
dialog_confirmed=False, other_formsemestre_ids: list[int] = None,
): ):
"""Formulaire changement formation d'un semestre""" """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) formsemestre_id = int(formsemestre_id)
other_formsemestre_ids = [int(x) for x in other_formsemestre_ids] formation: Formation = Formation.query.get_or_404(formation_id)
if not dialog_confirmed: other_formsemestre_ids = {int(x) for x in other_formsemestre_ids or []}
# dresse le liste des semestres de la meme formation et version if request.method == "GET":
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) # dresse la liste des semestres non verrouillés de la même formation
othersems = sco_formsemestre.do_formsemestre_list( other_formsemestres: list[FormSemestre] = formation.formsemestres.filter_by(
args={ etat=True
"formation_id": formsemestre.formation.id,
"version": formsemestre.formation.version,
"etat": "1",
},
) )
H = [] H = []
for s in othersems: for other_formsemestre in other_formsemestres:
checked = (
'checked="checked"'
if ( if (
s["formsemestre_id"] == formsemestre_id other_formsemestre.id == formsemestre_id
or s["formsemestre_id"] in other_formsemestre_ids or other_formsemestre.id in other_formsemestre_ids
): )
checked = 'checked="checked"' else ""
else: )
checked = "" disabled = (
if s["formsemestre_id"] == formsemestre_id: 'disabled="1"' if other_formsemestre.id == formsemestre_id else ""
disabled = 'disabled="1"' )
else:
disabled = ""
H.append( H.append(
f"""<div><input type="checkbox" name="other_formsemestre_ids:list" f"""<div><input type="checkbox" name="other_formsemestre_ids:list"
value="{s['formsemestre_id']}" {checked} {disabled} value="{other_formsemestre.id}" {checked} {disabled}
>{s['titremois']}</input></div>""" ><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( return scu.confirm_dialog(
f"""<h2>Associer à une nouvelle version de formation non verrouillée ?</h2> (
"""<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é <p class="help">Le programme pédagogique ("formation") va être dupliqué
pour que vous puissiez le modifier sans affecter les autres pour que vous puissiez le modifier sans affecter les semestres déjà terminés.
semestres. Les autres paramètres (étudiants, notes...) du
semestre seront inchangés.
</p> </p>
<p class="help">Veillez à ne pas abuser de cette possibilité, car créer <p class="help">Veillez à ne pas abuser de cette possibilité, car créer
trop de versions de formations va vous compliquer la gestion trop de versions de formations va vous compliquer la gestion
(à vous de garder trace des différences et à ne pas vous (à vous de garder trace des différences et à ne pas vous
tromper par la suite...). tromper par la suite...).
</p> </p>
<p class="help">Si vous souhaitez créer un programme pour de futurs semestres,
utilisez plutôt <a class="stdlink" href="{
url_for('notes.formation_create_new_version',
scodoc_dept=g.scodoc_dept, formation_id=formsemestre.formation.id
)}">Créer une nouvelle version</a>.
</p>
<div class="othersemlist"> <div class="othersemlist">
<p>Si vous voulez associer aussi d'autres semestres à la nouvelle <p>Si vous voulez associer des semestres à la nouvelle
version, cochez-les: version, cochez-les maintenant <br>
(<b>attention&nbsp;: vous ne pourrez pas le faire plus tard car on ne peut pas
changer la formation d'un semestre !</b>):
</p>""" </p>"""
+ "".join(H) + "".join(H)
+ """<p>Les données (étudiants, notes...) de ces semestres seront inchangées.</p>"""
+ "</div>", + "</div>",
OK="Associer ces semestres à une nouvelle version", OK="Créer une nouvelle version et y associer ces semestres",
dest_url="", dest_url="",
cancel_url=url_for( cancel_url=cancel_url,
"notes.formsemestre_status", 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, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formation_id=new_formation_id,
), )
parameters={"formsemestre_id": formsemestre_id},
) )
else: else:
do_formsemestres_associate_new_version(
[formsemestre_id] + other_formsemestre_ids
)
flash("Semestre associé à une nouvelle version de la formation")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
@ -1366,25 +1394,26 @@ def formsemestre_associate_new_version(
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
) )
else:
raise ScoValueError("Méthode invalide")
def do_formsemestres_associate_new_version(formsemestre_ids): def do_formsemestres_associate_new_version(
"""Cree une nouvelle version de la formation du semestre, et y rattache les semestres. 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 -associés à la nouvelle formation, ainsi que les decisions de jury Tous les moduleimpl sont -associés à la nouvelle formation, ainsi que les decisions de jury
si elles existent (codes d'UE validées). si elles existent (codes d'UE validées).
Les semestre doivent tous appartenir à la meme version de la formation 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 {formsemestre_ids}") log(f"do_formsemestres_associate_new_version {formation_id} {formsemestre_ids}")
if not formsemestre_ids:
return # Check: tous les semestre de la formation
# Check: tous de la même formation formsemestres = [FormSemestre.query.get_or_404(i) for i in formsemestre_ids]
assert isinstance(formsemestre_ids[0], int) if not all(
sem = sco_formsemestre.get_formsemestre(formsemestre_ids[0]) [formsemestre.formation_id == formation_id for formsemestre in formsemestres]
formation_id = sem["formation_id"] ):
for formsemestre_id in formsemestre_ids[1:]:
assert isinstance(formsemestre_id, int)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if formation_id != sem["formation_id"]:
raise ScoValueError("les semestres ne sont pas tous de la même formation !") raise ScoValueError("les semestres ne sont pas tous de la même formation !")
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
@ -1414,6 +1443,7 @@ def do_formsemestres_associate_new_version(formsemestre_ids):
_reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new) _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
cnx.commit() cnx.commit()
return formation_id
def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new): def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new):
@ -1676,7 +1706,7 @@ def formsemestre_change_publication_bul(
msg = "" msg = ""
return scu.confirm_dialog( return scu.confirm_dialog(
"<h2>Confirmer la %s publication des bulletins ?</h2>" % msg, "<h2>Confirmer la %s publication des bulletins ?</h2>" % msg,
helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins, help_msg="""Il est parfois utile de désactiver la diffusion des bulletins,
par exemple pendant la tenue d'un jury ou avant harmonisation des notes. par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
<br> <br>
Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant. Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant.

View File

@ -236,7 +236,10 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
{ {
"title": "Associer à une nouvelle version du programme", "title": "Associer à une nouvelle version du programme",
"endpoint": "notes.formsemestre_associate_new_version", "endpoint": "notes.formsemestre_associate_new_version",
"args": {"formsemestre_id": formsemestre_id}, "args": {
"formsemestre_id": formsemestre_id,
"formation_id": formsemestre.formation_id,
},
"enabled": current_user.has_permission(Permission.ScoChangeFormation) "enabled": current_user.has_permission(Permission.ScoChangeFormation)
and formsemestre.etat, and formsemestre.etat,
"helpmsg": "", "helpmsg": "",

View File

@ -1075,16 +1075,18 @@ def query_portal(req, msg="Portail Apogee", timeout=3):
def confirm_dialog( def confirm_dialog(
message="<p>Confirmer ?</p>", message="<p>Confirmer ?</p>",
OK="OK", OK="OK",
Cancel="Annuler",
dest_url="",
cancel_url="",
target_variable="dialog_confirmed",
parameters={},
add_headers=True, # complete page add_headers=True, # complete page
helpmsg=None, cancel_label="Annuler",
cancel_url="",
dest_url="",
help_msg=None,
parameters: dict = None,
target_variable="dialog_confirmed",
): ):
"""HTML confirmation dialog: submit (POST) to same page or dest_url if given."""
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
parameters = parameters or {}
# dialog de confirmation simple # dialog de confirmation simple
parameters[target_variable] = 1 parameters[target_variable] = 1
# Attention: la page a pu etre servie en GET avec des parametres # Attention: la page a pu etre servie en GET avec des parametres
@ -1105,24 +1107,22 @@ def confirm_dialog(
H.append(f'<input type="submit" value="{OK}"/>') H.append(f'<input type="submit" value="{OK}"/>')
if cancel_url: if cancel_url:
H.append( H.append(
"""<input type ="button" value="%s" f"""<input type ="button" value="{cancel_label}"
onClick="document.location='%s';"/>""" onClick="document.location='{cancel_url}';"/>"""
% (Cancel, cancel_url)
) )
for param in parameters.keys(): for param in parameters.keys():
if parameters[param] is None: if parameters[param] is None:
parameters[param] = "" parameters[param] = ""
if type(parameters[param]) == type([]): if isinstance(parameters[param], list):
for e in parameters[param]: for e in parameters[param]:
H.append('<input type="hidden" name="%s" value="%s"/>' % (param, e)) H.append(f"""<input type="hidden" name="{param}" value="{e}"/>""")
else: else:
H.append( H.append(
'<input type="hidden" name="%s" value="%s"/>' f"""<input type="hidden" name="{param}" value="{parameters[param]}"/>"""
% (param, parameters[param])
) )
H.append("</form>") H.append("</form>")
if helpmsg: if help_msg:
H.append('<p class="help">' + helpmsg + "</p>") H.append('<p class="help">' + help_msg + "</p>")
if add_headers: if add_headers:
return ( return (
html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer() html_sco_header.sco_header() + "\n".join(H) + html_sco_header.sco_footer()

View File

@ -3622,10 +3622,6 @@ div.othersemlist {
border: 1px solid gray; border: 1px solid gray;
} }
div.othersemlist p {
font-weight: bold;
margin-top: 0px;
}
div.othersemlist input { div.othersemlist input {
margin-left: 20px; margin-left: 20px;

View File

@ -738,11 +738,11 @@ def formation_import_xml_form():
""" """
sco_publish( # sco_publish(
"/formation_create_new_version", # "/formation_create_new_version",
sco_formations.formation_create_new_version, # sco_formations.formation_create_new_version,
Permission.ScoChangeFormation, # Permission.ScoChangeFormation,
) # )
# --- UE # --- UE
sco_publish( sco_publish(
@ -838,7 +838,7 @@ def formsemestre_flip_lock(formsemestre_id, dialog_confirmed=False):
msg = "verrouillage" if formsemestre.etat else "déverrouillage" msg = "verrouillage" if formsemestre.etat else "déverrouillage"
return scu.confirm_dialog( return scu.confirm_dialog(
f"<h2>Confirmer le {msg} du semestre ?</h2>", f"<h2>Confirmer le {msg} du semestre ?</h2>",
helpmsg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées. help_msg="""Les notes d'un semestre verrouillé ne peuvent plus être modifiées.
Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment Un semestre verrouillé peut cependant être déverrouillé facilement à tout moment
(par son responsable ou un administrateur). (par son responsable ou un administrateur).
<br> <br>

View File

@ -105,7 +105,9 @@ 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(formsemestre.id) ans = sco_formsemestre_edit.formsemestre_associate_new_version(
formsemestre.formation_id, formsemestre.id
)
ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id) ans = sco_formsemestre_edit.formsemestre_delete(formsemestre.id)
# ----- MENU INSCRIPTIONS # ----- MENU INSCRIPTIONS