From 512b38665f372f0b721e0b217d3767b0dbb16ada Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 4 Jul 2022 23:50:55 +0200 Subject: [PATCH 1/8] Jury BUT: traitement des DEF et DEM. Close #426 --- app/but/jury_but.py | 78 ++++++++++++++++++++++++++++------- app/but/jury_but_recap.py | 4 +- app/models/but_validations.py | 14 ++++++- sco_version.py | 2 +- 4 files changed, 78 insertions(+), 20 deletions(-) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 0a7660345f..397bd05f59 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -68,7 +68,7 @@ from flask import g, url_for from app import db from app import log from app.comp.res_but import ResultatsSemestreBUT -from app.comp import res_sem +from app.comp import inscr_mod, res_sem from app.models import formsemestre from app.models.but_refcomp import ( @@ -219,15 +219,16 @@ class DecisionsProposeesAnnee(DecisionsProposees): "le 1er semestre de l'année scolaire considérée (S1, S3, S5)" self.formsemestre_pair = formsemestre_pair "le second formsemestre de la même année scolaire (S2, S4, S6)" - self.annee_but = ( - (formsemestre_impair.semestre_id + 1) // 2 - if formsemestre_impair - else (formsemestre_pair.semestre_id + 1) // 2 - ) + formsemestre_last = formsemestre_pair or formsemestre_impair + "le formsemestre le plus avancé dans cette année" + + self.annee_but = (formsemestre_last.semestre_id + 1) // 2 "le rang de l'année dans le BUT: 1, 2, 3" assert self.annee_but in (1, 2, 3) self.rcues_annee = [] "RCUEs de l'année" + self.inscription_etat = etud.inscription_etat(formsemestre_last.id) + if self.formsemestre_impair is not None: self.validation = ApcValidationAnnee.query.filter_by( etudid=self.etud.id, @@ -255,13 +256,17 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.ues_impair, self.ues_pair = self.compute_ues_annee() # pylint: disable=all self.decisions_ues = { - ue.id: DecisionsProposeesUE(etud, formsemestre_impair, ue) + ue.id: DecisionsProposeesUE( + etud, formsemestre_impair, ue, self.inscription_etat + ) for ue in self.ues_impair } "{ue_id : DecisionsProposeesUE} pour toutes les UE de l'année" self.decisions_ues.update( { - ue.id: DecisionsProposeesUE(etud, formsemestre_pair, ue) + ue.id: DecisionsProposeesUE( + etud, formsemestre_pair, ue, self.inscription_etat + ) for ue in self.ues_pair } ) @@ -291,8 +296,10 @@ class DecisionsProposeesAnnee(DecisionsProposees): [rcue for rcue in rcues_avec_niveau if not rcue.est_suffisant()] ) "le nb de comp. sous la barre de 8/20" - # année ADM si toutes RCUE validées (sinon PASD) - self.admis = self.nb_validables == self.nb_competences + # année ADM si toutes RCUE validées (sinon PASD) et non DEM ou DEF + self.admis = (self.nb_validables == self.nb_competences) and ( + self.inscription_etat == scu.INSCRIT + ) "vrai si l'année est réussie, tous niveaux validables" self.valide_moitie_rcue = self.nb_validables > (self.nb_competences // 2) # Peut passer si plus de la moitié validables et tous > 8 @@ -310,6 +317,19 @@ class DecisionsProposeesAnnee(DecisionsProposees): if self.admis: self.codes = [sco_codes.ADM] + self.codes self.explanation = expl_rcues + elif self.inscription_etat != scu.INSCRIT: + self.codes = [ + sco_codes.DEM + if self.inscription_etat == scu.DEMISSION + else sco_codes.DEF, + # propose aussi d'autres codes, au cas où... + sco_codes.DEM + if self.inscription_etat != scu.DEMISSION + else sco_codes.DEF, + sco_codes.ABAN, + sco_codes.ABL, + sco_codes.EXCLU, + ] elif self.passage_de_droit: self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes self.explanation = expl_rcues @@ -482,6 +502,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): ue_impair, self.formsemestre_pair, ue_pair, + self.inscription_etat, ) ues_impair_sans_rcue.discard(ue_impair.id) break @@ -509,7 +530,7 @@ class DecisionsProposeesAnnee(DecisionsProposees): rcue = rc break if rcue is not None: - dec_rcue = DecisionsProposeesRCUE(self, rcue) + dec_rcue = DecisionsProposeesRCUE(self, rcue, self.inscription_etat) rc_niveaux.append((dec_rcue, niveau.id)) # prévient les UE concernées :-) self.decisions_ues[dec_rcue.rcue.ue_1.id].set_rcue(dec_rcue.rcue) @@ -724,14 +745,26 @@ class DecisionsProposeesRCUE(DecisionsProposees): ] def __init__( - self, dec_prop_annee: DecisionsProposeesAnnee, rcue: RegroupementCoherentUE + self, + dec_prop_annee: DecisionsProposeesAnnee, + rcue: RegroupementCoherentUE, + inscription_etat: str = scu.INSCRIT, ): super().__init__(etud=dec_prop_annee.etud) self.rcue = rcue if rcue is None: # RCUE non dispo, eg un seul semestre self.codes = [] return + self.inscription_etat = inscription_etat + "inscription: I, DEM, DEF" self.parcour = dec_prop_annee.parcour + if inscription_etat != scu.INSCRIT: + self.validation = None # cache toute validation + self.explanation = "non incrit (dem. ou déf.)" + self.codes = [ + sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF + ] + return self.validation = rcue.query_validations().first() if self.validation is not None: self.code_valide = self.validation.code @@ -828,12 +861,27 @@ class DecisionsProposeesUE(DecisionsProposees): etud: Identite, formsemestre: FormSemestre, ue: UniteEns, + inscription_etat: str = scu.INSCRIT, ): super().__init__(etud=etud) self.formsemestre = formsemestre self.ue: UniteEns = ue self.rcue: RegroupementCoherentUE = None "Le rcu auquel est rattaché cette UE, ou None" + self.inscription_etat = inscription_etat + "inscription: I, DEM, DEF" + if ue.type == sco_codes.UE_SPORT: + self.explanation = "UE bonus, pas de décision de jury" + self.codes = [] # aucun code proposé + return + if inscription_etat != scu.INSCRIT: + self.validation = None # cache toute validation + self.explanation = "non incrit (dem. ou déf.)" + self.codes = [ + sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF + ] + self.moy_ue = "-" + return # Une UE peut être validée plusieurs fois en cas de redoublement (qu'elle soit capitalisée ou non) # mais ici on a restreint au formsemestre donc une seule (prend la première) self.validation = ScolarFormSemestreValidation.query.filter_by( @@ -841,10 +889,6 @@ class DecisionsProposeesUE(DecisionsProposees): ).first() if self.validation is not None: self.code_valide = self.validation.code - if ue.type == sco_codes.UE_SPORT: - self.explanation = "UE bonus, pas de décision de jury" - self.codes = [] # aucun code proposé - return # Moyenne de l'UE ? res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) @@ -863,6 +907,8 @@ class DecisionsProposeesUE(DecisionsProposees): def compute_codes(self): """Calcul des .codes attribuables et de l'explanation associée""" + if self.inscription_etat != scu.INSCRIT: + return if self.moy_ue > (sco_codes.ParcoursBUT.BARRE_MOY - sco_codes.NOTES_TOLERANCE): self.codes.insert(0, sco_codes.ADM) self.explanation = (f"Moyenne >= {sco_codes.ParcoursBUT.BARRE_MOY}/20",) diff --git a/app/but/jury_but_recap.py b/app/but/jury_but_recap.py index c2944d98ce..7d47cbe5e4 100644 --- a/app/but/jury_but_recap.py +++ b/app/but/jury_but_recap.py @@ -409,7 +409,9 @@ def get_table_jury_but( )}" class="stdlink"> {"voir" if read_only else ("modif." if deca.code_valide else "saisie")} décision - """, + """ + if deca.inscription_etat == scu.INSCRIT + else deca.inscription_etat, "col_lien_saisie_but", ) rows.append(row) diff --git a/app/models/but_validations.py b/app/models/but_validations.py index 6c2814424c..ebedadd715 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -16,6 +16,7 @@ from app.models.ues import UniteEns from app.models.formations import Formation from app.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours as sco_codes +from app.scodoc import sco_utils as scu class ApcValidationRCUE(db.Model): @@ -84,6 +85,7 @@ class RegroupementCoherentUE: ue_1: UniteEns, formsemestre_2: FormSemestre, ue_2: UniteEns, + inscription_etat: str, ): from app.comp import res_sem from app.comp.res_but import ResultatsSemestreBUT @@ -109,6 +111,11 @@ class RegroupementCoherentUE: "semestre pair" self.ue_2 = ue_2 # Stocke les moyennes d'UE + if inscription_etat != scu.INSCRIT: + self.moy_rcue = None + self.moy_ue_1 = self.moy_ue_2 = "-" + self.moy_ue_1_val = self.moy_ue_2_val = 0.0 + return res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1) if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]: self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id] @@ -201,8 +208,9 @@ class RegroupementCoherentUE: return None +# unused def find_rcues( - formsemestre: FormSemestre, ue: UniteEns, etud: Identite + formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str ) -> list[RegroupementCoherentUE]: """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans ce semestre pour cette UE. @@ -250,7 +258,9 @@ def find_rcues( other_ue = UniteEns.query.get(ue_id) other_formsemestre = FormSemestre.query.get(formsemestre_id) rcues.append( - RegroupementCoherentUE(etud, formsemestre, ue, other_formsemestre, other_ue) + RegroupementCoherentUE( + etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat + ) ) # safety check: 1 seul niveau de comp. concerné: assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1 diff --git a/sco_version.py b/sco_version.py index 4d1ccafcb2..d7a1305ec9 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3.13" +SCOVERSION = "9.3.14" SCONAME = "ScoDoc" From aa97a10bf8428ee67276b7b89b2a903a46f340a0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Jul 2022 11:41:58 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Ajout=20d'informations=20sur=20l'arr=C3=AAt?= =?UTF-8?q?=C3=A9=20BUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../but/documentation_codes_jury.html | 41 +++++++++++++++++-- sco_version.py | 2 +- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html index 27e4aa0c6c..dbaf51ca20 100644 --- a/app/templates/but/documentation_codes_jury.html +++ b/app/templates/but/documentation_codes_jury.html @@ -1,9 +1,12 @@

