diff --git a/app/but/validations_view.py b/app/but/validations_view.py index 83c84301..06145a19 100644 --- a/app/but/validations_view.py +++ b/app/but/validations_view.py @@ -4,13 +4,10 @@ # See LICENSE ############################################################################## -"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes) - -Non spécifique au BUT. +"""Jury édition manuelle des décisions RCUE antérieures """ from flask import render_template -import sqlalchemy as sa from app import log from app.but import cursus_but diff --git a/app/scodoc/sco_formation_recap.py b/app/scodoc/sco_formation_recap.py index f0074e4e..331e641c 100644 --- a/app/scodoc/sco_formation_recap.py +++ b/app/scodoc/sco_formation_recap.py @@ -45,16 +45,15 @@ import app.scodoc.sco_utils as scu # ---- Table recap formation -def formation_table_recap(formation_id, fmt="html") -> Response: +def formation_table_recap(formation: Formation, fmt="html") -> Response: """Table recapitulant formation.""" - T = [] - formation = Formation.query.get_or_404(formation_id) + rows = [] ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero) can_edit = current_user.has_permission(Permission.EditFormation) li = 0 for ue in ues: # L'UE - T.append( + rows.append( { "sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-", "_sem_order": f"{li:04d}", @@ -83,7 +82,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response: for mod in modules: nb_moduleimpls = mod.modimpls.count() # le module (ou ressource ou sae) - T.append( + rows.append( { "sem": ( f"S{mod.semestre_id}" @@ -152,7 +151,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response: tab = GenTable( columns_ids=columns_ids, - rows=T, + rows=rows, titles=titles, origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}", caption=title, @@ -168,7 +167,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response: }" """, html_with_td_classes=True, - base_url=f"{request.base_url}?formation_id={formation_id}", + base_url=f"{request.base_url}", page_title=title, html_title=f"

{title}

