diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 52d5cd902a..6df7635931 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -228,7 +228,7 @@ class ResultatsSemestre(ResultatsCache): UE capitalisées. """ # Supposant qu'il y a peu d'UE capitalisées, - # on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée. + # on recalcule les moyennes gen des etuds ayant des UE capitalisée. if not self.validations: self.validations = res_sem.load_formsemestre_validations(self.formsemestre) ue_capitalisees = self.validations.ue_capitalisees @@ -396,10 +396,10 @@ class ResultatsSemestre(ResultatsCache): def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float: "Détermine le coefficient de l'UE pour cet étudiant." - # calcul différent en classqiue et BUT + # calcul différent en classique et BUT raise NotImplementedError() - def get_etud_ue_cap_coef(self, etudid, ue, ue_cap): + def get_etud_ue_cap_coef(self, etudid, ue, ue_cap): # UNUSED in ScoDoc 9 """Calcule le coefficient d'une UE capitalisée, pour cet étudiant, injectée dans le semestre courant. diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index 9dcd9edac6..5227091510 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -28,6 +28,7 @@ def TrivialFormulator( submitlabel="OK", name=None, formid="tf", + form_attrs="", cssclass="", cancelbutton=None, submitbutton=True, @@ -100,7 +101,8 @@ def TrivialFormulator( submitlabel, name, formid, - cssclass, + form_attrs=form_attrs, + cssclass=cssclass, cancelbutton=cancelbutton, submitbutton=submitbutton, submitbuttonattributes=submitbuttonattributes, @@ -132,6 +134,7 @@ class TF(object): submitlabel="OK", name=None, formid="tf", + form_attrs="", cssclass="", cancelbutton=None, submitbutton=True, @@ -154,6 +157,7 @@ class TF(object): else: self.name = formid # 'tf' self.formid = formid + self.form_attrs = form_attrs self.cssclass = cssclass self.cancelbutton = cancelbutton self.submitbutton = submitbutton @@ -408,8 +412,16 @@ class TF(object): klass = "" name = self.name R.append( - '
' - % (self.form_url, self.method, self.formid, enctype, name, klass) + '' + % ( + self.form_url, + self.method, + self.formid, + enctype, + name, + klass, + self.form_attrs, + ) ) R.append('' % self.formid) if self.top_buttons: diff --git a/app/scodoc/sco_formsemestre_exterieurs.py b/app/scodoc/sco_formsemestre_exterieurs.py index fa1bae279d..c8c3ff4ab1 100644 --- a/app/scodoc/sco_formsemestre_exterieurs.py +++ b/app/scodoc/sco_formsemestre_exterieurs.py @@ -39,20 +39,23 @@ from flask_login import current_user from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre +from app.models import ( + FormSemestre, + FormSemestreUECoef, + Identite, + ScolarFormSemestreValidation, + UniteEns, +) import app.scodoc.sco_utils as scu -import app.scodoc.notesdb as ndb from app import log from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc import html_sco_header -from app.scodoc import sco_cache -from app.scodoc import sco_edit_ue from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_cursus_dut from app.scodoc import sco_etud +from app.scodoc.sco_codes_parcours import UE_SPORT def formsemestre_ext_create(etudid, sem_params): @@ -84,7 +87,7 @@ def formsemestre_ext_create(etudid, sem_params): def formsemestre_ext_create_form(etudid, formsemestre_id): """Formulaire création/inscription à un semestre extérieur""" - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] + etud = Identite.query.get_or_404(etudid) H = [ html_sco_header.sco_header(), f"""

Enregistrement d'une inscription antérieure dans un autre @@ -103,7 +106,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):

Étudiant {etud["nomprenom"]}

+ }" class="stdlink">Étudiant {etud.nomprenom} """, ] F = html_sco_header.sco_footer() @@ -228,66 +231,82 @@ 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. + 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 du semestre est calculée et affichée, + La moyenne générale indicative du semestre est calculée et affichée, mais pas enregistrée. """ - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - ues = _list_ue_with_coef_and_validations(sem, etudid) - descr = _ue_form_description(ues, scu.get_request_args()) + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + etud = Identite.query.get_or_404(etudid) + ues = formsemestre.formation.ues.filter(UniteEns.type != UE_SPORT).order_by( + UniteEns.semestre_idx, UniteEns.numero + ) + descr = _ue_form_description(formsemestre, etud, ues, scu.get_request_args()) + initvalues = {} if request.method == "GET": - initvalues = { - "note_" + str(ue["ue_id"]): ue["validation"].get("moy_ue", "") for ue in ues - } - else: - initvalues = {} + 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, - cssclass="tf_ext_edit_ue_validations", 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: return "

annulation

" else: - H = _make_page(etud, sem, tf) + H = _make_page(etud, formsemestre, tf) if tf[0] == 0: # premier affichage return "\n".join(H) else: # soumission # simule erreur - ok, message = _check_values(ues, tf[2]) + ok, message = _check_values(formsemestre, ues, tf[2]) if not ok: - H = _make_page(etud, sem, tf, message=message) + H = _make_page(etud, formsemestre, tf, message=message) return "\n".join(H) else: # Submit - _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, tf[2]) + _record_ue_validations_and_coefs(formsemestre, etud, ues, tf[2]) return flask.redirect( - "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" - % (formsemestre_id, etudid) + url_for( + "notes.formsemestre_bulletinetud", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) ) -def _make_page(etud: dict, sem, tf, message="") -> list: - formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) +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["etudid"]) + moy_gen = nt.get_etud_moy_gen(etud.id) H = [ html_sco_header.sco_header( page_title="Validation des UE d'un semestre extérieur", javascripts=["js/formsemestre_ext_edit_ue_validations.js"], ), tf_error_message(message), - """

%(nomprenom)s est inscrit%(ne)s à ce semestre extérieur.

-

Voici les UE entregistrées avec leur notes et coefficients. + f"""

{etud.nomprenom} est inscrit{etud.e} à ce semestre extérieur.

+

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)"}.

- """ - % etud, + """, f"""

La moyenne de ce semestre serait: {moy_gen} / 20