Ci-dessous la signification de chaque code est expliquée, - ainsi que la correspondance avec les codes préconisés par - l'AMUE pour Apogée dans un document informel qui a circulé début - 2022 (les éventuelles erreurs n'engagent personne). -

+ ainsi que la correspondance avec certains codes préconisés par + l'AMUE et l'ADIUT pour Apogée. + + On distingue les codes ScoDoc (utilisés ci-dessus et dans les différentes + tables générées par ScoDoc) et leur transcription vers Apogée lors des exports + (transcription paramétrable par votre administrateur ScoDoc). +

Codes d'année
@@ -230,4 +233,34 @@
+ +
Rappels de l'arrêté BUT (extraits)
+
+
    +
  • Au sein de chaque regroupement cohérent d’UE, la compensation est intégrale. + Si une UE n’a pas été acquise en raison d’une moyenne inférieure à 10, + cette UE sera acquise par compensation si et seulement si l’étudiant + a obtenu la moyenne au regroupement cohérent auquel l’UE appartient.
  • +
  • La poursuite d'études dans un semestre pair d’une même année est de droit + pour tout étudiant. + La poursuite d’études dans un semestre impair est possible + si et seulement si l’étudiant a obtenu : +
      +
    • la moyenne à plus de la moitié des regroupements cohérents d’UE
    • +
    • et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent d’UE.
    • +
    +
  • +
  • La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des + semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.
  • +