", pdf_title=title, @@ -192,7 +191,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire): formation_ids = {formsemestre.formation.id for formsemestre in formsemestres} for formation_id in formation_ids: formation = db.session.get(Formation, formation_id) - xls = formation_table_recap(formation_id, fmt="xlsx").data + xls = formation_table_recap(formation, fmt="xlsx").data filename = ( scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX ) diff --git a/app/views/__init__.py b/app/views/__init__.py index df78e7cf..152a725c 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -140,10 +140,15 @@ class ScoData: return sco_formsemestre_status.formsemestre_status_menubar(self.formsemestre) +# Ajout des routes +from app.but import bulletin_but_court # ne pas enlever: ajoute des routes ! +from app.but import jury_dut120 # ne pas enlever: ajoute des routes ! +from app.pe import pe_view # ne pas enlever, ajoute des routes ! from app.views import ( absences, assiduites, but_formation, + jury_validations, notes_formsemestre, notes, pn_modules, diff --git a/app/views/jury_validations.py b/app/views/jury_validations.py new file mode 100644 index 00000000..ccef8946 --- /dev/null +++ b/app/views/jury_validations.py @@ -0,0 +1,904 @@ +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +""" +Vues sur les jurys et validations + +Emmanuel Viennet, 2024 +""" + +import datetime +import flask +from flask import flash, g, redirect, render_template, request, url_for +from flask_login import current_user + +from app import log +from app.but import ( + cursus_but, + jury_edit_manual, + jury_but, + jury_but_validation_auto, + jury_but_view, +) +from app.but.forms import jury_but_forms +from app.comp import jury +from app.decorators import ( + scodoc, + scodoc7func, + permission_required, +) +from app.models import ( + Evaluation, + Formation, + FormSemestre, + FormSemestreInscription, + Identite, + ScolarAutorisationInscription, + ScolarNews, + ScoDocSiteConfig, +) +from app.scodoc import ( + html_sco_header, + sco_bulletins_json, + sco_cache, + sco_formsemestre_exterieurs, + sco_formsemestre_validation, + sco_preferences, +) +from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ( + ScoPermissionDenied, + ScoValueError, +) +from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_pv_dict import descr_autorisations + +# from app.scodoc.TrivialFormulator import TrivialFormulator +from app.views import notes_bp as bp +from app.views import ScoData + + +# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES + + +@bp.route("/formsemestre_validation_etud_form") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_validation_etud_form( + formsemestre_id, + etudid=None, + etud_index=None, + check=0, + desturl="", + sortcol=None, +): + "Formulaire choix jury pour un étudiant" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + read_only = not formsemestre.can_edit_jury() + if formsemestre.formation.is_apc(): + return redirect( + url_for( + "notes.formsemestre_validation_but", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) + ) + return sco_formsemestre_validation.formsemestre_validation_etud_form( + formsemestre_id, + etudid=etudid, + etud_index=etud_index, + check=check, + read_only=read_only, + dest_url=desturl, + sortcol=sortcol, + ) + + +@bp.route("/formsemestre_validation_etud") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_validation_etud( + formsemestre_id, + etudid=None, + codechoice=None, + desturl="", + sortcol=None, +): + "Enregistre choix jury pour un étudiant" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + + return sco_formsemestre_validation.formsemestre_validation_etud( + formsemestre_id, + etudid=etudid, + codechoice=codechoice, + desturl=desturl, + sortcol=sortcol, + ) + + +@bp.route("/formsemestre_validation_etud_manu") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_validation_etud_manu( + formsemestre_id, + etudid=None, + code_etat="", + new_code_prev="", + devenir="", + assidu=False, + desturl="", + sortcol=None, +): + "Enregistre choix jury pour un étudiant" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + + return sco_formsemestre_validation.formsemestre_validation_etud_manu( + formsemestre_id, + etudid=etudid, + code_etat=code_etat, + new_code_prev=new_code_prev, + devenir=devenir, + assidu=assidu, + desturl=desturl, + sortcol=sortcol, + ) + + +# --- Jurys BUT +@bp.route( + "/formsemestre_validation_but//", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_validation_but( + formsemestre_id: int, + etudid: int, +): + "Form. saisie décision jury semestre BUT" + formsemestre: FormSemestre = FormSemestre.query.filter_by( + id=formsemestre_id, dept_id=g.scodoc_dept_id + ).first_or_404() + # la route ne donne pas le type d'etudid pour pouvoir construire des URLs + # provisoires avec NEXT et PREV + try: + etudid = int(etudid) + except ValueError as exc: + raise ScoValueError("adresse invalide") from exc + etud = Identite.get_etud(etudid) + nb_etuds = formsemestre.etuds.count() + read_only = not formsemestre.can_edit_jury() + can_erase = current_user.has_permission(Permission.EtudInscrit) + # --- Navigation + prev_lnk = ( + f"""{scu.EMO_PREV_ARROW} précédent + """ + if nb_etuds > 1 + else "" + ) + next_lnk = ( + f"""suivant {scu.EMO_NEXT_ARROW} + """ + if nb_etuds > 1 + else "" + ) + navigation_div = f""" +
+ + + +
+ """ + + H = [ + html_sco_header.sco_header( + page_title=f"Validation BUT S{formsemestre.semestre_id}", + formsemestre_id=formsemestre_id, + etudid=etudid, + cssstyles=[ + "css/jury_but.css", + "css/cursus_but.css", + ], + javascripts=("js/jury_but.js",), + ), + """
+ """, + ] + + if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT: + return ( + "\n".join(H) + + f""" +
+
+
+
Jury BUT
+
{etud.html_link_fiche()}
+
+ +
+
Impossible de statuer sur cet étudiant: + il est démissionnaire ou défaillant (voir sa fiche) +
+
+ {navigation_div} +
+ """ + + html_sco_header.sco_footer() + ) + + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + has_notes_en_attente = deca.has_notes_en_attente() + evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( + formsemestre, etud + ) + if has_notes_en_attente or evaluations_a_debloquer: + read_only = True + if request.method == "POST": + if not read_only: + deca.record_form(request.form) + ScolarNews.add( + typ=ScolarNews.NEWS_JURY, + obj=formsemestre.id, + text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""", + url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ), + ) + flash("codes enregistrés") + return flask.redirect( + url_for( + "notes.formsemestre_validation_but", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) + ) + + warning = "" + if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): + warning += f"""
Attention: {len(deca.niveaux_competences)} + niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.
""" + if (deca.parcour is None) and len(formsemestre.parcours) > 0: + warning += ( + """
L'étudiant n'est pas inscrit à un parcours.
""" + ) + if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12): + # encore loin de la fin du semestre de départ de ce jury ? + warning += f"""
Le semestre S{formsemestre.semestre_id} + terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)} : + êtes-vous certain de vouloir enregistrer une décision de jury ? +
""" + + if deca.formsemestre_impair: + inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id) + if (not inscription) or inscription.etat != scu.INSCRIT: + etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") + warning += f"""
{etat_ins} + en S{deca.formsemestre_impair.semestre_id}
""" + + if deca.formsemestre_pair: + inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id) + if (not inscription) or inscription.etat != scu.INSCRIT: + etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") + warning += f"""
{etat_ins} + en S{deca.formsemestre_pair.semestre_id}
""" + + if has_notes_en_attente: + warning += f"""
{etud.html_link_fiche() + } a des notes en ATTente dans les modules suivants. + Vous devez régler cela avant de statuer en jury ! +
" + if evaluations_a_debloquer: + links_evals = [ + f"""{e.description} en {e.moduleimpl.module.code}""" + for e in evaluations_a_debloquer + ] + warning += f"""
Impossible de statuer sur cet étudiant: + il a des notes dans des évaluations qui seront débloquées plus tard: + voir {", ".join(links_evals)} + """ + + if warning: + warning = f"""
{warning}
""" + H.append( + f""" +
+
+
+
Jury BUT{deca.annee_but} + - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} + - {deca.annee_scolaire_str()}
+
{etud.html_link_fiche()}
+
+ +
+ {warning} +
+ +
+ """ + ) + + H.append(jury_but_view.show_etud(deca, read_only=read_only)) + + autorisations_idx = deca.get_autorisations_passage() + div_autorisations_passage = ( + f""" +
+ Autorisé à passer en : + { ", ".join( ["S" + str(i) for i in autorisations_idx ] )} +
+ """ + if autorisations_idx + else """
+ pas d'autorisations de passage enregistrées. +
+ """ + ) + H.append(div_autorisations_passage) + + if read_only: + H.append( + f""" +
+ {"Vous n'avez pas la permission de modifier ces décisions." + if formsemestre.etat + else "Semestre verrouillé."} + Les champs entourés en vert sont enregistrés. +
""" + ) + else: + erase_span = f""" + effacer des décisions de jury + + enregistrer des UEs antérieures + + décerner le DUT "120ECTS" + """ + H.append( + f"""
+ + permettre la saisie manuelles des codes + {"d'année et " if deca.jury_annuel else ""} + de niveaux. + Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année ! + + +
+ +
+ + {erase_span} +
+ """ + ) + H.append(navigation_div) + H.append("
") + + # Affichage cursus BUT + but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation) + H += [ + """
+
Niveaux de compétences enregistrés :
+ """, + render_template( + "but/cursus_etud.j2", + cursus=but_cursus, + scu=scu, + ), + "
", + ] + H.append( + render_template( + "but/documentation_codes_jury.j2", + nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") + or sco_preferences.get_preference("UnivName") + or "Apogée"}""", + codes=ScoDocSiteConfig.get_codes_apo_dict(), + ) + ) + H.append( + f"""
+ {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE capitalisées (note > 10) + lors d'une année précédente peuvent être prise en compte pour former + un RCUE (associé à un niveau de compétence du BUT). +
+ """ + ) + return "\n".join(H) + html_sco_header.sco_footer() + + +@bp.route( + "/formsemestre_validation_auto_but/", methods=["GET", "POST"] +) +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_validation_auto_but(formsemestre_id: int = None): + "Saisie automatique des décisions de jury BUT" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + if not formsemestre.formation.is_apc(): + raise ScoValueError( + "formsemestre_validation_auto_but est réservé aux formations APC" + ) + + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + form = jury_but_forms.FormSemestreValidationAutoBUTForm() + if request.method == "POST": + if not form.cancel.data: + nb_etud_modif, _ = ( + jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre) + ) + flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)") + return redirect( + url_for( + "notes.formsemestre_recapcomplet", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + mode_jury=1, + ) + ) + # Avertissement si formsemestre impair + formsemestres_suspects = {} + if formsemestre.semestre_id % 2: + _, decas = jury_but_validation_auto.formsemestre_validation_auto_but( + formsemestre, dry_run=True + ) + # regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués + formsemestres_suspects = { + deca.formsemestre_pair.id: deca.formsemestre_pair + for deca in decas + if deca.formsemestre_pair + and deca.formsemestre_pair.date_debut > formsemestre.date_debut + and not deca.formsemestre_pair.block_moyennes + } + + return render_template( + "but/formsemestre_validation_auto_but.j2", + form=form, + formsemestres_suspects=formsemestres_suspects, + sco=ScoData(formsemestre=formsemestre), + title="Calcul automatique jury BUT", + ) + + +@bp.route( + "/formsemestre_validate_previous_ue//", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_validate_previous_ue(formsemestre_id, etudid=None): + "Form. saisie UE validée hors ScoDoc" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + etud: Identite = ( + Identite.query.filter_by(id=etudid) + .join(FormSemestreInscription) + .filter_by(formsemestre_id=formsemestre_id) + .first_or_404() + ) + + return sco_formsemestre_validation.formsemestre_validate_previous_ue( + formsemestre, etud + ) + + +@bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None): + "Form. edition UE semestre extérieur" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + + return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations( + formsemestre_id, etudid + ) + + +@bp.route("/formsemestre_validation_auto") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_validation_auto(formsemestre_id): + "Formulaire saisie automatisee des decisions d'un semestre" + 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( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + + if formsemestre.formation.is_apc(): + return redirect( + url_for( + "notes.formsemestre_validation_auto_but", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) + ) + return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id) + + +@bp.route("/do_formsemestre_validation_auto") +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def do_formsemestre_validation_auto(formsemestre_id): + "Formulaire saisie automatisee des decisions d'un semestre" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + + return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id) + + +@bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func +def formsemestre_validation_suppress_etud( + formsemestre_id, etudid, dialog_confirmed=False +): + """Suppression des décisions de jury pour un étudiant.""" + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) + if not formsemestre.can_edit_jury(): + raise ScoPermissionDenied( + dest_url=url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + etud = Identite.get_etud(etudid) + if formsemestre.formation.is_apc(): + next_url = url_for( + "scolar.fiche_etud", + scodoc_dept=g.scodoc_dept, + etudid=etudid, + ) + else: + next_url = url_for( + "notes.formsemestre_validation_etud_form", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) + if not dialog_confirmed: + d = sco_bulletins_json.dict_decision_jury( + etud, formsemestre, with_decisions=True + ) + + descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])] + dec_annee = d.get("decision_annee") + if dec_annee: + descr_annee = dec_annee.get("code", "-") + else: + descr_annee = "-" + + existing = f""" +
    +
  • Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}
  • +
  • Année BUT: {descr_annee}
  • +
  • UEs : {", ".join(descr_ues)}
  • +
  • RCUEs: {len(d.get("decision_rcue", []))} décisions
  • +
  • Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id, + etudid=etudid))} +