@@ -298,7 +317,7 @@ def _make_page(etud: dict, sem, tf, message="") -> list: f"""
retour au bulletin de notes
""", @@ -314,7 +333,9 @@ _UE_VALID_CODES = { } -def _ue_form_description(ues, values): +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. """ @@ -324,8 +345,14 @@ def _ue_form_description(ues, values): { "input_type": "separator", "template": """UE - Code juryNote/20Coefficient UE - """, + Code juryNote/20 + """ + + ( + """Coefficient UE""" + if not formsemestre.formation.is_apc() + else "" + ) + + "", }, ), ("formsemestre_id", {"input_type": "hidden"}), @@ -334,59 +361,80 @@ def _ue_form_description(ues, values): for ue in ues: # Menu pour code validation UE: # Ne propose que ADM, CMP et "Non inscrit" - select_name = "valid_" + str(ue["ue_id"]) - menu_code_UE = """""" + 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 = "" - menu_code_UE += '' % ( - code, - selected, - _UE_VALID_CODES[code], + # code jury: + menu_code_ue += ( + f"""""" ) - if cur_value is None: - disabled = 'disabled="1"' + if cur_code_value is None: + coef_disabled = 'disabled="1"' else: - disabled = "" - menu_code_UE += "" - cur_value = values.get("coef_" + str(ue["ue_id"]), False) - if cur_value is False: # pas dans le form, cherche en base - cur_value = ue["uecoef"].get("coefficient", "") + coef_disabled = "" + menu_code_ue += "" + 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 = ( - """%(label)s""" - + "" - + menu_code_UE - + "" # code jury - + '%(elem)s' # note - + """ - - """ - % (ue["ue_id"], cur_value, disabled) - + "" + f""" + + %(label)s + { menu_code_ue } + %(elem)s + """ + + ( + f""" + + """ + if not formsemestre.formation.is_apc() + else "" + ) + + """""" ) + descr.append( ( - "note_" + str(ue["ue_id"]), + f"note_{ue.id}", { "input_type": "text", "size": 4, "template": itemtemplate, - "title": "%(acronyme)s %(titre)s" % ue, - "attributes": [disabled], + "title": "" + + (f"S{ue.semestre_idx} " if ue.semestre_idx is not None else "") + + f"{ue.acronyme} {ue.titre}" + + f" ({ue.ects} ECTS)" + if ue.ects is not None + else "", + "attributes": [coef_disabled], }, ) ) return descr -def _check_values(ue_list, values): +def _check_values(formsemestre: FormSemestre, ue_list, values): """Check that form values are ok for each UE: code != None => note and coef @@ -396,107 +444,82 @@ def _check_values(ue_list, values): coef float >= 0 """ for ue in ue_list: - pu = " pour UE %s" % ue["acronyme"] - code = values.get("valid_" + str(ue["ue_id"]), False) + pu = f" pour UE {ue.acronyme}" + code = values.get(f"valid_{ue.id}", False) if code == "None": code = None - note = values.get("note_" + str(ue["ue_id"]), False) + note = values.get(f"note_{ue.id}", False) try: note = _convert_field_to_float(note) except ValueError: return False, "note invalide" + pu - coef = values.get("coef_" + str(ue["ue_id"]), False) + + 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 code != False: - if code not in _UE_VALID_CODES: - return False, "code invalide" + pu - if code != None: - if note is False or note == "": - return False, "note manquante" + pu - if note != False and note != "": - if code == None: + if note is not False and note != "": + if code is None: return ( False, - "code jury incohérent (code %s, note %s)" % (code, note) - + pu - + " (supprimer note et coef)", + 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): - return False, "coefficient manquant pour note %s" % note + pu - if coef != False and coef != "": - if coef < 0: - return False, "valeur coefficient 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): - """value may be empty, False, or a float. Raise exception""" - if val != False: + """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 _list_ue_with_coef_and_validations(sem, etudid): - """Liste des UE de la même formation que sem, - avec leurs coefs d'UE capitalisée (si déjà saisi) - et leur validation pour cet étudiant. +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 """ - cnx = ndb.GetDBConnexion() - formsemestre_id = sem["formsemestre_id"] - ues = sco_edit_ue.ue_list({"formation_id": sem["formation_id"]}) for ue in ues: - # add coefficient - uecoef = sco_formsemestre.formsemestre_uecoef_list( - cnx, args={"formsemestre_id": formsemestre_id, "ue_id": ue["ue_id"]} - ) - if uecoef: - ue["uecoef"] = uecoef[0] - else: - ue["uecoef"] = {} - # add validation - validation = sco_cursus_dut.scolar_formsemestre_validation_list( - cnx, - args={ - "formsemestre_id": formsemestre_id, - "etudid": etudid, - "ue_id": ue["ue_id"], - }, - ) - if validation: - ue["validation"] = validation[0] - else: - ue["validation"] = {} - return ues - - -def _record_ue_validations_and_coefs(formsemestre_id, etudid, ues, values): - for ue in ues: - code = values.get("valid_" + str(ue["ue_id"]), False) + code = values.get(f"valid_{ue.id}", False) if code == "None": code = None - note = values.get("note_" + str(ue["ue_id"]), False) + note = values.get(f"note_{ue.id}", False) note = _convert_field_to_float(note) - coef = values.get("coef_" + str(ue["ue_id"]), False) + coef = values.get(f"coef_{ue.id}", False) coef = _convert_field_to_float(coef) - if coef == "" or coef == False: + if coef == "" or coef is False: coef = None now_dmy = time.strftime("%d/%m/%Y") log( - "_record_ue_validations_and_coefs: %s etudid=%s ue_id=%s moy_ue=%s ue_coef=%s" - % (formsemestre_id, etudid, ue["ue_id"], note, repr(coef)) + f"_record_ue_validations_and_coefs: {formsemestre.id} etudid={etud.id} ue_id={ue.id} moy_ue={note} ue_coef={coef}" ) - assert code == None or (note) # si code validant, il faut une note + assert code is None or (note) # si code validant, il faut une note sco_formsemestre_validation.do_formsemestre_validate_previous_ue( - formsemestre_id, - etudid, - ue["ue_id"], + formsemestre.id, + etud.id, + ue.id, note, now_dmy, code=code, diff --git a/app/static/js/formsemestre_ext_edit_ue_validations.js b/app/static/js/formsemestre_ext_edit_ue_validations.js index b0e08d32d0..ed9576b528 100644 --- a/app/static/js/formsemestre_ext_edit_ue_validations.js +++ b/app/static/js/formsemestre_ext_edit_ue_validations.js @@ -2,66 +2,80 @@ function compute_moyenne() { - var notes = $(".tf_field_note input").map( - function() { return parseFloat($(this).val()); } - ).get(); - var coefs = $(".tf_field_coef input").map( - function() { return parseFloat($(this).val()); } + var notes = $(".tf_field_note input").map( + function () { return parseFloat($(this).val()); } ).get(); + // les coefs sont donnes (ECTS en BUT) + let coefs = $("form.tf_ext_edit_ue_validations").data("ue_coefs"); + // ou saisis (formations classiques) + if (coefs == 'undefined') { + coefs = $(".tf_field_coef input").map( + function () { return parseFloat($(this).val()); } + ).get(); + } var N = notes.length; var dp = 0.; var sum_coefs = 0.; - for (var i=0; i < N; i++) { + for (var i = 0; i < N; i++) { if (!(isNaN(notes[i]) || isNaN(coefs[i]))) { dp += notes[i] * coefs[i]; sum_coefs += coefs[i]; } } - return dp / sum_coefs; + let moy = dp / sum_coefs; + if (isNaN(moy)) { + moy = "-"; + } + if (typeof moy == "number") { + moy = moy.toFixed(2); + } + return moy; } // Callback select menu (UE code) function enable_disable_fields_cb() { enable_disable_fields(this); } -function enable_disable_fields(select_elt) { +function enable_disable_fields(select_elt) { // input fields controled by this menu - var input_fields = $(select_elt).parent().parent().find('input'); + var input_fields = $(select_elt).parent().parent().find('input:not(.ext_coef_disabled)'); var disabled = false; if ($(select_elt).val() === "None") { disabled = true; } - console.log('disabled=', disabled); - input_fields.each( function () { - var old_state = this.disabled; - console.log("old_state=", old_state) - if (old_state == disabled) { - return; /* state unchanged */ + input_fields.each(function () { + if (disabled) { + let cur_value = $(this).val(); + $(this).data('saved-value', cur_value); + $(this).val(""); + } else { + let saved_value = $(this).data('saved-value'); + if (typeof saved_value == 'undefined') { + saved_value = ''; + } + if (saved_value) { + $(this).val(saved_value); + } } - var saved_value = $(this).data('saved-value'); - if (typeof saved_value == 'undefined') { - saved_value = ''; - } - var cur_value = $(this).val(); - // swap - $(this).data('saved-value', cur_value); - $(this).val(saved_value); }); input_fields.prop('disabled', disabled); } function setup_text_fields() { $(".ueext_valid_select").each( - function() { + function () { enable_disable_fields(this); } ); } -$().ready(function(){ - $(".tf_ext_edit_ue_validations").change(function (){ - $(".ext_sem_moy_val")[0].innerHTML=compute_moyenne(); +$().ready(function () { + $(".tf_ext_edit_ue_validations").change(function () { + $(".ext_sem_moy_val")[0].innerHTML = compute_moyenne(); }); - $(".ueext_valid_select").change( enable_disable_fields_cb ); + $("form.tf_ext_edit_ue_validations input").blur(function () { + $(".ext_sem_moy_val")[0].innerHTML = compute_moyenne(); + }); + $(".ueext_valid_select").change(enable_disable_fields_cb); setup_text_fields(); });