+ Textes de référence: + + +
\ No newline at end of file diff --git a/sco_version.py b/sco_version.py index d7a1305ec9..ed3c66a32d 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.3.14" +SCOVERSION = "9.3.15" SCONAME = "ScoDoc" From 428cc6152782f1873cb4ffefd0a9c39b58821093 Mon Sep 17 00:00:00 2001 From: lehmann Date: Tue, 5 Jul 2022 15:46:17 +0200 Subject: [PATCH 3/8] =?UTF-8?q?D=C3=A9cisions=20RCUE=20relev=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/releve-but.js | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/app/static/js/releve-but.js b/app/static/js/releve-but.js index edfdf79f15..105256af78 100644 --- a/app/static/js/releve-but.js +++ b/app/static/js/releve-but.js @@ -224,9 +224,26 @@ class releveBUT extends HTMLElement {
Non justifiées
${data.semestre.absences?.injustifie ?? "-"}
Total
${data.semestre.absences?.total ?? "-"}
- - photo de l'étudiant - `; + `; + if(data.semestre.decision_rcue.length){ + output += ` +
+
RCUE
+ ${(()=>{ + let output = ""; + data.semestre.decision_rcue.forEach(competence=>{ + output += `
${competence.niveau.competence.titre}
${competence.code}
`; + }) + return output; + })()} +
+ ` + } + + output += ` + + photo de l'étudiant + `; /*${data.semestre.groupes.map(groupe => { return `
@@ -240,9 +257,11 @@ class releveBUT extends HTMLElement { }).join("") }*/ this.shadow.querySelector(".infoSemestre").innerHTML = output; - if(data.semestre.decision_annee?.code){ + + + /*if(data.semestre.decision_annee?.code){ this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code]; - } + }*/ this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || ""; /*if (data.semestre.decision?.code) { From 312faf74fb8ca81a362bef10d91ca56d08b66956 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Jul 2022 20:37:38 +0200 Subject: [PATCH 4/8] Fix: publication bulletin --- app/api/etudiants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 4649a6698a..e16f298f56 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -450,7 +450,7 @@ def etudiant_bulletin_semestre( app.set_sco_dept(dept.acronym) return sco_bulletins.get_formsemestre_bulletin_etud_json( - formsemestre, etud, version + formsemestre, etud, version=version ) From 83ddd2bf03f92efe9c05ad3dd16f6baf811fa945 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 5 Jul 2022 20:40:21 +0200 Subject: [PATCH 5/8] Bul. DUT/ classic JSON: champ 'publie' --- app/scodoc/sco_bulletins_json.py | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 78425028dc..c48c0d4394 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -92,7 +92,6 @@ def formsemestre_bulletinetud_published_dict( nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) d = {"type": "classic", "version": "0"} - if (not sem["bul_hide_xml"]) or force_publishing: published = True else: @@ -134,6 +133,7 @@ def formsemestre_bulletinetud_published_dict( ) d["etudiant"]["sexe"] = d["etudiant"]["civilite"] # backward compat for our clients # Disponible pour publication ? + d["publie"] = published if not published: return d # stop ! @@ -364,8 +364,35 @@ def formsemestre_bulletinetud_published_dict( return d -def dict_decision_jury(etudid, formsemestre_id, with_decisions=False): - "dict avec decision pour bulletins json" +def dict_decision_jury(etudid, formsemestre_id, with_decisions=False) -> dict: + """dict avec decision pour bulletins json + - decision : décision semestre + - decision_ue : list des décisions UE + - situation + + with_decision donne les décision même si bul_show_decision est faux. + + Exemple: + { + 'autorisation_inscription': [{'semestre_id': 4}], + 'decision': {'code': 'ADM', + 'compense_formsemestre_id': None, + 'date': '2022-01-21', + 'etat': 'I'}, + 'decision_ue': [ + { + 'acronyme': 'UE31', + 'code': 'ADM', + 'ects': 16.0, + 'numero': 23, + 'titre': 'Approfondissement métiers', + 'ue_id': 1787 + }, + ... + ], + 'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: ' + 'UE31, UE32. Diplôme obtenu.'} + """ from app.scodoc import sco_bulletins d = {} From c9aa55979bd92ea209a1770a921a6c5f693edbd3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 6 Jul 2022 00:05:14 +0200 Subject: [PATCH 6/8] Misc fixes. Suppr. validations BUT. --- app/comp/res_common.py | 2 +- app/models/but_validations.py | 1 + app/scodoc/sco_bulletins.py | 10 --- app/scodoc/sco_formsemestre_validation.py | 56 +++++++------ .../but/documentation_codes_jury.html | 2 +- app/views/notes.py | 79 +++++++++++-------- tests/api/exemple-api-basic.py | 10 +-- 7 files changed, 88 insertions(+), 72 deletions(-) diff --git a/app/comp/res_common.py b/app/comp/res_common.py index 04bd92d13b..98b5a9c6f1 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -650,7 +650,7 @@ class ResultatsSemestre(ResultatsCache): elif nb_ues_validables < len(ues_sans_bonus): row["_ues_validables_class"] += " moy_inf" row["_ues_validables_order"] = nb_ues_validables # pour tri - if mode_jury: + if mode_jury and self.validations: dec_sem = self.validations.decisions_jury.get(etudid) jury_code_sem = dec_sem["code"] if dec_sem else "" idx = add_cell( diff --git a/app/models/but_validations.py b/app/models/but_validations.py index ebedadd715..9d42c14d3f 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -43,6 +43,7 @@ class ApcValidationRCUE(db.Model): formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True ) + "formsemestre pair du RCUE" # Les deux UE associées à ce niveau: ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index daf4a9419d..4a82674e23 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -58,7 +58,6 @@ from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_groups from app.scodoc import sco_permissions_check -from app.scodoc import sco_photos from app.scodoc import sco_preferences from app.scodoc import sco_pvjury from app.scodoc import sco_users @@ -66,15 +65,6 @@ import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType, fmt_note import app.scodoc.notesdb as ndb -# ----- CLASSES DE BULLETINS DE NOTES -from app.scodoc import sco_bulletins_standard -from app.scodoc import sco_bulletins_legacy - -# import sco_bulletins_example # format exemple (à désactiver en production) - -# ... ajouter ici vos modules ... -from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun - def get_formsemestre_bulletin_etud_json( formsemestre: FormSemestre, diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 2af209c2a8..a9d13c016c 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -35,13 +35,17 @@ from app.models.etudiants import Identite import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu -from app import log +from app import db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import FormSemestre from app.models.notes import etud_has_notes_attente - +from app.models.validations import ( + ScolarAutorisationInscription, + ScolarFormSemestreValidation, +) +from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.scolog import logdb from app.scodoc.sco_codes_parcours import * @@ -989,28 +993,32 @@ def do_formsemestre_validation_auto(formsemestre_id): def formsemestre_validation_suppress_etud(formsemestre_id, etudid): - """Suppression des decisions de jury pour un etudiant.""" - log("formsemestre_validation_suppress_etud( %s, %s)" % (formsemestre_id, etudid)) - cnx = ndb.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - args = {"formsemestre_id": formsemestre_id, "etudid": etudid} - try: - # -- Validation du semestre et des UEs - cursor.execute( - """delete from scolar_formsemestre_validation - where etudid = %(etudid)s and formsemestre_id=%(formsemestre_id)s""", - args, - ) - # -- Autorisations d'inscription - cursor.execute( - """delete from scolar_autorisation_inscription - where etudid = %(etudid)s and origin_formsemestre_id=%(formsemestre_id)s""", - args, - ) - cnx.commit() - except: - cnx.rollback() - raise + """Suppression des décisions de jury pour un étudiant/formsemestre. + Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant: + code semestre, UEs, autorisations d'inscription + """ + log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})") + + # Validations jury classiques (semestres, UEs, autorisations) + for v in ScolarFormSemestreValidation.query.filter_by( + etudid=etudid, formsemestre_id=formsemestre_id + ): + db.session.delete(v) + for v in ScolarAutorisationInscription.query.filter_by( + etudid=etudid, origin_formsemestre_id=formsemestre_id + ): + db.session.delete(v) + # Validations jury spécifiques BUT + for v in ApcValidationRCUE.query.filter_by( + etudid=etudid, formsemestre_id=formsemestre_id + ): + db.session.delete(v) + for v in ApcValidationAnnee.query.filter_by( + etudid=etudid, formsemestre_id=formsemestre_id + ): + db.session.delete(v) + + db.session.commit() sem = sco_formsemestre.get_formsemestre(formsemestre_id) _invalidate_etud_formation_caches( diff --git a/app/templates/but/documentation_codes_jury.html b/app/templates/but/documentation_codes_jury.html index dbaf51ca20..abd3a74227 100644 --- a/app/templates/but/documentation_codes_jury.html +++ b/app/templates/but/documentation_codes_jury.html @@ -258,7 +258,7 @@
  • Bulletin officiel spécial n°4 du 17 juin 2021
  • version + href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">Version pdf complète
  • diff --git a/app/views/notes.py b/app/views/notes.py index 11b794810d..a5e50d3eec 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -57,7 +57,7 @@ from app.models.ues import UniteEns from app import api from app import db from app import models -from app.models import ScolarNews +from app.models import ScolarNews, but_validations from app.auth.models import User from app.but import apc_edit_ue, jury_but_recap from app.decorators import ( @@ -71,7 +71,7 @@ from app.views import notes_bp as bp # --------------- -from app.scodoc import sco_utils as scu +from app.scodoc import sco_bulletins_json, sco_utils as scu from app.scodoc import notesdb as ndb from app import log, send_scodoc_alarm @@ -2515,51 +2515,68 @@ def do_formsemestre_validation_auto(formsemestre_id): def formsemestre_validation_suppress_etud( formsemestre_id, etudid, dialog_confirmed=False ): - """Suppression des decisions de jury pour un etudiant.""" + """Suppression des décisions de jury pour un étudiant.""" if not sco_permissions_check.can_validate_sem(formsemestre_id): return scu.confirm_dialog( message="

    Opération non autorisée pour %s" % current_user, dest_url=scu.ScoURL(), ) + etud = Identite.query.get_or_404(etudid) + formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + if formsemestre.formation.is_apc(): + next_url = url_for( + "scolar.ficheEtud", + 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: - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - formsemestre = FormSemestre.query.get_or_404(formsemestre_id) - sem = formsemestre.to_dict() - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - decision_jury = nt.get_etud_decision_sem(etudid) - if decision_jury: - existing = ( - "

    Décision existante: %(code)s du %(event_date)s

    " % decision_jury - ) + d = sco_bulletins_json.dict_decision_jury( + etudid, formsemestre_id, with_decisions=True + ) + d.update(but_validations.dict_decision_jury(etud, formsemestre)) + + 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: - existing = "" + 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
    • +
    + """ return scu.confirm_dialog( - """

    Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?

    %s -

    Cette opération est irréversible. -

    - """ - % ( - sem["titre_num"], - sem["date_debut"], - sem["date_fin"], - etud["nomprenom"], - existing, - ), + 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="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s" - % (formsemestre_id, etudid), + cancel_url=next_url, parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, ) sco_formsemestre_validation.formsemestre_validation_suppress_etud( formsemestre_id, etudid ) - return flask.redirect( - scu.ScoURL() - + "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée" - % (formsemestre_id, etudid) - ) + flash("Décisions supprimées") + return flask.redirect(next_url) # ------------- PV de JURY et archives diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index a581a87838..50b4a11f0d 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -33,7 +33,7 @@ except NameError: load_dotenv(os.path.join(BASEDIR, ".env")) CHK_CERT = bool(int(os.environ.get("CHECK_CERTIFICATE", False))) -SCODOC_URL = os.environ["SCODOC_URL"] or "http://localhost:5000" +SCODOC_URL = os.environ.get("SCODOC_URL") or "http://localhost:5000" API_URL = SCODOC_URL + "/ScoDoc/api" SCODOC_USER = os.environ["SCODOC_USER"] SCODOC_PASSWORD = os.environ["SCODOC_PASSWORD"] @@ -85,13 +85,13 @@ if r.status_code != 200: print(f"{len(r.json())} étudiants courants") # Bulletin d'un BUT -formsemestre_id = 1052 # A adapter -etudid = 16400 +formsemestre_id = 1063 # A adapter +etudid = 16450 bul = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") # d'un DUT -formsemestre_id = 1028 # A adapter -etudid = 14721 +formsemestre_id = 1062 # A adapter +etudid = 16309 bul_dut = GET(f"/etudiant/etudid/{etudid}/formsemestre/{formsemestre_id}/bulletin") From 48912032c497a6d80e60f55dd8ce6767f87c14ed Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Jul 2022 11:56:18 +0200 Subject: [PATCH 7/8] =?UTF-8?q?Am=C3=A9lioration=20sco=5Fexcel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entreprises/routes.py | 4 +- app/scodoc/sco_excel.py | 159 ++++++++++++++++---------------------- 2 files changed, 70 insertions(+), 93 deletions(-) diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index ef15f32b89..6a831d866c 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -1419,7 +1419,7 @@ def get_import_donnees_file_sample(): @permission_required(Permission.RelationsEntreprisesExport) def import_donnees(): """ - Permet d'importer des entreprises a l'aide d'un fichier excel (.xlsx) + Permet d'importer des entreprises à partir d'un fichier excel (.xlsx) """ form = ImportForm() if form.validate_on_submit(): @@ -1428,7 +1428,7 @@ def import_donnees(): Config.SCODOC_VAR_DIR, "tmp", secure_filename(file.filename) ) file.save(file_path) - diag, lm = sco_excel.excel_file_to_list_are(file_path) + diag, lm = sco_excel.excel_workbook_to_list(file_path) os.remove(file_path) if lm is None or len(lm) < 2: flash("Veuillez utilisez la feuille excel à remplir") diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index b216520932..8e1ceabc54 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -40,10 +40,9 @@ from openpyxl.comments import Comment from openpyxl import Workbook, load_workbook from openpyxl.cell import WriteOnlyCell from openpyxl.styles import Font, Border, Side, Alignment, PatternFill +from openpyxl.worksheet.worksheet import Worksheet import app.scodoc.sco_utils as scu -from app.scodoc import notesdb -from app.scodoc import sco_preferences from app import log from app.scodoc.sco_exceptions import ScoValueError @@ -593,71 +592,87 @@ def excel_feuille_saisie(e, titreannee, description, lines): def excel_bytes_to_list(bytes_content): try: filelike = io.BytesIO(bytes_content) - return _excel_to_list(filelike) - except: + except Exception as exc: raise ScoValueError( """Le fichier xlsx attendu n'est pas lisible ! Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..) """ - ) + ) from exc + return _excel_to_list(filelike) def excel_file_to_list(filename): try: return _excel_to_list(filename) - except: + except Exception as exc: raise ScoValueError( """Le fichier xlsx attendu n'est pas lisible ! Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) """ - ) + ) from exc def excel_file_to_list_are(filename): try: return _excel_to_list_are(filename) - except: + except Exception as exc: raise ScoValueError( """Le fichier xlsx attendu n'est pas lisible ! Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) """ - ) + ) from exc + + +def _open_workbook(filelike, dump_debug=False) -> Workbook: + """Open document. + On error, if dump-debug is True, dump data in /tmp for debugging purpose + """ + try: + workbook = load_workbook(filename=filelike, read_only=True, data_only=True) + except Exception as exc: + log("Excel_to_list: failure to import document") + if dump_debug: + dump_filename = "/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX + log(f"Dumping problemetic file on {dump_filename}") + with open(dump_filename, "wb") as f: + f.write(filelike) + raise ScoValueError( + "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel xlsx !" + ) from exc + return workbook def _excel_to_list(filelike): - """returns list of list - convert_to_string is a conversion function applied to all non-string values (ie numbers) - """ - try: - wb = load_workbook(filename=filelike, read_only=True, data_only=True) - except: - log("Excel_to_list: failure to import document") - with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f: - f.write(filelike) - raise ScoValueError( - "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" - ) + """returns list of list""" + workbook = _open_workbook(filelike) diag = [] # liste de chaines pour former message d'erreur - # n'utilise que la première feuille - if len(wb.get_sheet_names()) < 1: + if len(workbook.get_sheet_names()) < 1: diag.append("Aucune feuille trouvée dans le classeur !") return diag, None - if len(wb.get_sheet_names()) > 1: + # n'utilise que la première feuille: + if len(workbook.get_sheet_names()) > 1: diag.append("Attention: n'utilise que la première feuille du classeur !") + sheet_name = workbook.get_sheet_names()[0] + ws = workbook[sheet_name] + matrix, diag_sheet = _excel_sheet_to_list(ws, sheet_name) + diag += diag_sheet + return diag, matrix + + +def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]: + """read a spreadsheet sheet, and returns: + - diag : a list of strings (error messages aimed at helping the user) + - a list of lists: the spreadsheet cells + """ + diag = [] # fill matrix - sheet_name = wb.get_sheet_names()[0] - ws = wb.get_sheet_by_name(sheet_name) - sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") values = {} - for row in ws.iter_rows(): + for row in sheet.iter_rows(): for cell in row: if cell.value is not None: values[(cell.row - 1, cell.column - 1)] = str(cell.value) if not values: - diag.append( - "Aucune valeur trouvée dans la feuille %s !" - % sheet_name.decode(scu.SCO_ENCODING) - ) + diag.append(f"Aucune valeur trouvée dans la feuille {sheet_name} !") return diag, None indexes = list(values.keys()) # search numbers of rows and cols @@ -665,76 +680,38 @@ def _excel_to_list(filelike): cols = [x[1] for x in indexes] nbcols = max(cols) + 1 nbrows = max(rows) + 1 - m = [] + matrix = [] for _ in range(nbrows): - m.append([""] * nbcols) + matrix.append([""] * nbcols) for row_idx, col_idx in indexes: v = values[(row_idx, col_idx)] - # if isinstance(v, six.text_type): - # v = v.encode(scu.SCO_ENCODING, "backslashreplace") - # elif convert_to_string: - # v = convert_to_string(v) - m[row_idx][col_idx] = v - diag.append( - 'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m)) - ) - # diag.append(str(M)) - # - return diag, m + matrix[row_idx][col_idx] = v + diag.append(f'Feuille "{sheet_name}", {len(matrix)} lignes') + + return diag, matrix -def _excel_to_list_are(filelike): - """returns list of list - convert_to_string is a conversion function applied to all non-string values (ie numbers) +def _excel_workbook_to_list(filelike): + """Lit un classeur (workbook): chaque feuille est lue + et est convertie en une liste de listes. + Returns: + - diag : a list of strings (error messages aimed at helping the user) + - a list of lists: the spreadsheet cells """ - try: - wb = load_workbook(filename=filelike, read_only=True, data_only=True) - except: - log("Excel_to_list: failure to import document") - with open("/tmp/last_scodoc_import_failure" + scu.XLSX_SUFFIX, "wb") as f: - f.write(filelike) - raise ScoValueError( - "Fichier illisible: assurez-vous qu'il s'agit bien d'un document Excel !" - ) + workbook = _open_workbook(filelike) diag = [] # liste de chaines pour former message d'erreur - if len(wb.get_sheet_names()) < 1: + if len(workbook.get_sheet_names()) < 1: diag.append("Aucune feuille trouvée dans le classeur !") return diag, None - lm = [] - for sheet_name in wb.get_sheet_names(): + matrix_list = [] + for sheet_name in workbook.get_sheet_names(): # fill matrix - ws = wb.get_sheet_by_name(sheet_name) - sheet_name = sheet_name.encode(scu.SCO_ENCODING, "backslashreplace") - values = {} - for row in ws.iter_rows(): - for cell in row: - if cell.value is not None: - values[(cell.row - 1, cell.column - 1)] = str(cell.value) - if not values: - diag.append( - "Aucune valeur trouvée dans la feuille %s !" - % sheet_name.decode(scu.SCO_ENCODING) - ) - return diag, None - indexes = list(values.keys()) - # search numbers of rows and cols - rows = [x[0] for x in indexes] - cols = [x[1] for x in indexes] - nbcols = max(cols) + 1 - nbrows = max(rows) + 1 - m = [] - for _ in range(nbrows): - m.append([""] * nbcols) - - for row_idx, col_idx in indexes: - v = values[(row_idx, col_idx)] - m[row_idx][col_idx] = v - diag.append( - 'Feuille "%s", %d lignes' % (sheet_name.decode(scu.SCO_ENCODING), len(m)) - ) - lm.append(m) - return diag, lm + sheet = workbook.get_sheet_by_name(sheet_name) + matrix, diag_sheet = _excel_sheet_to_list(sheet, sheet_name) + diag += diag_sheet + matrix_list.append(matrix) + return diag, matrix_list def excel_feuille_listeappel( From d8ff5152d1127426606af348f5c8cf2da9261517 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 7 Jul 2022 11:59:47 +0200 Subject: [PATCH 8/8] =?UTF-8?q?Am=C3=A9lioration=20sco=5Fexcel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_excel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scodoc/sco_excel.py b/app/scodoc/sco_excel.py index 8e1ceabc54..9233b5e525 100644 --- a/app/scodoc/sco_excel.py +++ b/app/scodoc/sco_excel.py @@ -612,9 +612,9 @@ def excel_file_to_list(filename): ) from exc -def excel_file_to_list_are(filename): +def excel_workbook_to_list(filename): try: - return _excel_to_list_are(filename) + return _excel_workbook_to_list(filename) except Exception as exc: raise ScoValueError( """Le fichier xlsx attendu n'est pas lisible !