+ """ + return scu.confirm_dialog( + f"""

Confirmer la suppression des décisions du semestre + {formsemestre.titre_mois()} pour {etud.nomprenom} +

+

Cette opération est irréversible.

+
+ {existing} +
+ """, + OK="Supprimer", + dest_url="", + cancel_url=next_url, + parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, + ) + + sco_formsemestre_validation.formsemestre_validation_suppress_etud( + formsemestre_id, etudid + ) + flash("Décisions supprimées") + return flask.redirect(next_url) + + +@bp.route( + "/formsemestre_jury_erase/", + methods=["GET", "POST"], + defaults={"etudid": None}, +) +@bp.route( + "/formsemestre_jury_erase//", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.ScoView) +def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None): + """Supprime toutes les décisions de jury (classique ou BUT) pour cette année. + Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits. + En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année. + En classique, n'affecte que les décisions issues de ce formsemestre. + """ + only_one_sem = int(request.args.get("only_one_sem") or False) + 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( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) + ) + is_apc = formsemestre.formation.is_apc() + if etudid is None: + etud = None + etuds = formsemestre.get_inscrits(include_demdef=True) + dest_url = url_for( + "notes.formsemestre_recapcomplet", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + mode_jury=1, + ) + else: + etud = Identite.get_etud(etudid) + etuds = [etud] + endpoint = ( + "notes.formsemestre_validation_but" + if is_apc + else "notes.formsemestre_validation_etud_form" + ) + dest_url = url_for( + endpoint, + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + etudid=etudid, + ) + if request.method == "POST": + with sco_cache.DeferredSemCacheManager(): + for etud in etuds: + if is_apc: + deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) + deca.erase(only_one_sem=only_one_sem) + else: + sco_formsemestre_validation.formsemestre_validation_suppress_etud( + formsemestre.id, etud.id + ) + log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})") + flash( + ( + "décisions de jury du semestre effacées" + if (only_one_sem or is_apc) + else "décisions de jury des semestres de l'année BUT effacées" + ) + + f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}" + ) + return redirect(dest_url) + + return render_template( + "confirm_dialog.j2", + title=f"""Effacer les validations de jury { + ("de " + etud.nomprenom) + if etud + else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre") + } ?""", + explanation=( + ( + f"""Les validations d'UE et autorisations de passage + du semestre S{formsemestre.semestre_id} seront effacées.""" + if (only_one_sem or is_apc) + 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.

+ """ + + """ +

Efface aussi toutes les validations concernant l'année BUT de ce semestre, + même si elles ont été acquises ailleurs, ainsi que les validations de DUT en 120 ECTS + obtenues après BUT1/BUT2. +

+ """ + if is_apc + else "" + + """ +
Cette opération est irréversible ! + A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite. +
+ """ + ), + cancel_url=dest_url, + ) + + +@bp.route( + "/erase_decisions_annee_formation///", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.EtudInscrit) +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.fiche_etud", + scodoc_dept=g.scodoc_dept, + etudid=etud.id, + ) + ) + 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, + cancel_url=url_for( + "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id + ), + etud=etud, + formation=formation, + formsemestre_origine=formsemestre_origine, + validations=validations, + sco=ScoData(), + title=f"Effacer décisions de jury {etud.nom} - année {annee}", + ) + + +@bp.route( + "/jury_delete_manual/", + methods=["GET", "POST"], +) +@scodoc +@permission_required(Permission.EtudInscrit) +def jury_delete_manual(etudid: int): + """Efface toute les décisions d'une année pour cet étudiant""" + etud = Identite.get_etud(etudid) + return jury_edit_manual.jury_delete_manual(etud) diff --git a/app/views/notes.py b/app/views/notes.py index 482b6c48..806a607d 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -30,7 +30,6 @@ Module notes: issu de ScoDoc7 / ZNotes.py Emmanuel Viennet, 2021 """ -import datetime import html from operator import itemgetter import time @@ -40,24 +39,13 @@ from flask import flash, redirect, render_template, url_for from flask import g, request from flask_login import current_user -from app import db +from app import db, log, send_scodoc_alarm from app import models from app.auth.models import User -from app.but import ( - apc_edit_ue, - cursus_but, - jury_edit_manual, - jury_but, - jury_but_pv, - jury_but_validation_auto, - jury_but_view, -) -from app.but import bulletin_but_court # ne pas enlever: ajoute des routes ! -from app.but import jury_dut120 # ne pas enlever: ajoute des routes ! -from app.but.forms import jury_but_forms +from app.but import apc_edit_ue, jury_but_pv -from app.comp import jury, res_sem +from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import ( ApcNiveau, @@ -72,10 +60,7 @@ from app.models import ( Identite, Module, ModuleImpl, - ScolarAutorisationInscription, - ScolarNews, Scolog, - ScoDocSiteConfig, UniteEns, ) from app.scodoc.sco_exceptions import ScoFormationConflict, ScoPermissionDenied @@ -90,9 +75,7 @@ from app.decorators import ( # --------------- -from app.pe import pe_view # ne pas enlever, ajoute des vues -from app.scodoc import sco_bulletins_json, sco_utils as scu -from app import log, send_scodoc_alarm +from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import ( AccessDenied, @@ -131,7 +114,6 @@ from app.scodoc import ( sco_formsemestre_exterieurs, sco_formsemestre_inscriptions, sco_formsemestre_status, - sco_formsemestre_validation, sco_groups_view, sco_inscr_passage, sco_liste_notes, @@ -157,7 +139,6 @@ from app.scodoc import ( sco_users, ) from app.scodoc.gen_tables import GenTable -from app.scodoc.sco_pv_dict import descr_autorisations from app.scodoc.sco_permissions import Permission from app.scodoc.TrivialFormulator import TrivialFormulator from app.views import ScoData @@ -560,12 +541,14 @@ sco_publish( ) -@bp.route("/formation_table_recap") +@bp.route("/formation_table_recap/") @scodoc @permission_required(Permission.ScoView) -@scodoc7func -def formation_table_recap(formation_id, fmt="html"): - return sco_formation_recap.formation_table_recap(formation_id, fmt=fmt) +def formation_table_recap(formation_id: int): + "Tableau récap. de la formation" + formation = Formation.get_formation(formation_id) + fmt = request.args.get("fmt", "html") + return sco_formation_recap.formation_table_recap(formation, fmt=fmt) sco_publish( @@ -829,9 +812,9 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.EditFormation) @permission_required(Permission.EditFormation) def ue_clone(): """Clone existing UE""" - ue_id = int(request.form.get("ue_id")) - ue = UniteEns.query.get_or_404(ue_id) - ue2 = ue.clone() + ue_id = request.form.get("ue_id") + ue = UniteEns.get_ue(ue_id) + _ = ue.clone() db.session.commit() flash(f"UE {ue.acronyme} dupliquée") return flask.redirect( @@ -2205,520 +2188,6 @@ def appreciation_add_form( return flask.redirect(bul_url) -# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES - - -@bp.route("/formsemestre_validation_etud_form") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_validation_etud_form( - formsemestre_id, - etudid=None, - etud_index=None, - check=0, - desturl="", - sortcol=None, -): - "Formulaire choix jury pour un étudiant" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - read_only = not formsemestre.can_edit_jury() - if formsemestre.formation.is_apc(): - return redirect( - url_for( - "notes.formsemestre_validation_but", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - etudid=etudid, - ) - ) - return sco_formsemestre_validation.formsemestre_validation_etud_form( - formsemestre_id, - etudid=etudid, - etud_index=etud_index, - check=check, - read_only=read_only, - dest_url=desturl, - sortcol=sortcol, - ) - - -@bp.route("/formsemestre_validation_etud") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_validation_etud( - formsemestre_id, - etudid=None, - codechoice=None, - desturl="", - sortcol=None, -): - "Enregistre choix jury pour un étudiant" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - - return sco_formsemestre_validation.formsemestre_validation_etud( - formsemestre_id, - etudid=etudid, - codechoice=codechoice, - desturl=desturl, - sortcol=sortcol, - ) - - -@bp.route("/formsemestre_validation_etud_manu") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_validation_etud_manu( - formsemestre_id, - etudid=None, - code_etat="", - new_code_prev="", - devenir="", - assidu=False, - desturl="", - sortcol=None, -): - "Enregistre choix jury pour un étudiant" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - - return sco_formsemestre_validation.formsemestre_validation_etud_manu( - formsemestre_id, - etudid=etudid, - code_etat=code_etat, - new_code_prev=new_code_prev, - devenir=devenir, - assidu=assidu, - desturl=desturl, - sortcol=sortcol, - ) - - -# --- Jurys BUT -@bp.route( - "/formsemestre_validation_but//", - methods=["GET", "POST"], -) -@scodoc -@permission_required(Permission.ScoView) -def formsemestre_validation_but( - formsemestre_id: int, - etudid: int, -): - "Form. saisie décision jury semestre BUT" - formsemestre: FormSemestre = FormSemestre.query.filter_by( - id=formsemestre_id, dept_id=g.scodoc_dept_id - ).first_or_404() - # la route ne donne pas le type d'etudid pour pouvoir construire des URLs - # provisoires avec NEXT et PREV - try: - etudid = int(etudid) - except ValueError as exc: - raise ScoValueError("adresse invalide") from exc - etud = Identite.get_etud(etudid) - nb_etuds = formsemestre.etuds.count() - read_only = not formsemestre.can_edit_jury() - can_erase = current_user.has_permission(Permission.EtudInscrit) - # --- Navigation - prev_lnk = ( - f"""{scu.EMO_PREV_ARROW} précédent - """ - if nb_etuds > 1 - else "" - ) - next_lnk = ( - f"""suivant {scu.EMO_NEXT_ARROW} - """ - if nb_etuds > 1 - else "" - ) - navigation_div = f""" -
- - - -
- """ - - H = [ - html_sco_header.sco_header( - page_title=f"Validation BUT S{formsemestre.semestre_id}", - formsemestre_id=formsemestre_id, - etudid=etudid, - cssstyles=[ - "css/jury_but.css", - "css/cursus_but.css", - ], - javascripts=("js/jury_but.js",), - ), - """
- """, - ] - - if formsemestre.etuds_inscriptions[etudid].etat != scu.INSCRIT: - return ( - "\n".join(H) - + f""" -
-
-
-
Jury BUT
-
{etud.html_link_fiche()}
-
- -
-
Impossible de statuer sur cet étudiant: - il est démissionnaire ou défaillant (voir sa fiche) -
-
- {navigation_div} -
- """ - + html_sco_header.sco_footer() - ) - - deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - has_notes_en_attente = deca.has_notes_en_attente() - evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( - formsemestre, etud - ) - if has_notes_en_attente or evaluations_a_debloquer: - read_only = True - if request.method == "POST": - if not read_only: - deca.record_form(request.form) - ScolarNews.add( - typ=ScolarNews.NEWS_JURY, - obj=formsemestre.id, - text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""", - url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - ), - ) - flash("codes enregistrés") - return flask.redirect( - url_for( - "notes.formsemestre_validation_but", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - etudid=etudid, - ) - ) - - warning = "" - if len(deca.niveaux_competences) != len(deca.decisions_rcue_by_niveau): - warning += f"""
Attention: {len(deca.niveaux_competences)} - niveaux mais {len(deca.decisions_rcue_by_niveau)} regroupements RCUE.
""" - if (deca.parcour is None) and len(formsemestre.parcours) > 0: - warning += ( - """
L'étudiant n'est pas inscrit à un parcours.
""" - ) - if formsemestre.date_fin - datetime.date.today() > datetime.timedelta(days=12): - # encore loin de la fin du semestre de départ de ce jury ? - warning += f"""
Le semestre S{formsemestre.semestre_id} - terminera le {formsemestre.date_fin.strftime(scu.DATE_FMT)} : - êtes-vous certain de vouloir enregistrer une décision de jury ? -
""" - - if deca.formsemestre_impair: - inscription = deca.formsemestre_impair.etuds_inscriptions.get(etud.id) - if (not inscription) or inscription.etat != scu.INSCRIT: - etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") - warning += f"""
{etat_ins} - en S{deca.formsemestre_impair.semestre_id}
""" - - if deca.formsemestre_pair: - inscription = deca.formsemestre_pair.etuds_inscriptions.get(etud.id) - if (not inscription) or inscription.etat != scu.INSCRIT: - etat_ins = scu.ETATS_INSCRIPTION.get(inscription.etat, "inconnu?") - warning += f"""
{etat_ins} - en S{deca.formsemestre_pair.semestre_id}
""" - - if has_notes_en_attente: - warning += f"""
{etud.html_link_fiche() - } a des notes en ATTente dans les modules suivants. - Vous devez régler cela avant de statuer en jury ! -
" - if evaluations_a_debloquer: - links_evals = [ - f"""{e.description} en {e.moduleimpl.module.code}""" - for e in evaluations_a_debloquer - ] - warning += f"""
Impossible de statuer sur cet étudiant: - il a des notes dans des évaluations qui seront débloquées plus tard: - voir {", ".join(links_evals)} - """ - - if warning: - warning = f"""
{warning}
""" - H.append( - f""" -
-
-
-
Jury BUT{deca.annee_but} - - Parcours {(deca.parcour.libelle if deca.parcour else False) or "non spécifié"} - - {deca.annee_scolaire_str()}
-
{etud.html_link_fiche()}
-
- -
- {warning} -
- -
- """ - ) - - H.append(jury_but_view.show_etud(deca, read_only=read_only)) - - autorisations_idx = deca.get_autorisations_passage() - div_autorisations_passage = ( - f""" -
- Autorisé à passer en : - { ", ".join( ["S" + str(i) for i in autorisations_idx ] )} -
- """ - if autorisations_idx - else """
- pas d'autorisations de passage enregistrées. -
- """ - ) - H.append(div_autorisations_passage) - - if read_only: - H.append( - f""" -
- {"Vous n'avez pas la permission de modifier ces décisions." - if formsemestre.etat - else "Semestre verrouillé."} - Les champs entourés en vert sont enregistrés. -
""" - ) - else: - erase_span = f""" - effacer des décisions de jury - - enregistrer des UEs antérieures - - décerner le DUT "120ECTS" - """ - H.append( - f"""
- - permettre la saisie manuelles des codes - {"d'année et " if deca.jury_annuel else ""} - de niveaux. - Dans ce cas, assurez-vous de la cohérence entre les codes d'UE/RCUE/Année ! - - -
- -
- - {erase_span} -
- """ - ) - H.append(navigation_div) - H.append("
") - - # Affichage cursus BUT - but_cursus = cursus_but.EtudCursusBUT(etud, deca.formsemestre.formation) - H += [ - """
-
Niveaux de compétences enregistrés :
- """, - render_template( - "but/cursus_etud.j2", - cursus=but_cursus, - scu=scu, - ), - "
", - ] - H.append( - render_template( - "but/documentation_codes_jury.j2", - nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") - or sco_preferences.get_preference("UnivName") - or "Apogée"}""", - codes=ScoDocSiteConfig.get_codes_apo_dict(), - ) - ) - H.append( - f"""
- {scu.EMO_WARNING} Rappel: pour les redoublants, seules les UE capitalisées (note > 10) - lors d'une année précédente peuvent être prise en compte pour former - un RCUE (associé à un niveau de compétence du BUT). -
- """ - ) - return "\n".join(H) + html_sco_header.sco_footer() - - -@bp.route( - "/formsemestre_validation_auto_but/", methods=["GET", "POST"] -) -@scodoc -@permission_required(Permission.ScoView) -def formsemestre_validation_auto_but(formsemestre_id: int = None): - "Saisie automatique des décisions de jury BUT" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - if not formsemestre.formation.is_apc(): - raise ScoValueError( - "formsemestre_validation_auto_but est réservé aux formations APC" - ) - - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - form = jury_but_forms.FormSemestreValidationAutoBUTForm() - if request.method == "POST": - if not form.cancel.data: - nb_etud_modif, _ = ( - jury_but_validation_auto.formsemestre_validation_auto_but(formsemestre) - ) - flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)") - return redirect( - url_for( - "notes.formsemestre_recapcomplet", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - mode_jury=1, - ) - ) - # Avertissement si formsemestre impair - formsemestres_suspects = {} - if formsemestre.semestre_id % 2: - _, decas = jury_but_validation_auto.formsemestre_validation_auto_but( - formsemestre, dry_run=True - ) - # regarde si il y a des semestres pairs postérieurs qui ne soient pas bloqués - formsemestres_suspects = { - deca.formsemestre_pair.id: deca.formsemestre_pair - for deca in decas - if deca.formsemestre_pair - and deca.formsemestre_pair.date_debut > formsemestre.date_debut - and not deca.formsemestre_pair.block_moyennes - } - - return render_template( - "but/formsemestre_validation_auto_but.j2", - form=form, - formsemestres_suspects=formsemestres_suspects, - sco=ScoData(formsemestre=formsemestre), - title="Calcul automatique jury BUT", - ) - - -@bp.route( - "/formsemestre_validate_previous_ue//", - methods=["GET", "POST"], -) -@scodoc -@permission_required(Permission.ScoView) -def formsemestre_validate_previous_ue(formsemestre_id, etudid=None): - "Form. saisie UE validée hors ScoDoc" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - etud: Identite = ( - Identite.query.filter_by(id=etudid) - .join(FormSemestreInscription) - .filter_by(formsemestre_id=formsemestre_id) - .first_or_404() - ) - - return sco_formsemestre_validation.formsemestre_validate_previous_ue( - formsemestre, etud - ) - - sco_publish( "/formsemestre_ext_create_form", sco_formsemestre_exterieurs.formsemestre_ext_create_form, @@ -2727,150 +2196,6 @@ sco_publish( ) -@bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"]) -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid=None): - "Form. edition UE semestre extérieur" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - - return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations( - formsemestre_id, etudid - ) - - -@bp.route("/formsemestre_validation_auto") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_validation_auto(formsemestre_id): - "Formulaire saisie automatisee des decisions d'un semestre" - 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( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - - if formsemestre.formation.is_apc(): - return redirect( - url_for( - "notes.formsemestre_validation_auto_but", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, - ) - ) - return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id) - - -@bp.route("/do_formsemestre_validation_auto") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def do_formsemestre_validation_auto(formsemestre_id): - "Formulaire saisie automatisee des decisions d'un semestre" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - - return sco_formsemestre_validation.do_formsemestre_validation_auto(formsemestre_id) - - -@bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"]) -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_validation_suppress_etud( - formsemestre_id, etudid, dialog_confirmed=False -): - """Suppression des décisions de jury pour un étudiant.""" - formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) - if not formsemestre.can_edit_jury(): - raise ScoPermissionDenied( - dest_url=url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - etud = Identite.get_etud(etudid) - if formsemestre.formation.is_apc(): - next_url = url_for( - "scolar.fiche_etud", - scodoc_dept=g.scodoc_dept, - etudid=etudid, - ) - else: - next_url = url_for( - "notes.formsemestre_validation_etud_form", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - etudid=etudid, - ) - if not dialog_confirmed: - d = sco_bulletins_json.dict_decision_jury( - etud, formsemestre, with_decisions=True - ) - - descr_ues = [f"{u['acronyme']}: {u['code']}" for u in d.get("decision_ue", [])] - dec_annee = d.get("decision_annee") - if dec_annee: - descr_annee = dec_annee.get("code", "-") - else: - descr_annee = "-" - - existing = f""" -
    -
  • Semestre : {d.get("decision", {"code":"-"})['code'] or "-"}
  • -
  • Année BUT: {descr_annee}
  • -
  • UEs : {", ".join(descr_ues)}
  • -
  • RCUEs: {len(d.get("decision_rcue", []))} décisions
  • -
  • Autorisations: {descr_autorisations(ScolarAutorisationInscription.query.filter_by(origin_formsemestre_id=formsemestre_id, - etudid=etudid))} -
