forked from ScoDoc/ScoDoc
547 lines
20 KiB
Python
547 lines
20 KiB
Python
# -*- mode: python -*-
|
|
# -*- 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
|
|
#
|
|
##############################################################################
|
|
|
|
"""Saisie et gestion des semestres extérieurs à ScoDoc dans un parcours.
|
|
|
|
On va créer/gérer des semestres de la même formation que le semestre ScoDoc
|
|
où est inscrit l'étudiant, leur attribuer la modalité 'EXT'.
|
|
Ces semestres n'auront qu'un seul inscrit !
|
|
"""
|
|
import time
|
|
|
|
import flask
|
|
from flask import flash, g, render_template, request, url_for
|
|
from flask_login import current_user
|
|
|
|
from app.comp import res_sem
|
|
from app.comp.res_compat import NotesTableCompat
|
|
from app.models import (
|
|
Formation,
|
|
FormSemestre,
|
|
FormSemestreUECoef,
|
|
Identite,
|
|
ScolarFormSemestreValidation,
|
|
UniteEns,
|
|
)
|
|
import app.scodoc.sco_utils as scu
|
|
from app import log
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
|
from app.scodoc import sco_formsemestre
|
|
from app.scodoc import sco_formsemestre_inscriptions
|
|
from app.scodoc import sco_formsemestre_validation
|
|
from app.scodoc.codes_cursus import UE_SPORT
|
|
|
|
|
|
def formsemestre_ext_create(etud: Identite | None, sem_params: dict) -> FormSemestre:
|
|
"""Crée un formsemestre exterieur et y inscrit l'étudiant.
|
|
sem_params: dict nécessaire à la création du formsemestre
|
|
"""
|
|
# Check args
|
|
_ = Formation.query.get_or_404(sem_params["formation_id"])
|
|
|
|
# Create formsemestre
|
|
sem_params["modalite"] = "EXT"
|
|
sem_params["etapes"] = None
|
|
sem_params["responsables"] = [current_user.id]
|
|
formsemestre = FormSemestre.create_formsemestre(sem_params, silent=True)
|
|
# nota: le semestre est créé vide: pas de modules
|
|
|
|
# Inscription au semestre
|
|
if etud:
|
|
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
|
|
formsemestre.id,
|
|
etud.id,
|
|
method="formsemestre_ext_create",
|
|
)
|
|
return formsemestre
|
|
|
|
|
|
def formsemestre_ext_create_form(etudid, formsemestre_id):
|
|
"""Formulaire création/inscription à un semestre extérieur"""
|
|
etud = Identite.get_etud(etudid)
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
H = [
|
|
f"""<h2>Enregistrement d'une inscription antérieure dans un autre
|
|
établissement</h2>
|
|
<p class="help">
|
|
Cette opération crée un semestre extérieur ("ancien") de la même
|
|
formation que le semestre courant, et y inscrit juste cet étudiant.
|
|
La décision de jury peut ensuite y être saisie.
|
|
</p>
|
|
<p class="help">
|
|
Notez que si un semestre extérieur similaire a déjà été créé pour un autre
|
|
étudiant, il est préférable d'utiliser la fonction
|
|
"<a href="{ url_for('notes.formsemestre_inscription_with_modules_form',
|
|
scodoc_dept=g.scodoc_dept, etudid=etud.id, only_ext=1) }">
|
|
inscrire à un autre semestre</a>"
|
|
</p>
|
|
<h3><a href="{ url_for('scolar.fiche_etud',
|
|
scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
|
}" class="stdlink">Étudiant {etud.nomprenom}</a></h3>
|
|
""",
|
|
]
|
|
# Ne propose que des semestres de semestre_id strictement inférieur
|
|
# au semestre courant
|
|
# et seulement si pas inscrit au même semestre_id d'un semestre ordinaire ScoDoc.
|
|
# Les autres situations (eg redoublements en changeant d'établissement)
|
|
# doivent être gérées par les validations de semestres "antérieurs"
|
|
insem = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
|
args={"etudid": etud.id, "etat": scu.INSCRIT}
|
|
)
|
|
semlist = [sco_formsemestre.get_formsemestre(i["formsemestre_id"]) for i in insem]
|
|
existing_semestre_ids = {s["semestre_id"] for s in semlist}
|
|
min_semestre_id = 1
|
|
max_semestre_id = formsemestre.semestre_id
|
|
semestre_ids = set(range(min_semestre_id, max_semestre_id)) - existing_semestre_ids
|
|
H.append(
|
|
f"""<p>L'étudiant est déjà inscrit dans des semestres ScoDoc de rangs:
|
|
{ sorted(list(existing_semestre_ids)) }
|
|
</p>
|
|
"""
|
|
)
|
|
if not semestre_ids:
|
|
H.append(
|
|
f"""<p class="warning">pas de semestres extérieurs possibles
|
|
(indices entre {min_semestre_id} et {max_semestre_id}, semestre courant.)
|
|
</p>"""
|
|
)
|
|
return render_template("sco_page.j2", content="\n".join(H))
|
|
# Formulaire
|
|
semestre_ids_list = sorted(semestre_ids)
|
|
semestre_ids_labels = [f"S{x}" for x in semestre_ids_list]
|
|
descr = [
|
|
("formsemestre_id", {"input_type": "hidden"}),
|
|
("etudid", {"input_type": "hidden"}),
|
|
(
|
|
"semestre_id",
|
|
{
|
|
"input_type": "menu",
|
|
"title": "Indice du semestre dans le cursus",
|
|
"allowed_values": semestre_ids_list,
|
|
"labels": semestre_ids_labels,
|
|
},
|
|
),
|
|
(
|
|
"titre",
|
|
{
|
|
"size": 40,
|
|
"title": "Nom de ce semestre extérieur",
|
|
"explanation": """par exemple: établissement.
|
|
N'indiquez pas les dates, ni le semestre, ni la modalité dans
|
|
le titre: ils seront automatiquement ajoutés""",
|
|
},
|
|
),
|
|
(
|
|
"date_debut",
|
|
{
|
|
"title": "Date de début", # j/m/a
|
|
"input_type": "datedmy",
|
|
"explanation": "j/m/a (peut être approximatif)",
|
|
"size": 9,
|
|
"allow_null": False,
|
|
},
|
|
),
|
|
(
|
|
"date_fin",
|
|
{
|
|
"title": "Date de fin", # j/m/a
|
|
"input_type": "datedmy",
|
|
"explanation": "j/m/a (peut être approximatif)",
|
|
"size": 9,
|
|
"allow_null": False,
|
|
},
|
|
),
|
|
(
|
|
"elt_help_ue",
|
|
{
|
|
"title": """Les notes et coefficients des UE
|
|
capitalisées seront saisis ensuite""",
|
|
"input_type": "separator",
|
|
},
|
|
),
|
|
]
|
|
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
descr,
|
|
cancelbutton="Annuler",
|
|
method="post",
|
|
submitlabel="Créer semestre extérieur et y inscrire l'étudiant",
|
|
cssclass="inscription",
|
|
name="tf",
|
|
)
|
|
if tf[0] == 0:
|
|
H.append(
|
|
"""<p>Ce formulaire sert à enregistrer un semestre antérieur dans
|
|
la formation effectué dans un autre établissement.
|
|
</p>"""
|
|
)
|
|
return render_template("sco_page.j2", content="\n".join(H) + "\n" + tf[1])
|
|
if tf[0] == -1:
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
etudid=etud.id,
|
|
)
|
|
)
|
|
|
|
# Le semestre extérieur est créé dans la même formation que le semestre courant
|
|
tf[2]["formation_id"] = formsemestre.formation_id
|
|
formsemestre_ext_create(etud, tf[2])
|
|
return flask.redirect(
|
|
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
|
)
|
|
|
|
|
|
def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
|
|
"""Edition des validations d'UE et de semestre (jury)
|
|
pour un semestre extérieur.
|
|
On peut saisir pour chaque UE du programme de formation
|
|
sa validation, son code jury, sa note, son coefficient
|
|
(sauf en BUT où le coef. des UE est toujours égal aux ECTS).
|
|
|
|
La moyenne générale indicative du semestre est calculée et affichée,
|
|
mais pas enregistrée.
|
|
"""
|
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
|
etud = Identite.get_etud(etudid)
|
|
ues = formsemestre.formation.ues.filter(UniteEns.type != UE_SPORT).order_by(
|
|
UniteEns.semestre_idx, UniteEns.numero
|
|
)
|
|
if formsemestre.formation.is_apc():
|
|
ues = ues.filter_by(semestre_idx=formsemestre.semestre_id)
|
|
descr = _ue_form_description(formsemestre, etud, ues, scu.get_request_args())
|
|
initvalues = {}
|
|
if request.method == "GET":
|
|
for ue in ues:
|
|
validation = ScolarFormSemestreValidation.query.filter_by(
|
|
ue_id=ue.id, etudid=etud.id, formsemestre_id=formsemestre.id
|
|
).first()
|
|
initvalues[f"note_{ue.id}"] = validation.moy_ue if validation else ""
|
|
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
descr,
|
|
submitlabel="Enregistrer ces validations",
|
|
cancelbutton="Annuler",
|
|
initvalues=initvalues,
|
|
cssclass=(
|
|
"tf_ext_edit_ue_validations ext_apc"
|
|
if formsemestre.formation.is_apc()
|
|
else "tf_ext_edit_ue_validations"
|
|
),
|
|
# En APC, stocke les coefficients pour l'affichage de la moyenne en direct
|
|
form_attrs=(
|
|
f"""data-ue_coefs='[{', '.join(str(ue.ects or 0) for ue in ues)}]'"""
|
|
if formsemestre.formation.is_apc()
|
|
else ""
|
|
),
|
|
)
|
|
if tf[0] == -1:
|
|
flash("annulation")
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
etudid=etudid,
|
|
)
|
|
)
|
|
else:
|
|
H = _make_page(etud, formsemestre, tf)
|
|
if tf[0] == 0: # premier affichage
|
|
return render_template(
|
|
"sco_page.j2",
|
|
title="Validation des UE d'un semestre extérieur",
|
|
javascripts=["js/formsemestre_ext_edit_ue_validations.js"],
|
|
content="\n".join(H),
|
|
)
|
|
else: # soumission
|
|
# simule erreur
|
|
ok, message = _check_values(formsemestre, ues, tf[2])
|
|
if not ok:
|
|
H = _make_page(etud, formsemestre, tf, message=message)
|
|
return render_template(
|
|
"sco_page.j2",
|
|
title="Validation des UE d'un semestre extérieur",
|
|
javascripts=["js/formsemestre_ext_edit_ue_validations.js"],
|
|
content="\n".join(H),
|
|
)
|
|
else:
|
|
# Submit
|
|
_record_ue_validations_and_coefs(formsemestre, etud, ues, tf[2])
|
|
return flask.redirect(
|
|
url_for(
|
|
"notes.formsemestre_bulletinetud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
etudid=etudid,
|
|
)
|
|
)
|
|
|
|
|
|
def _make_page(etud: Identite, formsemestre: FormSemestre, tf, message="") -> list[str]:
|
|
"""html formulaire saisie"""
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
moy_gen = nt.get_etud_moy_gen(etud.id)
|
|
H = [
|
|
tf_error_message(message),
|
|
f"""<p><b>{etud.nomprenom}</b> est inscrit{etud.e} à ce semestre extérieur.</p>
|
|
<p>Voici ses UE enregistrées avec leur notes
|
|
{ "et coefficients" if not formsemestre.formation.is_apc()
|
|
else " (en BUT, les coefficients sont égaux aux ECTS)"}.
|
|
</p>
|
|
""",
|
|
f"""<p>La moyenne de ce semestre serait:
|
|
<span class="ext_sem_moy"><span class="ext_sem_moy_val">{moy_gen}</span> / 20</span>
|
|
</p>
|
|
""",
|
|
'<div id="formsemestre_ext_edit_ue_validations">',
|
|
tf[1],
|
|
"</div>",
|
|
f"""<div>
|
|
<a class="stdlink"
|
|
href="{url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre.id, etudid=etud.id
|
|
)}">retour au bulletin de notes</a>
|
|
</div>
|
|
""",
|
|
]
|
|
return H
|
|
|
|
|
|
_UE_VALID_CODES = {
|
|
None: "Non inscrit",
|
|
"ADM": "Capitalisée (ADM)",
|
|
# "CMP": "Acquise (car semestre validé)",
|
|
}
|
|
|
|
|
|
def _ue_form_description(
|
|
formsemestre: FormSemestre, etud: Identite, ues: list[UniteEns], values
|
|
):
|
|
"""Description du formulaire de saisie des UE / validations
|
|
Pour chaque UE, on peut saisir: son code jury, sa note, son coefficient.
|
|
"""
|
|
descr = [
|
|
(
|
|
"head_sep",
|
|
{
|
|
"input_type": "separator",
|
|
"template": """<tr %(item_dom_attr)s><th>UE</th>
|
|
<th>Code jury</th><th>Note/20</th>
|
|
"""
|
|
+ (
|
|
"""<th>Coefficient UE</th>"""
|
|
if not formsemestre.formation.is_apc()
|
|
else ""
|
|
)
|
|
+ "</tr>",
|
|
},
|
|
),
|
|
("formsemestre_id", {"input_type": "hidden"}),
|
|
("etudid", {"input_type": "hidden"}),
|
|
]
|
|
for ue in ues:
|
|
# Menu pour code validation UE:
|
|
# Ne propose que ADM, CMP et "Non inscrit"
|
|
select_name = f"valid_{ue.id}"
|
|
menu_code_ue = f"""<select class="ueext_valid_select" name="{select_name}">"""
|
|
cur_code_value = values.get("valid_{ue.id}", False)
|
|
for code, explanation in _UE_VALID_CODES.items():
|
|
if cur_code_value is False: # pas dans le form, cherche en base
|
|
validation = ScolarFormSemestreValidation.query.filter_by(
|
|
ue_id=ue.id, etudid=etud.id, formsemestre_id=formsemestre.id
|
|
).first()
|
|
cur_code_value = validation.code if validation else None
|
|
if str(cur_code_value) == str(code):
|
|
selected = "selected"
|
|
else:
|
|
selected = ""
|
|
# code jury:
|
|
menu_code_ue += (
|
|
f"""<option value="{code}" {selected}>{explanation}</option>"""
|
|
)
|
|
if cur_code_value is None:
|
|
coef_disabled = 'disabled="1"'
|
|
else:
|
|
coef_disabled = ""
|
|
menu_code_ue += "</select>"
|
|
if formsemestre.formation.is_apc():
|
|
coef_disabled = 'disabled="1"'
|
|
cur_coef_value = ue.ects or 0
|
|
coef_input_class = "ext_coef_disabled"
|
|
else:
|
|
cur_coef_value = values.get(f"coef_{ue.id}", False)
|
|
coef_input_class = ""
|
|
if cur_coef_value is False: # pas dans le form, cherche en base
|
|
ue_coef: FormSemestreUECoef = FormSemestreUECoef.query.filter_by(
|
|
formsemestre_id=formsemestre.id, ue_id=ue.id
|
|
).first()
|
|
cur_coef_value = (ue_coef.coefficient if ue_coef else "") or ""
|
|
itemtemplate = (
|
|
f"""
|
|
<tr>
|
|
<td class="tf-fieldlabel">%(label)s</td>
|
|
<td>{ menu_code_ue }</td>
|
|
<td class="tf-field tf_field_note">%(elem)s</td>
|
|
"""
|
|
+ (
|
|
f"""<td class="tf-field tf_field_coef">
|
|
<input type="text" size="4" name="coef_{ue.id}"
|
|
class="{coef_input_class}"
|
|
value="{cur_coef_value}" {coef_disabled}></input>
|
|
</td>"""
|
|
if not formsemestre.formation.is_apc()
|
|
else ""
|
|
)
|
|
+ """</tr>"""
|
|
)
|
|
|
|
descr.append(
|
|
(
|
|
f"note_{ue.id}",
|
|
{
|
|
"input_type": "text",
|
|
"size": 4,
|
|
"template": itemtemplate,
|
|
"title": (
|
|
"<tt>"
|
|
+ (
|
|
f"S{ue.semestre_idx} "
|
|
if ue.semestre_idx is not None
|
|
else ""
|
|
)
|
|
+ f"<b>{ue.acronyme}</b></tt> {ue.titre or ''}"
|
|
+ f" ({ue.ects} ECTS)"
|
|
if ue.ects is not None
|
|
else ""
|
|
),
|
|
"attributes": [coef_disabled],
|
|
},
|
|
)
|
|
)
|
|
return descr
|
|
|
|
|
|
def _check_values(formsemestre: FormSemestre, ue_list: list[UniteEns], values):
|
|
"""Check that form values are ok
|
|
for each UE:
|
|
code != None => note and coef
|
|
note or coef => code != None
|
|
note float in [0, 20]
|
|
note => coef
|
|
coef float >= 0
|
|
"""
|
|
for ue in ue_list:
|
|
pu = f" pour UE {ue.acronyme}"
|
|
code = values.get(f"valid_{ue.id}", False)
|
|
if code == "None":
|
|
code = None
|
|
note = values.get(f"note_{ue.id}", False)
|
|
try:
|
|
note = _convert_field_to_float(note)
|
|
except ValueError:
|
|
return False, "note invalide" + pu
|
|
|
|
if code is not False:
|
|
if code not in _UE_VALID_CODES:
|
|
return False, "code invalide" + pu
|
|
if code is not None:
|
|
if note is False or note == "":
|
|
return False, "note manquante" + pu
|
|
coef = values.get(f"coef_{ue.id}", False)
|
|
try:
|
|
coef = _convert_field_to_float(coef)
|
|
except ValueError:
|
|
return False, "coefficient invalide" + pu
|
|
if note is not False and note != "":
|
|
if code is None:
|
|
return (
|
|
False,
|
|
f"""code jury incohérent (code {code}, note {note}) {pu}
|
|
(supprimer note)""",
|
|
)
|
|
if note < 0 or note > 20:
|
|
return False, "valeur note invalide" + pu
|
|
if not isinstance(coef, float) and not formsemestre.formation.is_apc():
|
|
return False, f"coefficient manquant pour note {note} {pu}"
|
|
|
|
# Vérifie valeur coef seulement pour formations classiques:
|
|
if not formsemestre.formation.is_apc():
|
|
if coef is not False and coef != "":
|
|
if coef < 0:
|
|
return False, "valeur coefficient invalide" + pu
|
|
|
|
return True, "ok"
|
|
|
|
|
|
def _convert_field_to_float(val):
|
|
"""val may be empty, False (left unchanged), or a float. Raise exception ValueError"""
|
|
if val is not False:
|
|
val = val.strip()
|
|
if val:
|
|
val = float(val)
|
|
return val
|
|
|
|
|
|
def _record_ue_validations_and_coefs(
|
|
formsemestre: FormSemestre, etud: Identite, ues: list[UniteEns], values
|
|
):
|
|
"""Enregistre en base les validations
|
|
En APC, le coef est toujours NULL
|
|
"""
|
|
for ue in ues:
|
|
code = values.get(f"valid_{ue.id}", False)
|
|
if code == "None":
|
|
code = None
|
|
note = values.get(f"note_{ue.id}", False)
|
|
note = _convert_field_to_float(note)
|
|
coef = values.get(f"coef_{ue.id}", False)
|
|
coef = _convert_field_to_float(coef)
|
|
if coef == "" or coef is False:
|
|
coef = None
|
|
now_dmy = time.strftime(scu.DATE_FMT)
|
|
log(
|
|
f"""_record_ue_validations_and_coefs: {
|
|
formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}"""
|
|
)
|
|
assert code is None or (note) # si code validant, il faut une note
|
|
sco_formsemestre_validation.do_formsemestre_validate_previous_ue(
|
|
formsemestre,
|
|
etud.id,
|
|
ue.id,
|
|
note,
|
|
now_dmy,
|
|
code=code,
|
|
ue_coefficient=coef,
|
|
)
|