diff --git a/app/api/jury.py b/app/api/jury.py index 6103d11649a7130373c4a11ea207eea842aaa57e..6f0710770d3333343ddd434e7380a29b75cd69c9 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -114,16 +114,16 @@ def _validation_ue_delete(etudid: int, validation_id: int): # rattachées à un formsemestre) if not g.scodoc_dept: # accès API if not current_user.has_permission(Permission.ScoEtudInscrit): - return json_error(403, "validation_delete: non autorise") + return json_error(403, "opération non autorisée (117)") else: if validation.formsemestre: if ( validation.formsemestre.dept_id != g.scodoc_dept_id ) or not validation.formsemestre.can_edit_jury(): - return json_error(403, "validation_delete: non autorise") + return json_error(403, "opération non autorisée (123)") elif not current_user.has_permission(Permission.ScoEtudInscrit): # Validation non rattachée à un semestre: on doit être chef - return json_error(403, "validation_delete: non autorise") + return json_error(403, "opération non autorisée (126)") log(f"validation_ue_delete: etuid={etudid} {validation}") db.session.delete(validation) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index aedf4cbaf9db321d1567eb74330bb625c18404eb..fcab132bd0fc0d7828faa03fae781468943bee58 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -158,8 +158,16 @@ class ApcValidationAnnee(db.Model): if self.date else "(sans date)" ) + link = ( + self.formsemestre.html_link_status( + label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}", + title=self.formsemestre.titre_annee(), + ) + if self.formsemestre + else "externe/antérieure" + ) return f"""Validation <b>année BUT{self.ordre}</b> émise par - {self.formsemestre.html_link_status() if self.formsemestre else "-"} + {link} : <b>{self.code}</b> {date_str} """ diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index cd296353f6e6da4cff66621e6dc0233b09193b93..64668fa16898538529e449db292d564a6f3e19a1 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -163,12 +163,12 @@ class FormSemestre(db.Model): def __repr__(self): return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>" - def html_link_status(self) -> str: + def html_link_status(self, label=None, title=None) -> str: "html link to status page" return f"""<a class="stdlink" href="{ url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=self.id,) - }">{self.titre_mois()}</a> + }" title="{title or ''}">{label or self.titre_mois()}</a> """ @classmethod diff --git a/app/models/validations.py b/app/models/validations.py index 9e2cf5e27d58bc96e73d42800cc8c57cac261aaa..91f17f605ab429e7e0b0aa6cd35d509bf39f6716 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -94,6 +94,14 @@ class ScolarFormSemestreValidation(db.Model): if self.moy_ue is not None else "" ) + link = ( + self.formsemestre.html_link_status( + label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}", + title=self.formsemestre.titre_annee(), + ) + if self.formsemestre + else "externe/antérieure" + ) return f"""Validation {'<span class="redboldtext">externe</span>' if self.is_external else ""} de l'UE <b>{self.ue.acronyme}</b> @@ -101,9 +109,7 @@ class ScolarFormSemestreValidation(db.Model): + ", ".join([p.code for p in self.ue.parcours])) + "</span>" if self.ue.parcours else ""} - de {self.ue.formation.acronyme} - {("émise par " + self.formsemestre.html_link_status()) - if self.formsemestre else "externe/antérieure"} + {("émise par " + link)} : <b>{self.code}</b>{moyenne} le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")} """ @@ -149,10 +155,16 @@ class ScolarAutorisationInscription(db.Model): def html(self) -> str: "Affichage html" + link = ( + self.origin_formsemestre.html_link_status( + label=f"{self.origin_formsemestre.titre_formation(with_sem_idx=1)}", + title=self.origin_formsemestre.titre_annee(), + ) + if self.origin_formsemestre + else "externe/antérieure" + ) return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par - {self.origin_formsemestre.html_link_status() - if self.origin_formsemestre - else "-"} + {link} le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")} """ diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py index 85d14b957b48ae55f7eff0fb5c1108d20bafaa37..dff4ead7b3d22f0bbc180e26dd6dc3eb634c4f37 100644 --- a/app/scodoc/codes_cursus.py +++ b/app/scodoc/codes_cursus.py @@ -196,6 +196,8 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente CODES_SEM_REO = {NAR} # reorientation +# Les codes d'UEs +CODES_JURY_UE = {ADM, CMP, ADJ, ADJR, ADSUP, AJ, ATJ, RAT, DEF, ABAN, DEM, UEBSL} CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit" CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP} "UE validée" diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 6234e5f53f70243a9f23a991e18dae3dcb4c6c7e..f8ce9847139f73c33ea9842a5da8a4c1b83433e7 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -398,7 +398,7 @@ def formsemestre_validation_etud( selected_choice = choice break if not selected_choice: - raise ValueError("code choix invalide ! (%s)" % codechoice) + raise ValueError(f"code choix invalide ! ({codechoice})") # Se.valide_decision(selected_choice) # enregistre return _redirect_valid_choice( @@ -1132,6 +1132,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite }, ) ) + ue_codes = sorted(codes_cursus.CODES_JURY_UE) form_descr += [ ( "date", @@ -1152,6 +1153,18 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite "title": "Moyenne (/20) obtenue dans cette UE:", }, ), + ( + "code_jury", + { + "input_type": "menu", + "title": "Code jury", + "explanation": " code donné par le jury (ADM si validée normalement)", + "allow_null": True, + "allowed_values": [""] + ue_codes, + "labels": ["-"] + ue_codes, + "default": ADM, + }, + ), ] tf = TrivialFormulator( request.base_url, @@ -1173,17 +1186,20 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite de {etud.html_link_fiche()} </h2> - <p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement, + <p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement, <em>dans un semestre hors ScoDoc</em>.</p> - <p class="expl"><b>Les UE validées dans ScoDoc sont déjà - automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant - suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré - <b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre - (<em>pour les semestres précédents gérés avec ScoDoc, - passer par la page jury normale)</em>). + <p class="expl"><b>Les UE validées dans ScoDoc sont + automatiquement prises en compte</b>. + </p> + <p>Cette page est surtout utile pour les étudiants ayant + suivi un début de cursus dans <b>un autre établissement</b>, ou qui + ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>. + </p> + <p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale. + </p> + <p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et + l'attribution des ECTS si le code jury est validant (ADM). </p> - <p>Notez que l'UE est validée (ADM), avec enregistrement immédiat de la décision et - l'attribution des ECTS.</p> <p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p> {_get_etud_ue_cap_html(etud, formsemestre)} @@ -1221,12 +1237,16 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite else: semestre_id = None + if tf[2]["code_jury"] not in CODES_JURY_UE: + flash("Code UE invalide") + return flask.redirect(dest_url) do_formsemestre_validate_previous_ue( formsemestre, etud.id, tf[2]["ue_id"], tf[2]["moy_ue"], tf[2]["date"], + code=tf[2]["code_jury"], semestre_id=semestre_id, ) flash("Validation d'UE enregistrée") @@ -1258,7 +1278,7 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str: <div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()}, sur des semestres ou déclarées comme "antérieures" (externes). </div> - <ul>""" + <ul class="liste_validations">""" ] for validation in validations: if validation.formsemestre_id is None: @@ -1267,17 +1287,20 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str: origine = f", du semestre {formsemestre.html_link_status()}" if validation.semestre_id is not None: origine += f" (<b>S{validation.semestre_id}</b>)" - H.append( - f""" - <li>{validation.html()} + H.append(f"""<li>{validation.html()}""") + if validation.formsemestre.can_edit_jury(): + H.append( + f""" <form class="inline-form"> <button data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}" >effacer</button> </form> - </li> - """, - ) + """, + ) + else: + H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé")) + H.append("</li>") H.append("</ul></div>") return "\n".join(H) @@ -1300,7 +1323,7 @@ def do_formsemestre_validate_previous_ue( ue: UniteEns = UniteEns.query.get_or_404(ue_id) cnx = ndb.GetDBConnexion() - if ue_coefficient != None: + if ue_coefficient is not None: sco_formsemestre.do_formsemestre_uecoef_edit_or_create( cnx, formsemestre.id, ue_id, ue_coefficient ) diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index e2dbd1cbce675ea4d822b355aaff6541db9f7a88..cbb26f08f4569bca1c9018ca0d85449bf72f0f0e 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -59,11 +59,13 @@ from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_t from app.scodoc.sco_permissions import Permission -def _menu_scolarite(authuser, sem: dict, etudid: int): +def _menu_scolarite( + authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str +): """HTML pour menu "scolarite" pour un etudiant dans un semestre. Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant. """ - locked = not sem["etat"] + locked = not formsemestre.etat if locked: lockicon = scu.icontag("lock32_img", title="verrouillé", border="0") return lockicon # no menu @@ -71,10 +73,10 @@ def _menu_scolarite(authuser, sem: dict, etudid: int): Permission.ScoEtudInscrit ) and not authuser.has_permission(Permission.ScoEtudChangeGroups): return "" # no menu - ins = sem["ins"] - args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]} - if ins["etat"] != "D": + args = {"etudid": etudid, "formsemestre_id": formsemestre.id} + + if etat_inscription != scu.DEMISSION: dem_title = "Démission" dem_url = "scolar.form_dem" else: @@ -82,14 +84,14 @@ def _menu_scolarite(authuser, sem: dict, etudid: int): dem_url = "scolar.do_cancel_dem" # Note: seul un etudiant inscrit (I) peut devenir défaillant. - if ins["etat"] != codes_cursus.DEF: + if etat_inscription != codes_cursus.DEF: def_title = "Déclarer défaillance" def_url = "scolar.form_def" - elif ins["etat"] == codes_cursus.DEF: + elif etat_inscription == codes_cursus.DEF: def_title = "Annuler la défaillance" def_url = "scolar.do_cancel_def" def_enabled = ( - (ins["etat"] != "D") + (etat_inscription != scu.DEMISSION) and authuser.has_permission(Permission.ScoEtudInscrit) and not locked ) @@ -128,6 +130,12 @@ def _menu_scolarite(authuser, sem: dict, etudid: int): "enabled": authuser.has_permission(Permission.ScoEtudInscrit) and not locked, }, + { + "title": "Gérer les validations d'UEs antérieures", + "endpoint": "notes.formsemestre_validate_previous_ue", + "args": args, + "enabled": formsemestre.can_edit_jury(), + }, { "title": "Inscrire à un autre semestre", "endpoint": "notes.formsemestre_inscription_with_modules_form", @@ -250,8 +258,8 @@ def ficheEtud(etudid=None): info["last_formsemestre_id"] = "" sem_info = {} for sem in info["sems"]: + formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"]) if sem["ins"]["etat"] != scu.INSCRIT: - formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"]) descr, _ = etud_descr_situation_semestre( etudid, formsemestre, @@ -283,7 +291,7 @@ def ficheEtud(etudid=None): ) grlink = ", ".join(grlinks) # infos ajoutées au semestre dans le parcours (groupe, menu) - menu = _menu_scolarite(authuser, sem, etudid) + menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"]) if menu: sem_info[sem["formsemestre_id"]] = ( "<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>" diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css index d41e4f802b16655c24fcf231c7c6ebc5ffbde8ac..eda3bc6331100faf73f9ccf7f0661015732d03c4 100644 --- a/app/static/css/jury_delete_manual.css +++ b/app/static/css/jury_delete_manual.css @@ -7,3 +7,7 @@ div.jury_decisions_list div { span.parcours { color:blueviolet; } + +div.ue_list_etud_validations ul.liste_validations li { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/app/static/js/validate_previous_ue.js b/app/static/js/validate_previous_ue.js index 22655f498e23016a74dc2ec745e0359b145e731c..c2fe3c88882ab4def85ce1d36b50e6f4b78edb4b 100644 --- a/app/static/js/validate_previous_ue.js +++ b/app/static/js/validate_previous_ue.js @@ -11,27 +11,30 @@ document.addEventListener("DOMContentLoaded", () => { // Handle button click event here event.preventDefault(); const etudid = event.target.dataset.etudid; - const v_id = event.target.dataset.v_id; + const validation_id = event.target.dataset.v_id; const validation_type = event.target.dataset.type; if (confirm("Supprimer cette validation ?")) { - fetch( - `${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`, - { - method: "POST", - } - ).then((response) => { - // Handle the response - if (response.ok) { - location.reload(); - } else { - throw new Error("Request failed"); - } - }); + delete_validation(etudid, validation_type, validation_id); } }); }); }); +async function delete_validation(etudid, validation_type, validation_id) { + const response = await fetch( + `${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`, + { + method: "POST", + } + ); + if (response.ok) { + location.reload(); + } else { + const data = await response.json(); + sco_error_message("erreur: " + data.message); + } +} + function update_ue_list() { var ue_id = $("#tf_ue_id")[0].value; if (ue_id) { diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2 index 0c231b5325c4fde38f3bb68ce31ae862f4b3a949..fc8fd0ce0217549f380d4f8202d51c3930337410 100644 --- a/app/templates/jury/erase_decisions_annee_formation.j2 +++ b/app/templates/jury/erase_decisions_annee_formation.j2 @@ -34,6 +34,32 @@ quelle que soit leur origine.</p> {% endif %} </form> </div> + +<div class="sco_box"> +<div class="sco_box_title">Autres actions:</div> +<ul> + <li><a class="stdlink" href="{{ + url_for('notes.jury_delete_manual', + scodoc_dept=g.scodoc_dept, + etudid=etud.id + ) + }}">effacer les décisions une à une</a> + </li> + {% if formsemestre_origine is not none %} + <li><a class="stdlink" href="{{ + url_for('notes.formsemestre_jury_but_erase', + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id, + etudid=etud.id, only_one_sem=1) + }}"> + effacer seulement les décisions émises par le semestre + {{formsemestre_origine.titre_formation(with_sem_idx=1)|safe}} + (efface aussi la décision annuelle) + </a> + </li> + {% endif %} +</ul> +</div> + {% endif %} diff --git a/app/views/notes.py b/app/views/notes.py index d6a77c614dba19381265486951b95ba96ee044f9..4bc79ebae849674de8d078e5fca46f72870aa66f 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2534,21 +2534,20 @@ def formsemestre_validation_but( </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" - title="efface décisions issues des jurys de cette année" - >effacer décisions de ce jury</a> - + erase_span = f""" <a style="margin-left: 16px;" class="stdlink" href="{ url_for("notes.erase_decisions_annee_formation", scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id, - etudid=deca.etud.id, annee=deca.annee_but)}" - title="efface toutes décisions concernant le BUT{deca.annee_but} - de cet étudiant (même extérieures ou issues d'un redoublement)" - >effacer toutes ses décisions de BUT{deca.annee_but}</a> + etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}" + >effacer des décisions de jury</a> + + <a style="margin-left: 16px;" class="stdlink" + href="{ + url_for("notes.formsemestre_validate_previous_ue", + scodoc_dept=g.scodoc_dept, + etudid=deca.etud.id, formsemestre_id=formsemestre_id)}" + >enregistrer des UEs antérieures</a> """ H.append( f"""<div class="but_settings"> @@ -2966,6 +2965,12 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int): ) ) validations = jury.erase_decisions_annee_formation(etud, formation, annee) + formsemestre_origine_id = request.args.get("formsemestre_id") + formsemestre_origine = ( + FormSemestre.query.get_or_404(formsemestre_origine_id) + if formsemestre_origine_id + else None + ) return render_template( "jury/erase_decisions_annee_formation.j2", annee=annee, @@ -2974,6 +2979,7 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int): ), etud=etud, formation=formation, + formsemestre_origine=formsemestre_origine, validations=validations, sco=ScoData(), title=f"Effacer décisions de jury {etud.nom} - année {annee}",