- """ - return scu.confirm_dialog( - f"""

Confirmer la suppression des décisions du semestre - {formsemestre.titre_mois()} pour {etud.nomprenom} -

-

Cette opération est irréversible.

-
- {existing} -
- """, - OK="Supprimer", - dest_url="", - cancel_url=next_url, - parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, - ) - - sco_formsemestre_validation.formsemestre_validation_suppress_etud( - formsemestre_id, etudid - ) - flash("Décisions supprimées") - return flask.redirect(next_url) - - # ------------- PV de JURY et archives sco_publish( "/formsemestre_pvjury", sco_pv_forms.formsemestre_pvjury, Permission.ScoView @@ -2878,192 +2203,6 @@ sco_publish( sco_publish("/pvjury_page_but", jury_but_pv.pvjury_page_but, Permission.ScoView) - -@bp.route("/formsemestre_saisie_jury") -@scodoc -@permission_required(Permission.ScoView) -@scodoc7func -def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None): - """Page de saisie: liste des étudiants et lien vers page jury - sinon, redirect vers page recap en mode jury - """ - return redirect( - url_for( - "notes.formsemestre_recapcomplet", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - mode_jury=1, - ) - ) - - -@bp.route( - "/formsemestre_jury_erase/", - methods=["GET", "POST"], - defaults={"etudid": None}, -) -@bp.route( - "/formsemestre_jury_erase//", - methods=["GET", "POST"], -) -@scodoc -@permission_required(Permission.ScoView) -def formsemestre_jury_erase(formsemestre_id: int, etudid: int = None): - """Supprime toutes les décisions de jury (classique ou BUT) pour cette année. - Si l'étudiant n'est pas spécifié, efface les décisions de tous les inscrits. - En BUT, si only_one_sem n'efface que pour le formsemestre indiqué, pas les deux de l'année. - En classique, n'affecte que les décisions issues de ce formsemestre. - """ - only_one_sem = int(request.args.get("only_one_sem") or False) - 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( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - ) - ) - is_apc = formsemestre.formation.is_apc() - if etudid is None: - etud = None - etuds = formsemestre.get_inscrits(include_demdef=True) - dest_url = url_for( - "notes.formsemestre_recapcomplet", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - mode_jury=1, - ) - else: - etud = Identite.get_etud(etudid) - etuds = [etud] - endpoint = ( - "notes.formsemestre_validation_but" - if is_apc - else "notes.formsemestre_validation_etud_form" - ) - dest_url = url_for( - endpoint, - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre_id, - etudid=etudid, - ) - if request.method == "POST": - with sco_cache.DeferredSemCacheManager(): - for etud in etuds: - if is_apc: - deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) - deca.erase(only_one_sem=only_one_sem) - else: - sco_formsemestre_validation.formsemestre_validation_suppress_etud( - formsemestre.id, etud.id - ) - log(f"formsemestre_jury_erase({formsemestre_id}, {etud.id})") - flash( - ( - "décisions de jury du semestre effacées" - if (only_one_sem or is_apc) - else "décisions de jury des semestres de l'année BUT effacées" - ) - + f" pour {len(etuds)} étudiant{'s' if len(etuds) > 1 else ''}" - ) - return redirect(dest_url) - - return render_template( - "confirm_dialog.j2", - title=f"""Effacer les validations de jury { - ("de " + etud.nomprenom) - if etud - else ("des " + str(len(etuds)) + " étudiants inscrits dans ce semestre") - } ?""", - explanation=( - ( - f"""Les validations d'UE et autorisations de passage - du semestre S{formsemestre.semestre_id} seront effacées.""" - if (only_one_sem or is_apc) - 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.

