diff --git a/app/but/jury_but.py b/app/but/jury_but.py index c1ded7b8837799c8126d8a5b39ae2a1d959fc85d..af9b46731e932b017f6e1d48d7ef02ba735032f8 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -75,11 +75,9 @@ from app.comp.res_but import ResultatsSemestreBUT from app.comp import res_sem from app.models.but_refcomp import ( - ApcAnneeParcours, ApcCompetence, ApcNiveau, ApcParcours, - ApcParcoursNiveauCompetence, ) from app.models import Scolog, ScolarAutorisationInscription from app.models.but_validations import ( @@ -89,7 +87,7 @@ from app.models.but_validations import ( ) from app.models.etudiants import Identite from app.models.formations import Formation -from app.models.formsemestre import FormSemestre, FormSemestreInscription +from app.models.formsemestre import FormSemestre from app.models.ues import UniteEns from app.models.validations import ScolarFormSemestreValidation from app.scodoc import sco_cache @@ -473,7 +471,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): scodoc_dept=g.scodoc_dept, semestre_idx=formsemestre.semestre_id, formation_id=formsemestre.formation.id)}"> - {formsemestre.formation.to_html()} ({ + {formsemestre.formation.html()} ({ formsemestre.formation.id})</a> </li> </ul> @@ -902,7 +900,8 @@ 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. + Si à cheval ou only_one_sem, n'efface que les décisions UE et les + autorisations de passage du semestre d'origine du deca. (commite la session.) """ if only_one_sem or self.a_cheval: diff --git a/app/but/jury_but_view.py b/app/but/jury_but_view.py index 0a20e9334ef2a34e98b7c608f56b028c34b6938d..a61f1f14bb7ec9e9ed6250649461210660ed9ecd 100644 --- a/app/but/jury_but_view.py +++ b/app/but/jury_but_view.py @@ -246,7 +246,7 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: scoplement = ( f"""<div class="scoplement">{ - dec_rcue.validation.to_html() + dec_rcue.validation.html() }</div>""" if dec_rcue.validation else "" diff --git a/app/comp/jury.py b/app/comp/jury.py index cee32ffddff4518ea05f0bef05f85f9b34a867c2..1c43158da4ad1d1221a10692622d5f8e36e09022 100644 --- a/app/comp/jury.py +++ b/app/comp/jury.py @@ -10,8 +10,17 @@ import pandas as pd import sqlalchemy as sa from app import db -from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns from app.comp.res_cache import ResultatsCache +from app.models import ( + ApcValidationAnnee, + ApcValidationRCUE, + Formation, + FormSemestre, + Identite, + ScolarAutorisationInscription, + ScolarFormSemestreValidation, + UniteEns, +) from app.scodoc import sco_cache from app.scodoc import codes_cursus @@ -81,7 +90,7 @@ class ValidationsSemestre(ResultatsCache): # UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }} decisions_jury_ues = {} - # Parcours les décisions d'UE: + # Parcoure les décisions d'UE: for decision in ( decisions_jury_q.filter(db.text("ue_id is not NULL")) .join(UniteEns) @@ -172,3 +181,80 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame with db.engine.begin() as connection: df = pd.read_sql_query(query, connection, params=params, index_col="etudid") return df + + +def erase_decisions_annee_formation( + etud: Identite, formation: Formation, annee: int, delete=False +) -> list: + """Efface toutes les décisions de jury de l'étudiant dans les formations de même code + que celle donnée pour cette année de la formation: + UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante. + Ne considère pas l'origine de la décision. + annee: entier, 1, 2, 3, ... + Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher. + """ + sem1, sem2 = annee * 2 - 1, annee * 2 + # UEs + validations = ( + ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) + .join(UniteEns) + .filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2)) + .join(Formation) + .filter_by(formation_code=formation.formation_code) + .order_by( + UniteEns.acronyme, UniteEns.numero + ) # acronyme d'abord car 2 semestres + .all() + ) + # RCUEs (a priori inutile de matcher sur l'ue2_id) + validations += ( + ApcValidationRCUE.query.filter_by(etudid=etud.id) + .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id) + .filter_by(semestre_idx=sem1) + .join(Formation) + .filter_by(formation_code=formation.formation_code) + .order_by(UniteEns.acronyme, UniteEns.numero) + .all() + ) + # Validation de semestres classiques + validations += ( + ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None) + .join( + FormSemestre, + FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id, + ) + .filter( + db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2) + ) + .join(Formation) + .filter_by(formation_code=formation.formation_code) + .all() + ) + # Année BUT + validations += ( + ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee) + .join(Formation) + .filter_by(formation_code=formation.formation_code) + .all() + ) + # Autorisations vers les semestres suivants ceux de l'année: + validations += ( + ScolarAutorisationInscription.query.filter_by( + etudid=etud.id, formation_code=formation.formation_code + ) + .filter( + db.or_( + ScolarAutorisationInscription.semestre_id == sem1 + 1, + ScolarAutorisationInscription.semestre_id == sem2 + 1, + ) + ) + .all() + ) + + if delete: + for validation in validations: + db.session.delete(validation) + db.session.commit() + sco_cache.invalidate_formsemestre_etud(etud) + return [] + return validations diff --git a/app/models/but_validations.py b/app/models/but_validations.py index c2be058b3f4dcceb146c1eb3545026fa8311ead8..778164765f2a76d4d8a9c5b4ee9ccfb9fe670529 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -66,7 +66,7 @@ class ApcValidationRCUE(db.Model): return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: { self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}""" - def to_html(self) -> str: + def html(self) -> str: "description en HTML" return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: <b>{self.code}</b> @@ -348,6 +348,13 @@ class ApcValidationAnnee(db.Model): "ordre": self.ordre, } + def html(self) -> str: + "Affichage html" + return f"""Validation <b>année BUT{self.ordre}</b> émise par + {self.formsemestre.html_link_status() if self.formsemestre else "-"} + le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")} + """ + def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: """ diff --git a/app/models/formations.py b/app/models/formations.py index e98d66f7b135daeeef19c693fede845698af680f..fb7529e323ef18a7f48a94e48f7a7ce1e2ffbf5a 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -60,7 +60,7 @@ class Formation(db.Model): return f"""<{self.__class__.__name__}(id={self.id}, dept_id={ self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>""" - def to_html(self) -> str: + def html(self) -> str: "titre complet pour affichage" return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}""" diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 516d8fc513c19a1ae3ffcf8d942fa8b5b45b0753..efbceb74b6d40f7e326a96e2786486ff660c8b9f 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -16,7 +16,7 @@ from operator import attrgetter from flask_login import current_user -from flask import flash, g +from flask import flash, g, url_for from sqlalchemy.sql import text import app.scodoc.sco_utils as scu @@ -163,6 +163,14 @@ class FormSemestre(db.Model): def __repr__(self): return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>" + def html_link_status(self) -> 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> + """ + @classmethod def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre": """ "FormSemestre ou 404, cherche uniquement dans le département courant""" diff --git a/app/models/validations.py b/app/models/validations.py index 229d15ad5425b53d88226bb592b182ab43a36613..9a938b6c5ab6759fea7eb21ecf4ed5988cd6b7e7 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -59,13 +59,16 @@ class ScolarFormSemestreValidation(db.Model): ) def __repr__(self): - return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})" + return f"""{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={ + self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})""" def __str__(self): if self.ue_id: # Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue ! - return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}""" - return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}""" + return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id + }: {self.code}""" + return f"""décision sur semestre {self.formsemestre.titre_mois()} du { + self.event_date.strftime("%d/%m/%Y")}""" def to_dict(self) -> dict: "as a dict" @@ -73,6 +76,20 @@ class ScolarFormSemestreValidation(db.Model): d.pop("_sa_instance_state", None) return d + def html(self) -> 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")}) + """ + else: + return f"""Validation du semestre S{ + self.formsemestre.semestre_id if self.formsemestre else "?"} + (<b>{self.code}</b> + le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}) + """ + class ScolarAutorisationInscription(db.Model): """Autorisation d'inscription dans un semestre""" @@ -93,6 +110,7 @@ class ScolarAutorisationInscription(db.Model): db.Integer, db.ForeignKey("notes_formsemestre.id"), ) + origin_formsemestre = db.relationship("FormSemestre", lazy="select", uselist=False) def __repr__(self) -> str: return f"""{self.__class__.__name__}(id={self.id}, etudid={ @@ -104,6 +122,15 @@ class ScolarAutorisationInscription(db.Model): d.pop("_sa_instance_state", None) return d + def html(self) -> str: + "Affichage html" + 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 "-"} + le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")} + """ + @classmethod def autorise_etud( cls, diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index 0b9d27a8350f86606c1ee46ae69ba3bbfe8064e8..36b8db30b3c117d5f3eed2c8ee6f24aa316fb8a0 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -315,6 +315,19 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa SemBulletinsPDFCache.invalidate_sems(formsemestre_ids) +def invalidate_formsemestre_etud(etud: "Identite"): + """Invalide tous les formsemestres auxquels l'étudiant est inscrit""" + from app.models import FormSemestre, FormSemestreInscription + + inscriptions = ( + FormSemestreInscription.query.filter_by(etudid=etud.id) + .join(FormSemestre) + .filter_by(dept_id=g.scodoc_dept_id) + ) + for inscription in inscriptions: + invalidate_formsemestre(inscription.formsemestre_id) + + class DeferredSemCacheManager: """Contexte pour effectuer des opérations indépendantes dans la même requete qui invalident le cache. Par exemple, quand on inscrit diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index f41804f1371e2ecd43b25d5d66a5354d800ac392..ba6cb918eeb2f3b045657438418e8b1fe9f9a7ef 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -757,7 +757,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list ], page_title=f"Programme {formation.acronyme} v{formation.version}", ), - f"""<h2>{formation.to_html()} {lockicon} + f"""<h2>{formation.html()} {lockicon} </h2> """, ] @@ -1010,12 +1010,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); <p><ul>""" ) for formsemestre in formsemestres: - H.append( - f"""<li><a class="stdlink" href="{ - url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id - )}">{formsemestre.titre_mois()}</a>""" - ) + H.append(f"""<li>{formsemestre.html_link_status()}""") if not formsemestre.etat: H.append(" [verrouillé]") else: diff --git a/app/scodoc/sco_recapcomplet.py b/app/scodoc/sco_recapcomplet.py index 549b375d1ce068a5b690e68e105998e8e9bd7e89..63a0d4e2521f15834fd95b0ecb075c83916db582 100644 --- a/app/scodoc/sco_recapcomplet.py +++ b/app/scodoc/sco_recapcomplet.py @@ -142,7 +142,7 @@ def formsemestre_recapcomplet( H.append( '<select name="tabformat" onchange="document.f.submit()" class="noprint">' ) - for (fmt, label) in ( + for fmt, label in ( ("html", "Tableau"), ("evals", "Avec toutes les évaluations"), ("xlsx", "Excel (non formaté)"), @@ -186,7 +186,7 @@ def formsemestre_recapcomplet( </li> <li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1) - }">Effacer <em>toutes</em> les décisions de jury (BUT) du semestre</a> + }">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a> </li> """ ) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index 1a4e68bbdcc3065b297e88b9e1b03c3409e657b9..c028429356269b73bf348a5208b85787a8fef330 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -145,12 +145,7 @@ class SemSet(dict): # Construction du ou des lien(s) vers le semestre self["semlinks"] = [ - f"""<a class="stdlink" href="{ - url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id) - }">{formsemestre.titre_annee()}</a> - """ - for formsemestre in self.formsemestres + formsemestre.html_link_status() for formsemestre in self.formsemestres ] self["semtitles_str"] = "<br>".join(self["semlinks"]) diff --git a/app/static/css/cursus_but.css b/app/static/css/cursus_but.css index 79cde97cb0b75ad8dfb01723e5f79109071a1a1c..e998ccda54caf0467d93062eb3d1fb8ea04970a6 100644 --- a/app/static/css/cursus_but.css +++ b/app/static/css/cursus_but.css @@ -15,7 +15,6 @@ padding-bottom: 0px; padding-left: 16px; padding-right: 0px; - background: #FFF; border: 1px solid #aaa; border-radius: 8px; @@ -39,4 +38,10 @@ div.code_rcue { padding-top: 8px; padding-bottom: 8px; position: relative; +} + +div.code_jury { + padding-right: 4px; + padding-left: 4px; + width: 64px; } \ No newline at end of file diff --git a/app/templates/bul_head.j2 b/app/templates/bul_head.j2 index 473393802b5e35133e05d0291d3770efb4acb3b2..691cb4fb3271f75590589a32e26cfb4518363068 100644 --- a/app/templates/bul_head.j2 +++ b/app/templates/bul_head.j2 @@ -15,11 +15,9 @@ <input type="hidden" name="etudid" value="{{etud.id}}"></input> <input type="hidden" name="format" value="{{format}}"></input> Bulletin - <span class="bull_liensemestre"><a href="{{ - url_for("notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_mois() - }}</a></span> + <span class="bull_liensemestre"> + {{formsemestre.html_link_status() | safe}} + </span> <div> <em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em> diff --git a/app/templates/but/cursus_etud.j2 b/app/templates/but/cursus_etud.j2 index 571909683a93c0fd1c2877c5c894a90b6d3c4ddc..4baaa96f191cc62d4ddbaecbfb141fb8939bc43e 100644 --- a/app/templates/but/cursus_etud.j2 +++ b/app/templates/but/cursus_etud.j2 @@ -15,14 +15,16 @@ <div class="code_jury">{{validation.code}}</div> <div class="scoplement"> <div>{{validation.ue1.acronyme}} - {{validation.ue2.acronyme}}</div> - <div>Jury de {{validation.formsemestre.titre_annee()}}</div> + <div>Jury de {{validation.formsemestre.titre_annee() if validation.formsemestre else "-"}}</div> <div>enregistré le {{ validation.date.strftime("%d/%m/%Y à %H:%M") }}</div> </div> </div> {% else %} - - + <div class="code_rcue"> + <div class="code_jury">-</div> + </div> {%endif%} </div> {% endfor %} diff --git a/app/templates/but/parcour_formation.j2 b/app/templates/but/parcour_formation.j2 index f128c052438953c2cd2919ceaa8b704e18f39f18..84aa11f02a781192d7218d295821911987c4a27c 100644 --- a/app/templates/but/parcour_formation.j2 +++ b/app/templates/but/parcour_formation.j2 @@ -44,7 +44,7 @@ {%- endmacro %} {% block app_content %} -<h2>{{formation.to_html()}}</h2> +<h2>{{formation.html()}}</h2> {# Liens vers les différents parcours #} <div class="les_parcours"> @@ -127,7 +127,7 @@ Choisissez un parcours... d'associer à chaque semestre d'un niveau de compétence une UE de la formation <a class="stdlink" href="{{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id ) - }}">{{formation.to_html()}} + }}">{{formation.html()}} </a>.</p> <p>Le symbole <span class="parc">TC</span> désigne un niveau du tronc commun diff --git a/app/templates/jury/erase_decisions_annee_formation.j2 b/app/templates/jury/erase_decisions_annee_formation.j2 new file mode 100644 index 0000000000000000000000000000000000000000..5298437a5c816961380174651c7df8742cc74ed0 --- /dev/null +++ b/app/templates/jury/erase_decisions_annee_formation.j2 @@ -0,0 +1,41 @@ +{% extends 'base.j2' %} + +{% block app_content %} + +{% if not validations %} +<p>Aucune validation de jury enregistrée pour <b>{{etud.nom_disp()}}</b> sur +<b>l'année {{annee}}</b> +de la formation <em>{{ formation.html() }}</em> +</p> + +<div style="margin-top: 16px;"> + <a class="stdlink" href="{{ cancel_url }}">continuer</a> +</div> +{% else %} + +<h2>Effacer les décisions de jury pour l'année {{annee}} de {{etud.nom_disp()}} ?</h2> + +<p class="help">Affectera toutes les décisions concernant l'année {{annee}} de la formation, +quelle que soit leur origine.</p> + +<p>Les décisions concernées sont:</p> +<ul> + {% for validation in validations %} + <li>{{ validation.html() | safe}} + </li> + {% endfor %} +</ul> +<div style="margin-top: 16px;"> + <form method="post"> + <input type="submit" value="Effacer ces décisions" /> + {% if cancel_url %} + <input type="button" value="Annuler" style="margin-left: 16px;" + onClick="document.location='{{ cancel_url }}';" /> + {% endif %} + </form> +</div> +{% endif %} + + + +{% endblock %} \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index 70990ac60a1af68b4ef2ebdc601c7c6fbcb08b99..e29a93c918c14bc004013cbb15a2f5c28d25b09d 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -48,9 +48,9 @@ from app.but.forms import jury_but_forms from app.but import jury_but_pv from app.but import jury_but_view -from app.comp import res_sem +from app.comp import jury, res_sem from app.comp.res_compat import NotesTableCompat -from app.models import ScolarAutorisationInscription, ScolarNews, Scolog +from app.models import Formation, ScolarAutorisationInscription, ScolarNews, Scolog from app.models.but_refcomp import ApcNiveau, ApcParcours from app.models.config import ScoDocSiteConfig from app.models.etudiants import Identite @@ -2494,7 +2494,19 @@ def formsemestre_validation_but( 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>""" + etudid=deca.etud.id)}" class="stdlink" + title="efface décisions issues des jurys de cette année" + >effacer décisions</a> + + <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> + """ H.append( f"""<div class="but_settings"> <input type="checkbox" onchange="enable_manual_codes(this)"> @@ -2815,15 +2827,15 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): ) @scodoc @permission_required(Permission.ScoView) -def formsemestre_jury_but_erase( - formsemestre_id: int, etudid: int = None, only_one_sem=False -): +def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None): """Supprime la décision de jury BUT pour cette année. - Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année. Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits. + Si only_one_sem, n'efface que pour le formsemestre indiqué, pas les deux de l'année. """ only_one_sem = int(request.args.get("only_one_sem") or False) - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() if not formsemestre.can_edit_jury(): raise ScoPermissionDenied( dest_url=url_for( @@ -2881,14 +2893,53 @@ def formsemestre_jury_but_erase( if only_one_sem else """Les validations de toutes les UE, RCUE (compétences) et année issues de cette année scolaire seront effacées. - Les décisions des années scolaires précédentes ne seront pas modifiées. """ ) - + """<div class="warning">Cette opération est irréversible !</div>""", + + """ + <p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p> + <div class="warning">Cette opération est irréversible !</div> + """, cancel_url=dest_url, ) +@bp.route( + "/erase_decisions_annee_formation/<int:etudid>/<int:formation_id>/<int:annee>", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoEtudInscrit) +def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int): + """Efface toute les décisions d'une année pour cet étudiant""" + etud: Identite = Identite.query.get_or_404(etudid) + formation: Formation = Formation.query.filter_by( + id=formation_id, dept_id=g.scodoc_dept_id + ).first_or_404() + if request.method == "POST": + jury.erase_decisions_annee_formation(etud, formation, annee, delete=True) + flash("Décisions de jury effacées") + return redirect( + url_for( + "scolar.ficheEtud", + scodoc_dept=g.scodoc_dept, + etudid=etud.id, + ) + ) + validations = jury.erase_decisions_annee_formation(etud, formation, annee) + return render_template( + "jury/erase_decisions_annee_formation.j2", + annee=annee, + cancel_url=url_for( + "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id + ), + etud=etud, + formation=formation, + validations=validations, + sco=ScoData(), + title=f"Effacer décisions de jury {etud.nom} - année {annee}", + ) + + sco_publish( "/formsemestre_lettres_individuelles", sco_pv_forms.formsemestre_lettres_individuelles,