diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 86a5e4a9a4561868f3af3468152b56e469964239..e01703ad627a5c265da1f4cb781b98a2228e028f 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -350,8 +350,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): ) "vrai si l'année est réussie, tous niveaux validables ou validés par le jury" self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) - "Peut passer si plus de la moitié validables et tous > 8" + "Vrai si plus de la moitié des RCUE validables" self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0) + "Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8" # XXX TODO ajouter condition pour passage en S5 # Enfin calcule les codes des UE: @@ -752,8 +753,9 @@ class DecisionsProposeesAnnee(DecisionsProposees): pour cette année: décisions d'UE, de RCUE, d'année, et autorisations d'inscription émises. Efface même si étudiant DEM ou DEF. + Si à cheval, n'efface que pour le semestre d'origine du deca. """ - if only_one_sem: + if only_one_sem or self.a_cheval: # N'efface que les autorisations venant de ce semestre, # et les validations de ses UEs ScolarAutorisationInscription.delete_autorisation_etud( @@ -906,6 +908,19 @@ class DecisionsProposeesRCUE(DecisionsProposees): or dec_prop_annee.formsemestre_pair.modalite == "EXT" ): self.codes.insert(0, sco_codes.ADM) + # S'il y a une décision enregistrée: si elle est plus favorable que celle que l'on + # proposerait, la place en tête. + # Sinon, la place en seconde place + if self.code_valide and self.code_valide != self.codes[0]: + code_default = self.codes[0] + if self.code_valide in self.codes: + self.codes.remove(self.code_valide) + if sco_codes.BUT_CODES_ORDERED.get( + self.code_valide, 0 + ) > sco_codes.BUT_CODES_ORDERED.get(code_default, 0): + self.codes.insert(0, self.code_valide) + else: + self.codes.insert(1, self.code_valide) def record(self, code: str, no_overwrite=False): """Enregistre le code""" diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index 24224d62946e0ac9fa7d176cdf78648c055b2a71..d08329ea477e70f0c92f2ef43ba6a49e7f40f5a6 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -41,32 +41,21 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: Si pas read_only, menus sélection codes jury. """ H = [] - if deca.code_valide and not read_only: - erase_span = f"""<a href="{ - url_for("notes.formsemestre_jury_but_erase", - scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id, - etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>""" - else: - erase_span = "" H.append("""<div class="but_section_annee">""") - if deca.jury_annuel: - H.append( - f""" - <div> - <b>Décision de jury pour l'année :</b> { - _gen_but_select("code_annee", deca.codes, deca.code_valide, - disabled=True, klass="manual") - } - <span>({'non ' if deca.code_valide is None else ''}enregistrée)</span> - <span>{erase_span}</span> - </div> + H.append( + f""" + <div> + <b>Décision de jury pour l'année :</b> { + _gen_but_select("code_annee", deca.codes, deca.code_valide, + disabled=True, klass="manual") + } + <span>({'non ' if deca.code_valide is None else ''}enregistrée)</span> + </div> """ - ) - div_explanation = f"""<div class="but_explanation">{deca.explanation}</div>""" - else: - H.append("""<div><em>Pas de décision annuelle (sem. impair).</em></div>""") - div_explanation = "" + ) + div_explanation = f"""<div class="but_explanation">{deca.explanation}</div>""" + H.append("""</div>""") formsemestre_1 = deca.formsemestre_impair @@ -245,9 +234,16 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: else "" ) + # Déjà enregistré ? + niveau_rcue_class = "" + if dec_rcue.code_valide is not None and dec_rcue.codes: + if dec_rcue.code_valide == dec_rcue.codes[0]: + niveau_rcue_class = "recorded" + else: + niveau_rcue_class = "recorded_different" + return f""" - <div class="but_niveau_rcue - {'recorded' if dec_rcue.code_valide is not None else ''} + <div class="but_niveau_rcue {niveau_rcue_class} "> <div class="but_note with_scoplement"> <div>{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div> @@ -351,7 +347,7 @@ def jury_but_semestriel( warning = "" H = [ html_sco_header.sco_header( - page_title="Validation BUT", + page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre.id, etudid=etud.id, cssstyles=("css/jury_but.css",), @@ -376,21 +372,21 @@ def jury_but_semestriel( {warning} </div> - <form method="POST"> + <form method="post" id="jury_but"> """, ] if (not read_only) and any([dec.code_valide for dec in decisions_ues.values()]): erase_span = f"""<a href="{ url_for("notes.formsemestre_jury_but_erase", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, - etudid=etud.id, only_one_sem=1)}" class="stdlink">effacer les décisions enregistrées</a>""" + etudid=etud.id, only_one_sem=1) + }" class="stdlink">effacer les décisions enregistrées</a>""" else: erase_span = "Cet étudiant n'a aucune décision enregistrée pour ce semestre." H.append( f""" <div class="but_section_annee"> - <span>{erase_span}</span> </div> <div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div> """ @@ -447,9 +443,10 @@ def jury_but_semestriel( else: H.append("""<div class="help">dernier semestre de la formation.</div>""") H.append( - """ + f""" <div class="but_buttons"> - <input type="submit" value="Enregistrer ces décisions"> + <span><input type="submit" value="Enregistrer ces décisions"></span> + <span>{erase_span}</span> </div> """ ) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index ad191d29b93c54e7f4f94fd17c40ce9c49578dbb..ab820bc38663792d48f9a94b3d548c64009bbd11 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -180,8 +180,9 @@ class RegroupementCoherentUE: return self.query_validations().count() > 0 def est_compensable(self): - """Vrai si ce RCUE est validable par compensation - c'est à dire que sa moyenne est > 10 avec une UE < 10 + """Vrai si ce RCUE est validable (uniquement) par compensation + c'est à dire que sa moyenne est > 10 avec une UE < 10. + Note: si ADM, est_compensable est faux. """ return ( (self.moy_rcue is not None) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 299ccdde88f50d73907434b84a3a8d541e501fcd..7d90edbcb173f1ff15a9d8079f7fb91f47780419 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -205,6 +205,20 @@ BUT_CODES_PASSAGE = { PAS1NCI, ATJ, } +# les codes, du plus "défavorable" à l'étudiant au plus favorable: +# (valeur par défaut 0) +BUT_CODES_ORDERED = { + "NAR": 0, + "DEF": 0, + "AJ": 10, + "ATJ": 20, + "CMP": 50, + "ADC": 50, + "PASD": 50, + "PAS1NCI": 60, + "ADJ": 100, + "ADM": 100, +} def code_semestre_validant(code: str) -> bool: diff --git a/app/static/css/jury_but.css b/app/static/css/jury_but.css index 955aed943a99dc91161ce3990f6d19246e4bab68..f865400a046140695697e7ec7d8263d3b493b555 100644 --- a/app/static/css/jury_but.css +++ b/app/static/css/jury_but.css @@ -22,7 +22,7 @@ margin-left: 32px; display: inline-grid; grid-template-columns: repeat(4, auto); - gap: 4px; + gap: 8px; } .but_annee_caption { @@ -143,8 +143,14 @@ div.but_code { div.but_niveau_ue.recorded, div.but_niveau_rcue.recorded { - border-color: rgb(136, 252, 136); - border-width: 2px; + border-color: rgb(0, 169, 0); + border-width: 3px; +} + +div.but_niveau_ue.recorded_different, +div.but_niveau_rcue.recorded_different { + box-shadow: 0 0 0 3px red; + outline: dashed 3px rgb(0, 169, 0); } div.but_niveau_ue.annee_prec { @@ -160,6 +166,7 @@ div.but_buttons { } div.but_buttons span { + margin-left: 16px; margin-right: 16px; } diff --git a/app/static/js/jury_but.js b/app/static/js/jury_but.js index c90f95d633019ee8f9efd5b570021f3a14e6eba8..54d28a841d7c98a6329e1adced62a812324a3ac6 100644 --- a/app/static/js/jury_but.js +++ b/app/static/js/jury_but.js @@ -63,6 +63,8 @@ $(function () { // ----- Etat du formulaire jury pour éviter sortie sans enregistrer let FORM_STATE = ""; +let IS_SUBMITTING = false; + // Une chaine décrivant l'état du form function get_form_state() { let codes = []; @@ -73,13 +75,19 @@ function get_form_state() { $('document').ready(function () { FORM_STATE = get_form_state(); + document.querySelector("form#jury_but").addEventListener('submit', jury_form_submit); }); function is_modified() { return FORM_STATE != get_form_state(); } + +function jury_form_submit(event) { + IS_SUBMITTING = true; +} + window.addEventListener("beforeunload", function (e) { - if (is_modified()) { + if ((!IS_SUBMITTING) && is_modified()) { var confirmationMessage = 'Changements non enregistrés !'; (e || window.event).returnValue = confirmationMessage; return confirmationMessage; diff --git a/app/views/notes.py b/app/views/notes.py index 48fb28aea38eaf56dc43a5a2188844f046f3f737..06d62445d499f2d26a6f97375e288905f1668db3 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2327,17 +2327,17 @@ def formsemestre_validation_but( # provisoires avec NEXT et PREV try: etudid = int(etudid) - except: + except ValueError: abort(404, "invalid etudid") read_only = not sco_permissions_check.can_validate_sem(formsemestre_id) # --- Navigation - prev = f"""{scu.EMO_PREV_ARROW} <a href="{url_for( + prev_lnk = f"""{scu.EMO_PREV_ARROW} <a href="{url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid="PREV" )}" class="stdlink"">précédent</a> """ - next = f"""<a href="{url_for( + next_lnk = f"""<a href="{url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etudid="NEXT" )}" class="stdlink"">suivant</a> {scu.EMO_NEXT_ARROW} @@ -2345,7 +2345,7 @@ def formsemestre_validation_but( navigation_div = f""" <div class="but_navigation"> <div class="prev"> - {prev} + {prev_lnk} </div> <div class="back_list"> <a href="{url_for( @@ -2354,21 +2354,20 @@ def formsemestre_validation_but( )}" class="stdlink">retour à la liste</a> </div> <div class="next"> - {next} + {next_lnk} </div> </div> """ H = [ html_sco_header.sco_header( - page_title="Validation BUT", + page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre_id, etudid=etudid, cssstyles=("css/jury_but.css",), javascripts=("js/jury_but.js",), ), - f""" - <div class="jury_but"> + """<div class="jury_but"> """, ] @@ -2401,7 +2400,6 @@ def formsemestre_validation_but( deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) if len(deca.rcues_annee) == 0: - # raise ScoValueError("année incomplète: pas de jury BUT annuel possible") return jury_but_view.jury_but_semestriel( formsemestre, etud, read_only, navigation_div=navigation_div ) @@ -2421,7 +2419,7 @@ def formsemestre_validation_but( warning = "" if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): if deca.a_cheval: - warning += f"""<div class="warning">Attention: regroupements RCUE + warning += """<div class="warning">Attention: regroupements RCUE entre années (redoublement).</div>""" else: warning += f"""<div class="warning">Attention: {len(deca.niveaux_competences)} @@ -2454,7 +2452,7 @@ def formsemestre_validation_but( {warning} </div> - <form method="POST"> + <form method="post" id="jury_but"> """ ) @@ -2467,6 +2465,10 @@ def formsemestre_validation_but( Les champs entourés en vert sont enregistrés.</div>""" ) else: + erase_span = f"""<a href="{ + url_for("notes.formsemestre_jury_but_erase", + scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id, + etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>""" H.append( f"""<div class="but_settings"> <input type="checkbox" onchange="enable_manual_codes(this)"> @@ -2477,7 +2479,8 @@ def formsemestre_validation_but( </div> <div class="but_buttons"> - <input type="submit" value="Enregistrer ces décisions"> + <span><input type="submit" value="Enregistrer ces décisions"></span> + <span>{erase_span}</span> </div> """ ) @@ -2790,7 +2793,9 @@ def formsemestre_jury_but_erase( explanation=f"""Les validations d'UE et autorisations de passage du semestre S{formsemestre.semestre_id} seront effacées.""" if only_one_sem - else """Les validations de toutes les UE, RCUE (compétences) et année seront effacées.""", + else """Les validations de toutes les UE, RCUE (compétences) et année seront effacées. + Les décisions de l'année scolaire précédente ne seront pas modifiées. + """, cancel_url=dest_url, )