- """ - + """ -

Efface aussi toutes les validations concernant l'année BUT de ce semestre, - même si elles ont été acquises ailleurs, ainsi que les validations de DUT en 120 ECTS - obtenues après BUT1/BUT2. -

- """ - if is_apc - else "" - + """ -
Cette opération est irréversible ! - A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite. -
- """ - ), - cancel_url=dest_url, - ) - - -@bp.route( - "/erase_decisions_annee_formation///", - methods=["GET", "POST"], -) -@scodoc -@permission_required(Permission.EtudInscrit) -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.fiche_etud", - scodoc_dept=g.scodoc_dept, - etudid=etud.id, - ) - ) - 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, - cancel_url=url_for( - "scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id - ), - etud=etud, - formation=formation, - formsemestre_origine=formsemestre_origine, - validations=validations, - sco=ScoData(), - title=f"Effacer décisions de jury {etud.nom} - année {annee}", - ) - - -@bp.route( - "/jury_delete_manual/", - methods=["GET", "POST"], -) -@scodoc -@permission_required(Permission.EtudInscrit) -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, @@ -3306,12 +2445,12 @@ def check_sem_integrity(formsemestre_id, fix=False): if not bad_ue and not bad_sem: H.append("

Aucun problème à signaler !

") else: - log("check_sem_integrity: problem detected: formations_set=%s" % formations_set) + log(f"check_sem_integrity: problem detected: formations_set={formations_set}") if sem["formation_id"] in formations_set: formations_set.remove(sem["formation_id"]) if len(formations_set) == 1: if fix: - log("check_sem_integrity: trying to fix %s" % formsemestre_id) + log(f"check_sem_integrity: trying to fix {formsemestre_id}") formation_id = formations_set.pop() if sem["formation_id"] != formation_id: sem["formation_id"] = formation_id @@ -3319,11 +2458,11 @@ def check_sem_integrity(formsemestre_id, fix=False): H.append("""

