diff --git a/app/api/jury.py b/app/api/jury.py index 28c0ae8bc3..2800c77a99 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//jury/validation_ue//delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/validation_ue//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//jury/validation_formsemestre//delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/validation_formsemestre//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//jury/autorisation_inscription//delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/autorisation_inscription//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//jury/validation_rcue//delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/validation_rcue//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//jury/validation_annee_but//delete", + methods=["POST"], +) +@api_web_bp.route( + "/etudiant//jury/validation_annee_but//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 0000000000..73e21ecf54 --- /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 8a6047097e..d67a848281 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -78,6 +78,12 @@ class Identite(db.Model): f"" ) + def html_link_fiche(self) -> str: + "lien vers la fiche" + return f"""{self.nomprenom}""" + @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 9a938b6c5a..8a1a8dd0d4 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} - ({self.code} - 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 ""} + :{self.code} + 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 9901dad365..4b1973768f 100644 --- a/app/scodoc/sco_page_etud.py +++ b/app/scodoc/sco_page_etud.py @@ -312,7 +312,13 @@ def ficheEtud(etudid=None): ] = f"""inscrire à un autre semestre""" + }">inscrire à un autre semestre + éditer toutes décisions de jury + """ + 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 0000000000..6580e089f3 --- /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 5298437a5c..7d0353eb89 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 %} -

Aucune validation de jury enregistrée pour {{etud.nom_disp()}} sur +

Aucune validation de jury enregistrée pour {{etud.html_link_fiche()}} sur l'année {{annee}} de la formation {{ formation.html() }}

@@ -13,7 +13,7 @@ de la formation {{ formation.html() }} {% else %} -

Effacer les décisions de jury pour l'année {{annee}} de {{etud.nom_disp()}} ?

+

Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()}} ?

Affectera toutes les décisions concernant l'année {{annee}} de la formation, quelle que soit leur origine.

diff --git a/app/templates/jury/jury_delete_manual.j2 b/app/templates/jury/jury_delete_manual.j2 new file mode 100644 index 0000000000..ed7a9d2e8e --- /dev/null +++ b/app/templates/jury/jury_delete_manual.j2 @@ -0,0 +1,134 @@ +{% extends 'base.j2' %} + +{% block styles %} + {{super()}} + +{% endblock %} + +{% block app_content %} + + +

Décisions de jury enregistrées pour {{etud.html_link_fiche()|safe}}

+ +

+Cette page liste toutes les décisions de jury connus de ScoDoc concernant cet étudiant +et permet de les effacer une par une. +

+

+Attention, il vous appartient de vérifier la cohérence du résultat ! +En principe, l'usage de cette page devrait rester exceptionnel. +Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les +pages de saisie de jury habituelles). +

+{% if sem_vals.first() %} +
+
Décisions de semestres
+
    + {% for v in sem_vals %} +
  • {{v.html()|safe}} +
    +
  • + {% endfor %} +
+
+{% endif %} + +{% if ue_vals.first() %} +
+
Décisions d'UEs
+
    + {% for v in ue_vals %} +
  • {{v.html(detail=True)|safe}} +
    +
  • + {% endfor %} +
+
+{% endif %} + +{% if rcue_vals.first() %} +
+
Décisions de RCUE (niveaux de compétences)
+
    + {% for v in rcue_vals %} +
  • {{v.html()|safe}} +
    +
  • + {% endfor %} +
+
+{% endif %} + +{% if annee_but_vals.first() %} +
+
Décisions d'années BUT
+
    + {% for v in annee_but_vals %} +
  • {{v.html()|safe}} +
    +
  • + {% endfor %} +
+
+{% endif %} + +{% if autorisations.first() %} +
+
Autorisations d'inscriptions (passages)
+
    + {% for v in autorisations %} +
  • {{v.html()|safe}} +
    +
  • + {% endfor %} +
+
+{% endif %} + +{% if not( + sem_vals.first() or sem_ues.first() or sem_rcues.first() + or annee_but_vals.first() or autorisations.first()) +%} +
+

aucune décision enregistrée

+
+{% endif %} + + + +{% endblock %} + + +{% block scripts %} +{{super()}} + + +{% endblock %} diff --git a/app/views/notes.py b/app/views/notes.py index e29a93c918..3a5e3bbe74 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/", + 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,