diff --git a/app/api/jury.py b/app/api/jury.py index 28c0ae8bc3b79fd36d6b70fd3b903c0602168d0a..2800c77a99da18b0d4db3b2c0076c09191468aab 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -12,11 +12,20 @@ from flask_json import as_json from flask_login import login_required import app -from app.api import api_bp as bp, api_web_bp +from app import db, log +from app.api import api_bp as bp, api_web_bp, tools from app.decorators import scodoc, permission_required from app.scodoc.sco_exceptions import ScoException from app.but import jury_but_results -from app.models import FormSemestre +from app.models import ( + ApcValidationAnnee, + ApcValidationRCUE, + FormSemestre, + Identite, + ScolarAutorisationInscription, + ScolarFormSemestreValidation, +) +from app.scodoc import sco_cache from app.scodoc.sco_permissions import Permission @@ -36,3 +45,134 @@ def decisions_jury(formsemestre_id: int): return rows else: raise ScoException("non implemente") + + +@bp.route( + "/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoEtudInscrit) +@as_json +def validation_ue_delete(etudid: int, validation_id: int): + "Efface cette validation" + return _validation_ue_delete(etudid, validation_id) + + +@bp.route( + "/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoEtudInscrit) +@as_json +def validation_formsemestre_delete(etudid: int, validation_id: int): + "Efface cette validation" + # c'est la même chose (formations classiques) + return _validation_ue_delete(etudid, validation_id) + + +def _validation_ue_delete(etudid: int, validation_id: int): + "Efface cette validation (semestres classiques ou UEs)" + etud = tools.get_etud(etudid) + if etud is None: + return "étudiant inconnu", 404 + validation = ScolarFormSemestreValidation.query.filter_by( + id=validation_id, etudid=etudid + ).first_or_404() + log(f"validation_ue_delete: etuid={etudid} {validation}") + db.session.delete(validation) + sco_cache.invalidate_formsemestre_etud(etud) + db.session.commit() + return "ok" + + +@bp.route( + "/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoEtudInscrit) +@as_json +def autorisation_inscription_delete(etudid: int, validation_id: int): + "Efface cette validation" + etud = tools.get_etud(etudid) + if etud is None: + return "étudiant inconnu", 404 + validation = ScolarAutorisationInscription.query.filter_by( + id=validation_id, etudid=etudid + ).first_or_404() + log(f"autorisation_inscription_delete: etuid={etudid} {validation}") + db.session.delete(validation) + sco_cache.invalidate_formsemestre_etud(etud) + db.session.commit() + return "ok" + + +@bp.route( + "/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoEtudInscrit) +@as_json +def validation_rcue_delete(etudid: int, validation_id: int): + "Efface cette validation" + etud = tools.get_etud(etudid) + if etud is None: + return "étudiant inconnu", 404 + validation = ApcValidationRCUE.query.filter_by( + id=validation_id, etudid=etudid + ).first_or_404() + log(f"validation_ue_delete: etuid={etudid} {validation}") + db.session.delete(validation) + sco_cache.invalidate_formsemestre_etud(etud) + db.session.commit() + return "ok" + + +@bp.route( + "/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete", + methods=["POST"], +) +@login_required +@scodoc +@permission_required(Permission.ScoEtudInscrit) +@as_json +def validation_annee_but_delete(etudid: int, validation_id: int): + "Efface cette validation" + etud = tools.get_etud(etudid) + if etud is None: + return "étudiant inconnu", 404 + validation = ApcValidationAnnee.query.filter_by( + id=validation_id, etudid=etudid + ).first_or_404() + log(f"validation_annee_but: etuid={etudid} {validation}") + db.session.delete(validation) + sco_cache.invalidate_formsemestre_etud(etud) + db.session.commit() + return "ok" diff --git a/app/but/jury_edit_manual.py b/app/but/jury_edit_manual.py new file mode 100644 index 0000000000000000000000000000000000000000..73e21ecf541e0047edd7bd6e5b41254826c44462 --- /dev/null +++ b/app/but/jury_edit_manual.py @@ -0,0 +1,66 @@ +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## + +"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes) + +Non spécifique au BUT. +""" + +import flask +from flask import flash, render_template, url_for +from flask import g, request + +from app import db + +from app.models import ( + ApcValidationAnnee, + ApcValidationRCUE, + FormSemestre, + Identite, + UniteEns, + ScolarAutorisationInscription, + ScolarFormSemestreValidation, +) +from app.views import ScoData + + +def jury_delete_manual(etud: Identite): + """Vue (réservée au chef de dept.) + présentant *toutes* les décisions de jury concernant cet étudiant + et permettant de les supprimer une à une. + """ + sem_vals = ScolarFormSemestreValidation.query.filter_by( + etudid=etud.id, ue_id=None + ).order_by(ScolarFormSemestreValidation.event_date) + ue_vals = ( + ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) + .join(UniteEns) + .order_by(ScolarFormSemestreValidation.event_date, UniteEns.numero) + ) + autorisations = ScolarAutorisationInscription.query.filter_by( + etudid=etud.id + ).order_by( + ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date + ) + rcue_vals = ( + ApcValidationRCUE.query.filter_by(etudid=etud.id) + .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id) + .order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date) + ) + annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by( + ApcValidationAnnee.ordre, ApcValidationAnnee.date + ) + return render_template( + "jury/jury_delete_manual.j2", + etud=etud, + sem_vals=sem_vals, + ue_vals=ue_vals, + autorisations=autorisations, + rcue_vals=rcue_vals, + annee_but_vals=annee_but_vals, + sco=ScoData(), + title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}", + ) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 8a6047097ec66d98c089263112b0784d11b178d7..d67a8482814fcece7e092199fb5be51c8362036e 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -78,6 +78,12 @@ class Identite(db.Model): f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>" ) + def html_link_fiche(self) -> str: + "lien vers la fiche" + return f"""<a class="stdlink" href="{ + url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id) + }">{self.nomprenom}</a>""" + @classmethod def from_request(cls, etudid=None, code_nip=None) -> "Identite": """Étudiant à partir de l'etudid ou du code_nip, soit diff --git a/app/models/validations.py b/app/models/validations.py index 9a938b6c5ab6759fea7eb21ecf4ed5988cd6b7e7..8a1a8dd0d4f0693d95c9f5a6376ac3b0ff0e2e06 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -76,12 +76,14 @@ class ScolarFormSemestreValidation(db.Model): d.pop("_sa_instance_state", None) return d - def html(self) -> str: + def html(self, detail=False) -> str: "Affichage html" if self.ue_id is not None: - return f"""Validation de l'UE {self.ue.acronyme} - (<b>{self.code}</b> - le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}) + return f"""Validation de l'UE {self.ue.acronyme} de {self.ue.formation.acronyme} + {("émise par " + self.formsemestre.html_link_status()) + if self.formsemestre else ""} + :<b>{self.code}</b> + le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")} """ else: return f"""Validation du semestre S{ diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py index 9901dad365fab3ba8e992779a7ca469ecf5c2c14..4b1973768f9aa7129391e6ceb0e3e5a343b11345 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -312,7 +312,13 @@ def ficheEtud(etudid=None): ] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{ url_for("notes.formsemestre_inscription_with_modules_form", scodoc_dept=g.scodoc_dept, etudid=etudid) - }">inscrire à un autre semestre</a></span>""" + }">inscrire à un autre semestre</a></span> + <span class="link_bul_pdf"><a class="stdlink" href="{ + url_for("notes.jury_delete_manual", + scodoc_dept=g.scodoc_dept, etudid=etudid) + }">éditer toutes décisions de jury</a></span> + """ + else: info["link_inscrire_ailleurs"] = "" else: diff --git a/app/static/css/jury_delete_manual.css b/app/static/css/jury_delete_manual.css new file mode 100644 index 0000000000000000000000000000000000000000..6580e089f38b1bad0ff634c120b4f443e4c58a01 --- /dev/null +++ b/app/static/css/jury_delete_manual.css @@ -0,0 +1,9 @@ + +div.jury_decisions_list div { + font-size: 120%; + font-weight: bold; +} + +div.jury_decisions_list form { + display: inline-block; +} \ No newline at end of file diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2 index 5298437a5c816961380174651c7df8742cc74ed0..7d0353eb893247ddbd3b1333c04fc5e88d2a8957 100644 --- a/app/templates/jury/erase_decisions_annee_formation.j2 +++ b/app/templates/jury/erase_decisions_annee_formation.j2 @@ -3,7 +3,7 @@ {% block app_content %} {% if not validations %} -<p>Aucune validation de jury enregistrée pour <b>{{etud.nom_disp()}}</b> sur +<p>Aucune validation de jury enregistrée pour <b>{{etud.html_link_fiche()}}</b> sur <b>l'année {{annee}}</b> de la formation <em>{{ formation.html() }}</em> </p> @@ -13,7 +13,7 @@ de la formation <em>{{ formation.html() }}</em> </div> {% else %} -<h2>Effacer les décisions de jury pour l'année {{annee}} de {{etud.nom_disp()}} ?</h2> +<h2>Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()}} ?</h2> <p class="help">Affectera toutes les décisions concernant l'année {{annee}} de la formation, quelle que soit leur origine.</p> diff --git a/app/templates/jury/jury_delete_manual.j2 b/app/templates/jury/jury_delete_manual.j2 new file mode 100644 index 0000000000000000000000000000000000000000..ed7a9d2e8e05eb7e7c2a911aba9bdcea6a3152d0 --- /dev/null +++ b/app/templates/jury/jury_delete_manual.j2 @@ -0,0 +1,134 @@ +{% extends 'base.j2' %} + +{% block styles %} + {{super()}} + <link href="{{scu.STATIC_DIR}}/css/jury_delete_manual.css" rel="stylesheet" type="text/css" /> +{% endblock %} + +{% block app_content %} + + +<h2>Décisions de jury enregistrées pour {{etud.html_link_fiche()|safe}}</h2> + +<p class="help"> +Cette page liste toutes les décisions de jury connus de ScoDoc concernant cet étudiant +et permet de les effacer une par une. +</p> +<p class="help"> +<b>Attention</b>, il vous appartient de vérifier la cohérence du résultat ! +En principe, <b>l'usage de cette page devrait rester exceptionnel</b>. +Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les +pages de saisie de jury habituelles). +</p> +{% if sem_vals.first() %} +<div class="jury_decisions_list jury_decisions_sems"> + <div>Décisions de semestres</div> + <ul> + {% for v in sem_vals %} + <li>{{v.html()|safe}} + <form><button data-v_id="{{v.id}}" data-type="validation_formsemestre">effacer</button></form> + </li> + {% endfor %} + </ul> +</div> +{% endif %} + +{% if ue_vals.first() %} +<div class="jury_decisions_list jury_decisions_ues"> + <div>Décisions d'UEs</div> + <ul> + {% for v in ue_vals %} + <li>{{v.html(detail=True)|safe}} + <form><button data-v_id="{{v.id}}" data-type="validation_ue">effacer</button></form> + </li> + {% endfor %} + </ul> +</div> +{% endif %} + +{% if rcue_vals.first() %} +<div class="jury_decisions_list jury_decisions_rcues"> + <div>Décisions de RCUE (niveaux de compétences)</div> + <ul> + {% for v in rcue_vals %} + <li>{{v.html()|safe}} + <form><button data-v_id="{{v.id}}" data-type="validation_rcue">effacer</button></form> + </li> + {% endfor %} + </ul> +</div> +{% endif %} + +{% if annee_but_vals.first() %} +<div class="jury_decisions_list jury_decisions_annees_but"> + <div>Décisions d'années BUT</div> + <ul> + {% for v in annee_but_vals %} + <li>{{v.html()|safe}} + <form><button data-v_id="{{v.id}}" data-type="validation_annee_but">effacer</button></form> + </li> + {% endfor %} + </ul> +</div> +{% endif %} + +{% if autorisations.first() %} +<div class="jury_decisions_list jury_decisions_autorisation_inscription"> + <div>Autorisations d'inscriptions (passages)</div> + <ul> + {% for v in autorisations %} + <li>{{v.html()|safe}} + <form><button data-v_id="{{v.id}}" data-type="autorisation_inscription">effacer</button></form> + </li> + {% endfor %} + </ul> +</div> +{% endif %} + +{% if not( + sem_vals.first() or sem_ues.first() or sem_rcues.first() + or annee_but_vals.first() or autorisations.first()) +%} +<div> + <p class="fontred">aucune décision enregistrée</p> +</div> +{% endif %} + +<div> + <a class="stdlink" href="{{etud.html_link_fiche()}}">retour à sa fiche</a> +</div> + +{% endblock %} + + +{% block scripts %} +{{super()}} + +<script> +document.addEventListener('DOMContentLoaded', () => { + const buttons = document.querySelectorAll('.jury_decisions_list button'); + + buttons.forEach(button => { + button.addEventListener('click', (event) => { + // Handle button click event here + event.preventDefault(); + const v_id = event.target.dataset.v_id; + const validation_type = event.target.dataset.type; + if (confirm("Supprimer cette validation ?")) { + fetch(`${SCO_URL}/../api/etudiant/{{etud.id}}/jury/${validation_type}/${v_id}/delete`, + { + method: "POST", + }).then(response => { + // Handle the response + if (response.ok) { + location.reload(); + } else { + throw new Error('Request failed'); + } + }); + } + }); + }); +}); +</script> +{% endblock %} diff --git a/app/views/notes.py b/app/views/notes.py index e29a93c918c14bc004013cbb15a2f5c28d25b09d..3a5e3bbe74a6bdb11fdab40c7c0e929ffbfe63b5 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -47,11 +47,12 @@ from app.but import jury_but, jury_but_validation_auto from app.but.forms import jury_but_forms from app.but import jury_but_pv from app.but import jury_but_view +from app.but import jury_edit_manual from app.comp import jury, res_sem from app.comp.res_compat import NotesTableCompat from app.models import Formation, ScolarAutorisationInscription, ScolarNews, Scolog -from app.models.but_refcomp import ApcNiveau, ApcParcours +from app.models.but_refcomp import ApcNiveau from app.models.config import ScoDocSiteConfig from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre @@ -2940,6 +2941,18 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int): ) +@bp.route( + "/jury_delete_manual/<int:etudid>", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoEtudInscrit) +def jury_delete_manual(etudid: int): + """Efface toute les décisions d'une année pour cet étudiant""" + etud: Identite = Identite.query.get_or_404(etudid) + return jury_edit_manual.jury_delete_manual(etud) + + sco_publish( "/formsemestre_lettres_individuelles", sco_pv_forms.formsemestre_lettres_individuelles,