Problème réparé: vérifiez

""") else: H.append( - """ + f"""

Problème détecté réparable: - réparer maintenant

+ réparer maintenant

""" - % (formsemestre_id,) ) else: H.append("""

Problème détecté !

""") diff --git a/misc/example-api-python2.py b/misc/example-api-python2.py deleted file mode 100644 index 03fd26be..00000000 --- a/misc/example-api-python2.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python -# -*- mode: python -*- -# -*- coding: utf-8 -*- - -"""Exemple connexion sur ScoDoc et utilisation de l'API - -Attention: cet exemple est en Python 2. -Voir example-api-1.py pour une version en Python3 plus moderne. -""" - -import urllib, urllib2 - -# A modifier pour votre serveur: -BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite" -USER = "XXX" -PASSWORD = "XXX" - -values = { - "__ac_name": USER, - "__ac_password": PASSWORD, -} - -# Configure memorisation des cookies: -opener = urllib2.build_opener(urllib2.HTTPCookieProcessor()) -urllib2.install_opener(opener) - -data = urllib.urlencode(values) - -req = urllib2.Request(BASEURL, data) # this is a POST http request -response = urllib2.urlopen(req) - -# --- Use API - -# Affiche la liste des formations en format XML -req = urllib2.Request(BASEURL + "/Notes/formation_list?fmt=xml") -response = urllib2.urlopen(req) -print response.read()[:100] # limite aux 100 premiers caracteres... - -# Recupere la liste de tous les semestres: -req = urllib2.Request(BASEURL + "/Notes/formsemestre_list?fmt=json") # format json -response = urllib2.urlopen(req) -js_data = response.read() - -# Plus amusant: va retrouver le bulletin de notes du premier etudiant (au hasard donc) du premier semestre (au hasard aussi) -try: - import json # Attention: ceci demande Python >= 2.6 -except: - import simplejson as json # python2.4 with simplejson installed - -data = json.loads(js_data) # decode la reponse JSON -if not data: - print "Aucun semestre !" -else: - formsemestre_id = str(data[0]["formsemestre_id"]) - # Obtient la liste des groupes: - req = urllib2.Request( - BASEURL - + "/Notes/formsemestre_partition_list?fmt=json&formsemestre_id=" - + str(formsemestre_id) - ) # format json - response = urllib2.urlopen(req) - js_data = response.read() - data = json.loads(js_data) - group_id = data[0]["group"][0][ - "group_id" - ] # premier groupe (normalement existe toujours) - # Liste les étudiants de ce groupe: - req = urllib2.Request( - BASEURL + "/Notes/group_list?fmt=json&with_codes=1&group_id=" + str(group_id) - ) # format json - response = urllib2.urlopen(req) - js_data = response.read() - data = json.loads(js_data) - # Le code du premier étudiant: - if not data: - print ("pas d'etudiants dans ce semestre !") - else: - etudid = data[0]["etudid"] - # Récupère bulletin de notes: - req = urllib2.Request( - BASEURL - + "/Notes/formsemestre_bulletinetud?formsemestre_id=" - + str(formsemestre_id) - + "&etudid=" - + str(etudid) - + "&fmt=xml" - ) # format XML ici ! - response = urllib2.urlopen(req) - xml_bulletin = response.read() - print "----- Bulletin de notes en XML:" - print xml_bulletin - # Récupère la moyenne générale: - import xml.dom.minidom - - doc = xml.dom.minidom.parseString(xml_bulletin) - moy = doc.getElementsByTagName("note")[0].getAttribute( - "value" - ) # une chaine unicode - print "\nMoyenne generale: ", moy