From b2296d94dfe3d56f9350f6262baff18b0cac4d80 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 17 Apr 2023 12:10:17 +0200 Subject: [PATCH 1/9] =?UTF-8?q?Fix=20export=20Apo=20BUT/Simpair=20isol?= =?UTF-8?q?=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_apogee_csv.py | 10 +++++++--- sco_version.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 64ca9a35..64860522 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -476,8 +476,10 @@ class ApoEtud(dict): # return None # element Apogee non trouvé dans ce semestre - def comp_elt_semestre(self, nt, decision, etudid): - """Calcul résultat apo semestre""" + def comp_elt_semestre(self, nt: NotesTableCompat, decision: dict, etudid: int): + """Calcul résultat apo semestre. + Toujours vide pour en BUT/APC. + """ if self.is_apc: # pas de code semestre en APC ! return dict(N="", B=20, J="", R="", M="") @@ -522,7 +524,9 @@ class ApoEtud(dict): cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"]) cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre) - if not self.is_apc: + if self.is_apc: + cur_decision = {} # comp_elt_semestre sera vide. + else: cur_decision = cur_nt.get_etud_decision_sem(etudid) if not cur_decision: # pas de decision => pas de résultat annuel diff --git a/sco_version.py b/sco_version.py index eed78321..26e724b0 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.71" +SCOVERSION = "9.4.72" SCONAME = "ScoDoc" From 339d70edd2ba23c884ac0d780ce53f014fc7c938 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Apr 2023 02:20:32 +0200 Subject: [PATCH 2/9] =?UTF-8?q?Apo:=20am=C3=A9liore=20changement=20p=C3=A9?= =?UTF-8?q?riode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_etape_apogee.py | 7 ++++--- app/scodoc/sco_etape_apogee_view.py | 16 ++++++++-------- app/scodoc/sco_semset.py | 14 ++++++++++---- sco_version.py | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py index ef60acca..be10eb23 100644 --- a/app/scodoc/sco_etape_apogee.py +++ b/app/scodoc/sco_etape_apogee.py @@ -143,9 +143,10 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): if annee_scolaire: e = re.compile(str(annee_scolaire) + "-.+") oids = [x for x in oids if e.match(x)] - if sem_id: - e = re.compile(r"[0-9]{4}-" + str(sem_id)) - oids = [x for x in oids if e.match(x)] + # Note: ne filtre plus sur la période, afin que l'on puisse chnager le semset de période. + # if sem_id: + # e = re.compile(r"[0-9]{4}-" + str(sem_id)) + # oids = [x for x in oids if e.match(x)] infos = [] # liste d'infos for oid in oids: diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py index b53fe5e0..e40cbe64 100644 --- a/app/scodoc/sco_etape_apogee_view.py +++ b/app/scodoc/sco_etape_apogee_view.py @@ -640,20 +640,20 @@ def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect= apo_data = sco_apogee_csv.ApoData( data_str, periode=semset["sem_id"] ) # parse le fichier -> exceptions + dest_url = url_for( + "notes.apo_semset_maq_status", + scodoc_dept=g.scodoc_dept, + semset_id=semset_id, + ) if apo_data.etape not in semset["etapes"]: raise ScoValueError( - "Le code étape de ce fichier ne correspond pas à ceux de cet ensemble" + "Le code étape de ce fichier ne correspond pas à ceux de cet ensemble", + dest_url=dest_url, ) sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"]) - return flask.redirect( - url_for( - "notes.apo_semset_maq_status", - scodoc_dept=g.scodoc_dept, - semset_id=semset_id, - ) - ) + return flask.redirect(dest_url) def view_apo_csv_download_and_store(etape_apo="", semset_id=""): diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index c91ee762..f8cf5e81 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -137,8 +137,15 @@ class SemSet(dict): self["semtitles"] = [sem["titre_num"] for sem in self.sems] # Construction du ou des lien(s) vers le semestre - pattern = '%(titreannee)s' - self["semlinks"] = [(pattern % sem) for sem in self.sems] + self["semlinks"] = [ + f"""{sem["titreannee"]} + """ + for sem in self.sems + ] + self["semtitles_str"] = "
".join(self["semlinks"]) def fill_formsemestres(self): @@ -364,8 +371,7 @@ class SemSet(dict): """ if sco_portal_apogee.has_portal(): return self.bilan.html_diagnostic() - else: - return "" + return "" def get_semsets_list(): diff --git a/sco_version.py b/sco_version.py index 26e724b0..f6d69c13 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.72" +SCOVERSION = "9.4.73" SCONAME = "ScoDoc" From 54bcbff179a646a5ec5c648cc48d44e8d90dc08c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Apr 2023 11:47:10 +0200 Subject: [PATCH 3/9] =?UTF-8?q?Apo:=20modif=20semset=20pour=20BUT.=20Inter?= =?UTF-8?q?dit=20changement=20p=C3=A9riode.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/semset.py | 55 +++++++++++---------- app/comp/res_compat.py | 24 +++++---- app/scodoc/sco_etape_apogee.py | 7 ++- app/scodoc/sco_etape_apogee_view.py | 6 ++- app/scodoc/sco_semset.py | 77 ++++++++++++++++------------- 5 files changed, 93 insertions(+), 76 deletions(-) diff --git a/app/api/semset.py b/app/api/semset.py index f1239534..5eba3f3e 100644 --- a/app/api/semset.py +++ b/app/api/semset.py @@ -7,33 +7,34 @@ """ ScoDoc 9 API : accès aux formsemestres """ -from flask import g, jsonify, request -from flask_login import login_required +# from flask import g, jsonify, request +# from flask_login import login_required -import app -from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR -from app.decorators import scodoc, permission_required -from app.scodoc.sco_utils import json_error -from app.models.formsemestre import NotesSemSet -from app.scodoc.sco_permissions import Permission +# import app +# from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR +# from app.decorators import scodoc, permission_required +# from app.scodoc.sco_utils import json_error +# from app.models.formsemestre import NotesSemSet +# from app.scodoc.sco_permissions import Permission -@bp.route("/semset/set_periode/", methods=["POST"]) -@api_web_bp.route("/semset/set_periode/", methods=["POST"]) -@login_required -@scodoc -@permission_required(Permission.ScoEditApo) -# TODO à modifier pour utiliser @as_json -def semset_set_periode(semset_id: int): - "Change la période d'un semset" - query = NotesSemSet.query.filter_by(semset_id=semset_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - semset: NotesSemSet = query.first_or_404() - data = request.get_json(force=True) # may raise 400 Bad Request - try: - periode = int(data) - semset.set_periode(periode) - except ValueError: - return json_error(API_CLIENT_ERROR, "invalid periode value") - return jsonify({"OK": True}) +# Impossible de changer la période à cause des archives +# @bp.route("/semset/set_periode/", methods=["POST"]) +# @api_web_bp.route("/semset/set_periode/", methods=["POST"]) +# @login_required +# @scodoc +# @permission_required(Permission.ScoEditApo) +# # TODO à modifier pour utiliser @as_json +# def semset_set_periode(semset_id: int): +# "Change la période d'un semset" +# query = NotesSemSet.query.filter_by(semset_id=semset_id) +# if g.scodoc_dept: +# query = query.filter_by(dept_id=g.scodoc_dept_id) +# semset: NotesSemSet = query.first_or_404() +# data = request.get_json(force=True) # may raise 400 Bad Request +# try: +# periode = int(data) +# semset.set_periode(periode) +# except ValueError: +# return json_error(API_CLIENT_ERROR, "invalid periode value") +# return jsonify({"OK": True}) diff --git a/app/comp/res_compat.py b/app/comp/res_compat.py index bc501b64..7b20ffd5 100644 --- a/app/comp/res_compat.py +++ b/app/comp/res_compat.py @@ -19,6 +19,7 @@ from app.models import Identite, FormSemestre, ModuleImpl, ScolarAutorisationIns from app.scodoc.codes_cursus import UE_SPORT, DEF from app.scodoc import sco_utils as scu + # Pour raccorder le code des anciens codes qui attendent une NoteTable class NotesTableCompat(ResultatsSemestre): """Implementation partielle de NotesTable @@ -266,16 +267,21 @@ class NotesTableCompat(ResultatsSemestre): ue_status_list.append(ue_status) return self.parcours.check_barre_ues(ue_status_list) - def all_etuds_have_sem_decisions(self): - """True si tous les étudiants du semestre ont une décision de jury. - Ne regarde pas les décisions d'UE. + def etudids_without_decisions(self) -> list[int]: + """Liste des id d'étudiants du semestre non démissionnaires + n'ayant pas de décision de jury. + - En classic: ne regarde pas que la décision de semestre (pas les décisions d'UE). + - en BUT: utilise etud_has_decision """ - for ins in self.formsemestre.inscriptions: - if ins.etat != scu.INSCRIT: - continue # skip démissionnaires - if self.get_etud_decision_sem(ins.etudid) is None: - return False - return True + check_func = ( + self.etud_has_decision if self.is_apc else self.get_etud_decision_sem + ) + etudids = [ + ins.etudid + for ins in self.formsemestre.inscriptions + if (ins.etat == scu.INSCRIT) and (not check_func(ins.etudid)) + ] + return etudids def etud_has_decision(self, etudid): """True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre. diff --git a/app/scodoc/sco_etape_apogee.py b/app/scodoc/sco_etape_apogee.py index be10eb23..ef60acca 100644 --- a/app/scodoc/sco_etape_apogee.py +++ b/app/scodoc/sco_etape_apogee.py @@ -143,10 +143,9 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None): if annee_scolaire: e = re.compile(str(annee_scolaire) + "-.+") oids = [x for x in oids if e.match(x)] - # Note: ne filtre plus sur la période, afin que l'on puisse chnager le semset de période. - # if sem_id: - # e = re.compile(r"[0-9]{4}-" + str(sem_id)) - # oids = [x for x in oids if e.match(x)] + if sem_id: + e = re.compile(r"[0-9]{4}-" + str(sem_id)) + oids = [x for x in oids if e.match(x)] infos = [] # liste d'infos for oid in oids: diff --git a/app/scodoc/sco_etape_apogee_view.py b/app/scodoc/sco_etape_apogee_view.py index e40cbe64..77d2bb40 100644 --- a/app/scodoc/sco_etape_apogee_view.py +++ b/app/scodoc/sco_etape_apogee_view.py @@ -240,7 +240,11 @@ def apo_semset_maq_status( if semset["jury_ok"]: H.append("""
  • Décisions de jury saisies
  • """) else: - H.append("""
  • Il manque des décisions de jury !
  • """) + H.append( + f"""
  • Il manque de {semset["jury_nb_missing"]} + décision{"s" if semset["jury_nb_missing"] > 1 else ""} + de jury !
  • """ + ) if ok_for_export: H.append("""
  • %d étudiants, prêt pour l'export.
  • """ % len(nips_ok)) diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index f8cf5e81..e4cb5852 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -156,6 +156,8 @@ class SemSet(dict): def add(self, formsemestre_id): "Ajoute ce semestre à l'ensemble" + # check for valid formsemestre_id + formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) # check if formsemestre_id in self.formsemestre_ids: return # already there @@ -166,6 +168,19 @@ class SemSet(dict): f"can't add {formsemestre_id} to set {self.semset_id}: incompatible sem_id" ) + if self.formsemestre_ids: + formsemestre_1 = formsemestre.query.get(self.formsemestre_ids[0]) + if formsemestre.formation.is_apc() != formsemestre_1.formation.is_apc(): + raise ScoValueError( + """On ne peut pas mélanger des semestres BUT/APC + avec des semestres ordinaires dans le même export.""", + dest_url=url_for( + "notes.apo_semset_maq_status", + scodoc_dept=g.scodoc_dept, + semset_id=self.semset_id, + ), + ) + ndb.SimpleQuery( """INSERT INTO notes_semset_formsemestre (formsemestre_id, semset_id) @@ -249,17 +264,28 @@ class SemSet(dict): def load_etuds(self): self["etuds_without_nip"] = set() # etudids self["jury_ok"] = True + self["jury_nb_missing"] = 0 + is_apc = None for sem in self.sems: formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"]) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + if is_apc is not None and is_apc != nt.is_apc: + raise ScoValueError( + "Incohérence: semestre APC (BUT) et ordinaires mélangés !" + ) + else: + is_apc = nt.is_apc sem["etuds"] = list(nt.identdict.values()) sem["nips"] = {e["code_nip"] for e in sem["etuds"] if e["code_nip"]} sem["etuds_without_nip"] = { e["etudid"] for e in sem["etuds"] if not e["code_nip"] } self["etuds_without_nip"] |= sem["etuds_without_nip"] - sem["jury_ok"] = nt.all_etuds_have_sem_decisions() + sem["etudids_no_jury"] = nt.etudids_without_decisions() + sem["jury_ok"] = not sem["etudids_no_jury"] self["jury_ok"] &= sem["jury_ok"] + self["jury_nb_missing"] += len(sem["etudids_no_jury"]) + self["is_apc"] = bool(is_apc) def html_descr(self): """Short HTML description""" @@ -279,36 +305,21 @@ class SemSet(dict): ) H.append("

    ") - H.append( - f"""

    Période: -

    - - """ - ) + if self["sem_id"] == 1: + periode = "1re période (S1, S3)" + elif self["sem_id"] == 1: + periode = "2de période (S2, S4)" + else: + periode = "non semestrialisée (LP, ...). Incompatible avec BUT." H.append( - f"

    Etapes: {sco_formsemestre.etapes_apo_str(self.list_etapes())}

    " + f""" +

    Période: {periode}

    +

    Etapes: {sco_formsemestre.etapes_apo_str(self.list_etapes())}

    + +

    Semestres de l'ensemble: