From 07c2f00277372101ec2ea509ffe89461b6fcd7b2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 14 Jun 2024 20:15:20 +0200 Subject: [PATCH 1/8] Import utilisateurs: strip champs --- app/models/formsemestre.py | 2 +- app/scodoc/sco_import_users.py | 2 +- app/scodoc/sco_moduleimpl_status.py | 10 +- app/scodoc/sco_saisie_notes.py | 310 ++++++++++++++++------------ app/scodoc/sco_ue_external.py | 2 +- app/scodoc/sco_users.py | 2 +- sco_version.py | 2 +- tests/unit/test_notes_modules.py | 65 +++--- tests/unit/test_notes_rattrapage.py | 38 ++-- tests/unit/test_sco_basic.py | 9 +- 10 files changed, 244 insertions(+), 198 deletions(-) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 39c657aa..738a4744 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -973,7 +973,7 @@ class FormSemestre(models.ScoDocModel): def etudids_actifs(self) -> tuple[list[int], set[int]]: """Liste les etudids inscrits (incluant DEM et DEF), - qui ser al'index des dataframes de notes + qui sera l'index des dataframes de notes et donne l'ensemble des inscrits non DEM ni DEF. """ return [inscr.etudid for inscr in self.inscriptions], { diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py index 8f0ac770..314822a1 100644 --- a/app/scodoc/sco_import_users.py +++ b/app/scodoc/sco_import_users.py @@ -140,7 +140,7 @@ def read_users_excel_file(datafile, titles=TITLES) -> list[dict]: for line in data[1:]: d = {} for i, field in enumerate(xls_titles): - d[field] = line[i] + d[field] = (line[i] or "").strip() users.append(d) return users diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index abe85457..5897ea46 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -70,13 +70,21 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str: menu_eval = [ { - "title": "Saisir notes", + "title": "Saisir les notes", "endpoint": "notes.saisie_notes", "args": { "evaluation_id": evaluation_id, }, "enabled": can_edit_notes_ens, }, + { + "title": "Saisir par fichier tableur", + "id": "menu_saisie_tableur", + "endpoint": "notes.saisie_notes_tableur", + "args": { + "evaluation_id": evaluation.id, + }, + }, { "title": "Modifier évaluation", "endpoint": "notes.evaluation_edit", diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index d8006e49..7df74fa3 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -29,12 +29,15 @@ Formulaire revu en juillet 2016 """ +import html import time -import psycopg2 + import flask from flask import g, url_for, request from flask_login import current_user +from flask_sqlalchemy.query import Query +import psycopg2 from app import db, log from app.auth.models import User @@ -75,8 +78,6 @@ import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import ModuleType -from flask_sqlalchemy.query import Query - def convert_note_from_string( note: str, @@ -115,7 +116,7 @@ def convert_note_from_string( return note_value, invalid -def _displayNote(val): +def _display_note(val): """Convert note from DB to viewable string. Utilisé seulement pour I/O vers formulaires (sans perte de precision) (Utiliser fmt_note pour les affichages) @@ -272,7 +273,7 @@ def do_evaluation_upload_xls(): diag.append("Notes invalides pour: " + ", ".join(etudsnames)) raise InvalidNoteValue() else: - etudids_changed, nb_suppress, etudids_with_decisions = notes_add( + etudids_changed, nb_suppress, etudids_with_decisions, messages = notes_add( current_user, evaluation_id, valid_notes, comment ) # news @@ -292,9 +293,19 @@ def do_evaluation_upload_xls(): max_frequency=30 * 60, # 30 minutes ) - msg = f"""

{len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, { - len(absents)} absents, {nb_suppress} note supprimées) + msg = f"""

+ {len(etudids_changed)} notes changées ({len(withoutnotes)} sans notes, + {len(absents)} absents, {nb_suppress} note supprimées)

""" + if messages: + msg += f"""
Attention : + +
""" if etudids_with_decisions: msg += """

Important: il y avait déjà des décisions de jury enregistrées, qui sont peut-être à revoir suite à cette modification !

@@ -322,7 +333,7 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) - # Convert and check value L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation) if len(invalids) == 0: - etudids_changed, _, _ = notes_add( + etudids_changed, _, _, _ = notes_add( current_user, evaluation.id, L, "Initialisation notes" ) if len(etudids_changed) == 1: @@ -398,7 +409,9 @@ def do_evaluation_set_missing( ) # ok comment = "Initialisation notes manquantes" - etudids_changed, _, _ = notes_add(current_user, evaluation_id, valid_notes, comment) + etudids_changed, _, _, _ = notes_add( + current_user, evaluation_id, valid_notes, comment + ) # news url = url_for( "notes.moduleimpl_status", @@ -456,7 +469,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): ) if not dialog_confirmed: - etudids_changed, nb_suppress, existing_decisions = notes_add( + etudids_changed, nb_suppress, existing_decisions, _ = notes_add( current_user, evaluation_id, notes, do_it=False, check_inscription=False ) msg = f"""

Confirmer la suppression des {nb_suppress} notes ? @@ -477,7 +490,7 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False): ) # modif - etudids_changed, nb_suppress, existing_decisions = notes_add( + etudids_changed, nb_suppress, existing_decisions, _ = notes_add( current_user, evaluation_id, notes, @@ -519,7 +532,7 @@ def notes_add( comment=None, do_it=True, check_inscription=True, -) -> tuple[list[int], int, list[int]]: +) -> tuple[list[int], int, list[int], list[str]]: """ Insert or update notes notes is a list of tuples (etudid,value) @@ -528,30 +541,48 @@ def notes_add( Nota: - si la note existe deja avec valeur distincte, ajoute une entree au log (notes_notes_log) - Return: tuple (etudids_changed, nb_suppress, etudids_with_decision) + Return: tuple (etudids_changed, nb_suppress, etudids_with_decision, messages) + + messages = list de messages d'avertissement/information pour l'utilisateur """ evaluation = Evaluation.get_evaluation(evaluation_id) now = psycopg2.Timestamp(*time.localtime()[:6]) - - # Vérifie inscription et valeur note - inscrits = { + messages = [] + # Vérifie inscription au module (même DEM/DEF) + etudids_inscrits_mod = { x[0] for x in sco_groups.do_evaluation_listeetuds_groups( evaluation_id, getallstudents=True, include_demdef=True ) } - # Les étudiants inscrits au semestre ni DEM ni DEF - _, etudids_actifs = evaluation.moduleimpl.formsemestre.etudids_actifs() + # Les étudiants inscrits au semestre et ceux "actifs" (ni DEM ni DEF) + etudids_inscrits_sem, etudids_actifs = ( + evaluation.moduleimpl.formsemestre.etudids_actifs() + ) for etudid, value in notes: - if check_inscription and ( - (etudid not in inscrits) or (etudid not in etudids_actifs) - ): - log(f"notes_add: {etudid} non inscrit ou DEM/DEF: aborting") - raise NoteProcessError(f"étudiant {etudid} non inscrit dans ce module") + + if check_inscription: + msg_err, msg_warn = "", "" + if etudid not in etudids_inscrits_sem: + msg_err = "non inscrit au semestre" + elif etudid not in etudids_inscrits_mod: + msg_err = "non inscrit au module" + elif etudid not in etudids_actifs: + # DEM ou DEF + msg_warn = "démissionnaire ou défaillant (note enregistrée)" + if msg_err or msg_warn: + etud = Identite.query.get(etudid) if isinstance(etudid, int) else None + msg = f"étudiant {etud.nomprenom if etud else etudid} {msg_err or msg_warn}" + if msg_err: + log(f"notes_add: {etudid} non inscrit ou DEM/DEF: aborting") + raise NoteProcessError(msg) + if msg_warn: + messages.append(msg) if (value is not None) and not isinstance(value, float): log(f"notes_add: {etudid} valeur de note invalide ({value}): aborting") + etud = Identite.query.get(etudid) if isinstance(etudid, int) else None raise NoteProcessError( - f"etudiant {etudid}: valeur de note invalide ({value})" + f"etudiant {etud.nomprenom if etud else etudid}: valeur de note invalide ({value})" ) # Recherche notes existantes notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) @@ -566,102 +597,20 @@ def notes_add( etudids_with_decision = [] try: for etudid, value in notes: - changed = False - if etudid not in notes_db: - # nouvelle note - if value != scu.NOTES_SUPPRESS: - if do_it: - args = { - "etudid": etudid, - "evaluation_id": evaluation_id, - "value": value, - "comment": comment, - "uid": user.id, - "date": now, - } - ndb.quote_dict(args) - # Note: le conflit ci-dessous peut arriver si un autre thread - # a modifié la base après qu'on ait lu notes_db - cursor.execute( - """INSERT INTO notes_notes - (etudid, evaluation_id, value, comment, date, uid) - VALUES - (%(etudid)s,%(evaluation_id)s,%(value)s, - %(comment)s,%(date)s,%(uid)s) - ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key - DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s, - value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s - """, - args, - ) - changed = True - else: - # il y a deja une note - oldval = notes_db[etudid]["value"] - if type(value) != type(oldval): - changed = True - elif isinstance(value, float) and ( - abs(value - oldval) > scu.NOTES_PRECISION - ): - changed = True - elif value != oldval: - changed = True - if changed: - # recopie l'ancienne note dans notes_notes_log, puis update - if do_it: - cursor.execute( - """INSERT INTO notes_notes_log - (etudid,evaluation_id,value,comment,date,uid) - SELECT etudid, evaluation_id, value, comment, date, uid - FROM notes_notes - WHERE etudid=%(etudid)s - and evaluation_id=%(evaluation_id)s - """, - {"etudid": etudid, "evaluation_id": evaluation_id}, - ) - args = { - "etudid": etudid, - "evaluation_id": evaluation_id, - "value": value, - "date": now, - "comment": comment, - "uid": user.id, - } - ndb.quote_dict(args) - if value != scu.NOTES_SUPPRESS: - if do_it: - cursor.execute( - """UPDATE notes_notes - SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s - WHERE etudid = %(etudid)s - and evaluation_id = %(evaluation_id)s - """, - args, - ) - else: # suppression ancienne note - if do_it: - log( - f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={ - etudid}, oldval={oldval}""" - ) - cursor.execute( - """DELETE FROM notes_notes - WHERE etudid = %(etudid)s - AND evaluation_id = %(evaluation_id)s - """, - args, - ) - # garde trace de la suppression dans l'historique: - args["value"] = scu.NOTES_SUPPRESS - cursor.execute( - """INSERT INTO notes_notes_log - (etudid,evaluation_id,value,comment,date,uid) - VALUES - (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s) - """, - args, - ) - nb_suppress += 1 + changed, suppressed = _record_note( + cursor, + notes_db, + etudid, + evaluation_id, + value, + comment=comment, + user=user, + date=now, + do_it=do_it, + ) + if suppressed: + nb_suppress += 1 + if changed: etudids_changed.append(etudid) if res.etud_has_decision(etudid, include_rcues=False): @@ -678,7 +627,108 @@ def notes_add( cnx.commit() sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.EvaluationCache.delete(evaluation_id) - return etudids_changed, nb_suppress, etudids_with_decision + return etudids_changed, nb_suppress, etudids_with_decision, messages + + +def _record_note( + cursor, + notes_db, + etudid: int, + evaluation_id: int, + value: float, + comment: str = "", + user: User = None, + date=None, + do_it=False, +): + "Enregistrement de la note en base" + changed = False + suppressed = False + args = { + "etudid": etudid, + "evaluation_id": evaluation_id, + "value": value, + # convention scodoc7 quote comment: + "comment": (html.escape(comment) if isinstance(comment, str) else comment), + "uid": user.id, + "date": date, + } + if etudid not in notes_db: + # nouvelle note + if value != scu.NOTES_SUPPRESS: + if do_it: + # Note: le conflit ci-dessous peut arriver si un autre thread + # a modifié la base après qu'on ait lu notes_db + cursor.execute( + """INSERT INTO notes_notes + (etudid, evaluation_id, value, comment, date, uid) + VALUES + (%(etudid)s,%(evaluation_id)s,%(value)s, + %(comment)s,%(date)s,%(uid)s) + ON CONFLICT ON CONSTRAINT notes_notes_etudid_evaluation_id_key + DO UPDATE SET etudid=%(etudid)s, evaluation_id=%(evaluation_id)s, + value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s + """, + args, + ) + changed = True + else: + # il y a deja une note + oldval = notes_db[etudid]["value"] + if type(value) != type(oldval): + changed = True + elif isinstance(value, float) and (abs(value - oldval) > scu.NOTES_PRECISION): + changed = True + elif value != oldval: + changed = True + if changed: + # recopie l'ancienne note dans notes_notes_log, puis update + if do_it: + cursor.execute( + """INSERT INTO notes_notes_log + (etudid,evaluation_id,value,comment,date,uid) + SELECT etudid, evaluation_id, value, comment, date, uid + FROM notes_notes + WHERE etudid=%(etudid)s + and evaluation_id=%(evaluation_id)s + """, + args, + ) + if value != scu.NOTES_SUPPRESS: + if do_it: + cursor.execute( + """UPDATE notes_notes + SET value=%(value)s, comment=%(comment)s, date=%(date)s, uid=%(uid)s + WHERE etudid = %(etudid)s + and evaluation_id = %(evaluation_id)s + """, + args, + ) + else: # suppression ancienne note + if do_it: + log( + f"""notes_add, suppress, evaluation_id={evaluation_id}, etudid={ + etudid}, oldval={oldval}""" + ) + cursor.execute( + """DELETE FROM notes_notes + WHERE etudid = %(etudid)s + AND evaluation_id = %(evaluation_id)s + """, + args, + ) + # garde trace de la suppression dans l'historique: + args["value"] = scu.NOTES_SUPPRESS + cursor.execute( + """INSERT INTO notes_notes_log + (etudid,evaluation_id,value,comment,date,uid) + VALUES + (%(etudid)s, %(evaluation_id)s, %(value)s, %(comment)s, %(date)s, %(uid)s) + """, + args, + ) + suppressed = True + return changed, suppressed def saisie_notes_tableur(evaluation_id, group_ids=()): @@ -703,7 +753,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()): ) page_title = "Saisie des notes" + ( - f"""de {evaluation.description}""" if evaluation.description else "" + f""" de {evaluation.description}""" if evaluation.description else "" ) # Informations sur les groupes à afficher: @@ -797,9 +847,13 @@ def saisie_notes_tableur(evaluation_id, group_ids=()): }"> Revenir au tableau de bord du module     + Charger un autre fichier de notes +     Charger d'autres notes dans cette évaluation + }">Formulaire de saisie des notes

""" ) else: @@ -1015,7 +1069,7 @@ def saisie_notes(evaluation_id: int, group_ids: list = None): "Autres opérations", [ { - "title": "Saisie par fichier tableur", + "title": "Saisir par fichier tableur", "id": "menu_saisie_tableur", "endpoint": "notes.saisie_notes_tableur", "args": { @@ -1126,7 +1180,7 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in # Note actuelle de l'étudiant: if etudid in notes_db: - e["val"] = _displayNote(notes_db[etudid]["value"]) + e["val"] = _display_note(notes_db[etudid]["value"]) comment = notes_db[etudid]["comment"] if comment is None: comment = "" @@ -1368,7 +1422,7 @@ def save_notes( # valid_notes, _, _, _, _ = _check_notes(notes, evaluation) if valid_notes: - etudids_changed, _, etudids_with_decision = notes_add( + etudids_changed, _, etudids_with_decision, messages = notes_add( current_user, evaluation.id, valid_notes, comment=comment, do_it=True ) ScolarNews.add( @@ -1386,12 +1440,14 @@ def save_notes( etudid: get_note_history_menu(evaluation.id, etudid) for etudid in etudids_changed }, + "messages": messages, } else: result = { "etudids_changed": [], "etudids_with_decision": [], "history_menu": [], + "messages": [], } return result @@ -1420,7 +1476,7 @@ def get_note_history_menu(evaluation_id: int, etudid: int) -> str: first = True for i in history: jt = i["date"].strftime("le %d/%m/%Y à %H:%M") + " (%s)" % i["user_name"] - dispnote = _displayNote(i["value"]) + dispnote = _display_note(i["value"]) if first: nv = "" # ne repete pas la valeur de la note courante else: diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index f1840386..ff5bca9b 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -180,7 +180,7 @@ def external_ue_inscrit_et_note( description="note externe", ) # Saisie des notes - _, _, _ = sco_saisie_notes.notes_add( + _, _, _, _ = sco_saisie_notes.notes_add( current_user, evaluation.id, list(notes_etuds.items()), diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index eb19901b..f1ab3191 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -35,7 +35,7 @@ from flask import url_for, g, request from flask_login import current_user -from app import db, Departement +from app import Departement from app.auth.models import Permission, Role, User, UserRole from app.models import ScoDocSiteConfig, USERNAME_STR_LEN diff --git a/sco_version.py b/sco_version.py index 0a1eeda1..2d37b22b 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.975" +SCOVERSION = "9.6.976" SCONAME = "ScoDoc" diff --git a/tests/unit/test_notes_modules.py b/tests/unit/test_notes_modules.py index 1542eaec..7a5a9ea5 100644 --- a/tests/unit/test_notes_modules.py +++ b/tests/unit/test_notes_modules.py @@ -2,6 +2,7 @@ Vérif moyennes de modules des bulletins et aussi moyennes modules et UE internes (via nt) """ + import datetime import numpy as np from flask import g @@ -107,16 +108,16 @@ def test_notes_modules(test_client): # --- Notes ordinaires note_1 = 12.0 note_2 = 13.0 - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etuds[0]["etudid"], note=note_1 ) - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etuds[0]["etudid"], note=note_2 ) - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=note_1 / 2 ) - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etuds[1]["etudid"], note=note_2 / 3 ) b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -139,22 +140,20 @@ def test_notes_modules(test_client): ) # Absence à une évaluation - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etudid, note=None ) # abs - _, _, _ = G.create_note( - evaluation_id=e2["evaluation_id"], etudid=etudid, note=note_2 - ) + _ = G.create_note(evaluation_id=e2["evaluation_id"], etudid=etudid, note=note_2) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) note_th = (coef_1 * 0.0 + coef_2 * note_2) / (coef_1 + coef_2) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(note_th) # Absences aux deux évaluations - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etudid, note=None ) # abs - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etudid, note=None ) # abs b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -171,10 +170,8 @@ def test_notes_modules(test_client): ) # Note excusée EXC <-> scu.NOTES_NEUTRALISE - _, _, _ = G.create_note( - evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1 - ) - _, _, _ = G.create_note( + _ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1) + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE ) # EXC b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -190,10 +187,8 @@ def test_notes_modules(test_client): expected_moy_ue=note_1, ) # Note en attente ATT <-> scu.NOTES_ATTENTE - _, _, _ = G.create_note( - evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1 - ) - _, _, _ = G.create_note( + _ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=note_1) + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE ) # ATT b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -209,10 +204,10 @@ def test_notes_modules(test_client): expected_moy_ue=note_1, ) # Neutralisation (EXC) des 2 évals - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE ) # EXC - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE ) # EXC b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -228,10 +223,10 @@ def test_notes_modules(test_client): expected_moy_ue=np.nan, ) # Attente (ATT) sur les 2 evals - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE ) # ATT - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_ATTENTE ) # ATT b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -290,7 +285,7 @@ def test_notes_modules(test_client): {"etudid": etudid, "moduleimpl_id": moduleimpl_id}, formsemestre_id=formsemestre_id, ) - _, _, _ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=12.5) + _ = G.create_note(evaluation_id=e1["evaluation_id"], etudid=etudid, note=12.5) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) mod_stats = nt.get_mod_stats(moduleimpl_id) @@ -318,9 +313,7 @@ def test_notes_modules(test_client): description="evaluation mod 2", coefficient=1.0, ) - _, _, _ = G.create_note( - evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=19.5 - ) + _ = G.create_note(evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=19.5) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ue_status = nt.get_etud_ue_status(etudid, ue_id) @@ -328,22 +321,20 @@ def test_notes_modules(test_client): # Moyenne d'UE si l'un des modules est EXC ("NA") # 2 modules, notes EXC dans le premier, note valide n dans le second # la moyenne de l'UE doit être n - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE ) # EXC - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etudid, note=scu.NOTES_NEUTRALISE ) # EXC - _, _, _ = G.create_note( - evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=12.5 - ) - _, _, _ = G.create_note( + _ = G.create_note(evaluation_id=e_m2["evaluation_id"], etudid=etudid, note=12.5) + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0 ) - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e2["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0 ) - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e_m2["evaluation_id"], etudid=etuds[1]["etudid"], note=11.0 ) @@ -407,12 +398,12 @@ def test_notes_modules_att_dem(test_client): coefficient=coef_1, ) # Attente (ATT) sur les 2 evals - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etuds[0]["etudid"], note=scu.NOTES_ATTENTE, ) # ATT - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=scu.NOTES_ATTENTE, @@ -455,7 +446,7 @@ def test_notes_modules_att_dem(test_client): assert note_e1 == scu.NOTES_ATTENTE # XXXX un peu contestable # Saisie note ABS pour le deuxième etud - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e1["evaluation_id"], etudid=etuds[1]["etudid"], note=None ) nt = check_nt( diff --git a/tests/unit/test_notes_rattrapage.py b/tests/unit/test_notes_rattrapage.py index 22cd1e47..4ee29c32 100644 --- a/tests/unit/test_notes_rattrapage.py +++ b/tests/unit/test_notes_rattrapage.py @@ -72,8 +72,8 @@ def test_notes_rattrapage(test_client): evaluation_type=Evaluation.EVALUATION_RATTRAPAGE, ) etud = etuds[0] - _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0) - _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=11.0) + _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=12.0) + _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=11.0) # --- Vérifications internes structures ScoDoc formsemestre = db.session.get(FormSemestre, formsemestre_id) @@ -98,23 +98,21 @@ def test_notes_rattrapage(test_client): # Note moyenne: ici le ratrapage est inférieur à la note: assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(12.0) # rattrapage > moyenne: - _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=18.0) + _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=18.0) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(18.0) # rattrapage vs absences - _, _, _ = G.create_note( - evaluation_id=e["id"], etudid=etud["etudid"], note=None - ) # abs - _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=17.0) + _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=None) # abs + _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=17.0) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(17.0) # et sans note de rattrapage - _, _, _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=10.0) - _, _, _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=None) + _ = G.create_note(evaluation_id=e["id"], etudid=etud["etudid"], note=10.0) + _ = G.create_note(evaluation_id=e_rat["id"], etudid=etud["etudid"], note=None) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) @@ -159,18 +157,14 @@ def test_notes_rattrapage(test_client): assert len(mod_res.get_evaluations_completes(moduleimpl)) == 2 # Saisie note session 2: - _, _, _ = G.create_note( - evaluation_id=e_session2["id"], etudid=etud["etudid"], note=5.0 - ) + _ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["etudid"], note=5.0) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) # Note moyenne: utilise session 2 même si inférieure assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(5.0) - _, _, _ = G.create_note( - evaluation_id=e_session2["id"], etudid=etud["etudid"], note=20.0 - ) + _ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["etudid"], note=20.0) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) @@ -178,16 +172,14 @@ def test_notes_rattrapage(test_client): assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(20.0) # Met la note session2 à ABS (None) - _, _, _ = G.create_note( - evaluation_id=e_session2["id"], etudid=etud["etudid"], note=None - ) + _ = G.create_note(evaluation_id=e_session2["id"], etudid=etud["etudid"], note=None) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) # Note moyenne: zéro car ABS assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(0.0) # Supprime note session 2 - _, _, _ = G.create_note( + _ = G.create_note( evaluation_id=e_session2["id"], etudid=etud["etudid"], note=scu.NOTES_SUPPRESS ) b = sco_bulletins.formsemestre_bulletinetud_dict( @@ -216,18 +208,14 @@ def test_notes_rattrapage(test_client): # Note moyenne sans bonus assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0) # Saisie note bonus - _, _, _ = G.create_note( - evaluation_id=e_bonus["id"], etudid=etud["etudid"], note=1.0 - ) + _ = G.create_note(evaluation_id=e_bonus["id"], etudid=etud["etudid"], note=1.0) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) # Note moyenne sans bonus assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(11.0) # Négatif, avec clip à zéro - _, _, _ = G.create_note( - evaluation_id=e_bonus["id"], etudid=etud["etudid"], note=-20.0 - ) + _ = G.create_note(evaluation_id=e_bonus["id"], etudid=etud["etudid"], note=-20.0) b = sco_bulletins.formsemestre_bulletinetud_dict( sem["formsemestre_id"], etud["etudid"] ) diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index b3bba2e2..e159391c 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -105,7 +105,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre: # --- Saisie toutes les notes de l'évaluation for idx, etud in enumerate(etuds): - etudids_changed, nb_suppress, existing_decisions = G.create_note( + etudids_changed, nb_suppress, existing_decisions, messages = G.create_note( evaluation_id=e1.id, etudid=etud["etudid"], note=NOTES_T[idx % len(NOTES_T)], @@ -113,6 +113,7 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre: assert not existing_decisions assert nb_suppress == 0 assert len(etudids_changed) == 1 + assert messages == [] # --- Vérifie que les notes sont prises en compte: b = sco_bulletins.formsemestre_bulletinetud_dict(formsemestre_id, etud["etudid"]) @@ -139,11 +140,12 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre: db.session.commit() # Saisie les notes des 5 premiers étudiants: for idx, etud in enumerate(etuds[:5]): - etudids_changed, nb_suppress, existing_decisions = G.create_note( + etudids_changed, nb_suppress, existing_decisions, messages = G.create_note( evaluation_id=e2.id, etudid=etud["etudid"], note=NOTES_T[idx % len(NOTES_T)], ) + assert messages == [] # Cette éval n'est pas complète etat = sco_evaluations.do_evaluation_etat(e2.id) assert etat["evalcomplete"] is False @@ -162,11 +164,12 @@ def run_sco_basic(verbose=False, dept=None) -> FormSemestre: # Saisie des notes qui manquent: for idx, etud in enumerate(etuds[5:]): - etudids_changed, nb_suppress, existing_decisions = G.create_note( + etudids_changed, nb_suppress, existing_decisions, messages = G.create_note( evaluation_id=e2.id, etudid=etud["etudid"], note=NOTES_T[idx % len(NOTES_T)], ) + assert messages == [] etat = sco_evaluations.do_evaluation_etat(e2.id) assert etat["evalcomplete"] assert etat["nb_att"] == 0 From 8a49d99292a9dd80e56838da3d65defd57be9a5b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jun 2024 01:21:33 +0200 Subject: [PATCH 2/8] =?UTF-8?q?Clonage=20semestre:=20am=C3=A9liore=20code?= =?UTF-8?q?=20+=20qq=20modifs=20cosm=C3=A9tiques?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/formsemestre.py | 73 +++++++++++++++++++-- app/models/groups.py | 4 ++ app/scodoc/sco_edit_ue.py | 4 +- app/scodoc/sco_formsemestre.py | 5 +- app/scodoc/sco_formsemestre_edit.py | 94 ++++++++++++++++----------- app/scodoc/sco_formsemestre_status.py | 4 +- app/scodoc/sco_utils.py | 6 +- app/scodoc/sco_vdi.py | 4 +- 8 files changed, 143 insertions(+), 51 deletions(-) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 738a4744..eb10f57b 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -36,6 +36,7 @@ from app.models.config import ScoDocSiteConfig from app.models.departements import Departement from app.models.etudiants import Identite from app.models.evaluations import Evaluation +from app.models.events import ScolarNews from app.models.formations import Formation from app.models.groups import GroupDescr, Partition from app.models.moduleimpls import ( @@ -207,6 +208,70 @@ class FormSemestre(models.ScoDocModel): ).first_or_404() return cls.query.filter_by(id=formsemestre_id).first_or_404() + @classmethod + def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre": + """Création d'un formsemestre, avec toutes les valeurs par défaut + et notification (sauf si silent). + Crée la partition par défaut. + """ + # was sco_formsemestre.do_formsemestre_create + if "dept_id" not in args: + args["dept_id"] = g.scodoc_dept_id + formsemestre: "FormSemestre" = cls.create_from_dict(args) + db.session.flush() + for etape in args["etapes"]: + formsemestre.add_etape(etape) + db.session.commit() + for u in args["responsables"]: + formsemestre.responsables.append(u) + # create default partition + partition = Partition( + formsemestre=formsemestre, partition_name=None, numero=1000000 + ) + db.session.add(partition) + partition.create_group(default=True) + db.session.commit() + + if not silent: + url = url_for( + "notes.formsemestre_status", + scodoc_dept=formsemestre.departement.acronym, + formsemestre_id=formsemestre.id, + ) + ScolarNews.add( + typ=ScolarNews.NEWS_SEM, + text=f"""Création du semestre {formsemestre.titre}""", + url=url, + max_frequency=0, + ) + + return formsemestre + + @classmethod + def convert_dict_fields(cls, args: dict) -> dict: + """Convert fields in the given dict. + args: dict with args in application. + returns: dict to store in model's db. + """ + if "date_debut" in args: + args["date_debut"] = scu.convert_fr_date(args["date_debut"]) + if "date_fin" in args: + args["date_fin"] = scu.convert_fr_date(args["date_debut"]) + if "etat" in args: + args["etat"] = bool(args["etat"]) + if "bul_bgcolor" in args: + args["bul_bgcolor"] = args.get("bul_bgcolor") or "white" + if "titre" in args: + args["titre"] = args.get("titre") or "sans titre" + return args + + @classmethod + def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict: + """Returns a copy of dict with only the keys belonging to the Model and not in excluded. + Add 'etapes' to excluded.""" + # on ne peut pas affecter directement etapes + return super().filter_model_attributes(data, (excluded or set()) | {"etapes"}) + def sort_key(self) -> tuple: """clé pour tris par ordre de date_debut, le plus ancien en tête (pour avoir le plus récent d'abord, sort avec reverse=True)""" @@ -729,7 +794,7 @@ class FormSemestre(models.ScoDocModel): FormSemestre.titre, ) - def etapes_apo_vdi(self) -> list[ApoEtapeVDI]: + def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]: "Liste des vdis" # was read_formsemestre_etapes return [e.as_apovdi() for e in self.etapes if e.etape_apo] @@ -742,9 +807,9 @@ class FormSemestre(models.ScoDocModel): return "" return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape])) - def add_etape(self, etape_apo: str): + def add_etape(self, etape_apo: str | ApoEtapeVDI): "Ajoute une étape" - etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo) + etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=str(etape_apo)) db.session.add(etape) def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]: @@ -1271,7 +1336,7 @@ class FormSemestreEtape(db.Model): def __str__(self): return self.etape_apo or "" - def as_apovdi(self) -> ApoEtapeVDI: + def as_apovdi(self) -> "ApoEtapeVDI": return ApoEtapeVDI(self.etape_apo) diff --git a/app/models/groups.py b/app/models/groups.py index 7250f1e6..68c7156b 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -93,6 +93,10 @@ class Partition(ScoDocModel): ): group.remove_etud(etud) + def is_default(self) -> bool: + "vrai si partition par défault (tous les étudiants)" + return not self.partition_name + def is_parcours(self) -> bool: "Vrai s'il s'agit de la partition de parcours" return self.partition_name == scu.PARTITION_PARCOURS diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 152103ae..9d8df2d4 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -1056,10 +1056,10 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); if current_user.has_permission(Permission.EditFormSemestre): H.append( f"""""" ) diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 0a9264ea..16daef1a 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -229,11 +229,14 @@ def etapes_apo_str(etapes): return ", ".join([str(x) for x in etapes]) -def do_formsemestre_create(args, silent=False): +def do_formsemestre_create( # DEPRECATED, use FormSemestre.create_formsemestre() + args, silent=False +): "create a formsemestre" from app.models import ScolarNews from app.scodoc import sco_groups + log("Warning: do_formsemestre_create is deprecated") cnx = ndb.GetDBConnexion() formsemestre_id = _formsemestreEditor.create(cnx, args) if args["etapes"]: diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 07717f59..70237d20 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -37,16 +37,17 @@ from app import db from app.auth.models import User from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import ( - Module, - ModuleImpl, - Evaluation, - UniteEns, - ScoDocSiteConfig, - ScolarFormSemestreValidation, - ScolarAutorisationInscription, ApcValidationAnnee, ApcValidationRCUE, + Evaluation, + FormSemestreUECoef, + Module, + ModuleImpl, + ScoDocSiteConfig, + ScolarAutorisationInscription, + ScolarFormSemestreValidation, ScolarNews, + UniteEns, ) from app.models.formations import Formation from app.models.formsemestre import FormSemestre @@ -439,12 +440,13 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N { "size": 32, "title": "Element(s) Apogée sem.:", - "explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.", - "allow_null": not sco_preferences.get_preference( - "always_require_apo_sem_codes" - ) - or (formsemestre and formsemestre.modalite == "EXT") - or (formsemestre.formation.is_apc()), + "explanation": """associé(s) au résultat du semestre (ex: VRTW1). + Inutile en BUT. Séparés par des virgules.""", + "allow_null": ( + not sco_preferences.get_preference("always_require_apo_sem_codes") + or (formsemestre and formsemestre.modalite == "EXT") + or (formsemestre and formsemestre.formation.is_apc()) + ), }, ) ) @@ -1250,7 +1252,7 @@ def formsemestre_clone(formsemestre_id): raise ScoValueError("id responsable invalide") new_formsemestre_id = do_formsemestre_clone( formsemestre_id, - resp.id, + resp, tf[2]["date_debut"], tf[2]["date_fin"], clone_evaluations=tf[2]["clone_evaluations"], @@ -1268,7 +1270,7 @@ def formsemestre_clone(formsemestre_id): def do_formsemestre_clone( orig_formsemestre_id, - responsable_id, # new resp. + responsable: User, # new resp. date_debut, date_fin, # 'dd/mm/yyyy' clone_evaluations=False, @@ -1281,49 +1283,63 @@ def do_formsemestre_clone( formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404( orig_formsemestre_id ) - orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id) - cnx = ndb.GetDBConnexion() # 1- create sem - args = orig_sem.copy() + args = formsemestre_orig.to_dict() del args["formsemestre_id"] - args["responsables"] = [responsable_id] + del args["id"] + del args["parcours"] # copiés ensuite + args["responsables"] = [responsable] args["date_debut"] = date_debut args["date_fin"] = date_fin args["etat"] = 1 # non verrouillé - formsemestre_id = sco_formsemestre.do_formsemestre_create(args) - log(f"created formsemestre {formsemestre_id}") - formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) + + formsemestre = FormSemestre.create_formsemestre(args) + log(f"created formsemestre {formsemestre}") # 2- create moduleimpls modimpl_orig: ModuleImpl for modimpl_orig in formsemestre_orig.modimpls: + assert isinstance(modimpl_orig, ModuleImpl) + assert isinstance(modimpl_orig.id, int) + log(f"cloning {modimpl_orig}") args = modimpl_orig.to_dict(with_module=False) - args["formsemestre_id"] = formsemestre_id + args["formsemestre_id"] = formsemestre.id modimpl_new = ModuleImpl.create_from_dict(args) + log(f"created ModuleImpl from {args}") db.session.flush() # copy enseignants for ens in modimpl_orig.enseignants: modimpl_new.enseignants.append(ens) db.session.add(modimpl_new) + db.session.flush() + log(f"new moduleimpl.id = {modimpl_new.id}") # optionally, copy evaluations if clone_evaluations: + e: Evaluation for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id): + log(f"cloning evaluation {e.id}") # copie en enlevant la date - new_eval = e.clone( - not_copying=("date_debut", "date_fin", "moduleimpl_id") - ) - new_eval.moduleimpl_id = modimpl_new.id + args = dict(e.__dict__) + args.pop("_sa_instance_state") + args.pop("id") + args["moduleimpl_id"] = modimpl_new.id + new_eval = Evaluation(**args) + db.session.add(new_eval) + db.session.commit() # Copie les poids APC de l'évaluation new_eval.set_ue_poids_dict(e.get_ue_poids_dict()) db.session.commit() # 3- copy uecoefs - objs = sco_formsemestre.formsemestre_uecoef_list( - cnx, args={"formsemestre_id": orig_formsemestre_id} - ) - for obj in objs: - args = obj.copy() - args["formsemestre_id"] = formsemestre_id - _ = sco_formsemestre.formsemestre_uecoef_create(cnx, args) + for ue_coef in FormSemestreUECoef.query.filter_by( + formsemestre_id=formsemestre_orig.id + ): + new_ue_coef = FormSemestreUECoef( + formsemestre_id=formsemestre.id, + ue_id=ue_coef.ue_id, + coefficient=ue_coef.coefficient, + ) + db.session.add(new_ue_coef) + db.session.flush() # NB: don't copy notes_formsemestre_custommenu (usually specific) @@ -1335,11 +1351,11 @@ def do_formsemestre_clone( if not prefs.is_global(pname): pvalue = prefs[pname] try: - prefs.base_prefs.set(formsemestre_id, pname, pvalue) + prefs.base_prefs.set(formsemestre.id, pname, pvalue) except ValueError: log( - "do_formsemestre_clone: ignoring old preference %s=%s for %s" - % (pname, pvalue, formsemestre_id) + f"""do_formsemestre_clone: ignoring old preference { + pname}={pvalue} for {formsemestre}""" ) # 5- Copie les parcours @@ -1350,10 +1366,10 @@ def do_formsemestre_clone( # 6- Copy partitions and groups if clone_partitions: sco_groups_copy.clone_partitions_and_groups( - orig_formsemestre_id, formsemestre_id + orig_formsemestre_id, formsemestre.id ) - return formsemestre_id + return formsemestre.id def formsemestre_delete(formsemestre_id: int) -> str | flask.Response: diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index fc7df731..8c3ac414 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -794,7 +794,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
{ 'Groupes de ' + partition.partition_name if partition.partition_name else - 'Tous les étudiants'} + ('aucun étudiant inscrit' if partition_is_empty else 'Tous les étudiants')}
{ "Assiduité" if not partition_is_empty else "" @@ -885,7 +885,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str: ) H.append("
") # /sem-groups-assi - if partition_is_empty: + if partition_is_empty and not partition.is_default(): H.append( '
Aucun groupe peuplé dans cette partition' ) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 07aaacae..bc5943e2 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -109,13 +109,17 @@ ETATS_INSCRIPTION = { } -def convert_fr_date(date_str: str, allow_iso=True) -> datetime.datetime: +def convert_fr_date( + date_str: str | datetime.datetime, allow_iso=True +) -> datetime.datetime: """Converti une date saisie par un humain français avant 2070 en un objet datetime. 12/2/1972 => 1972-02-12, 12/2/72 => 1972-02-12, mais 12/2/24 => 2024-02-12 Le pivot est 70. ScoValueError si date invalide. """ + if isinstance(date_str, datetime.datetime): + return date_str try: return datetime.datetime.strptime(date_str, DATE_FMT) except ValueError: diff --git a/app/scodoc/sco_vdi.py b/app/scodoc/sco_vdi.py index 09d1a90a..0ceca257 100644 --- a/app/scodoc/sco_vdi.py +++ b/app/scodoc/sco_vdi.py @@ -30,7 +30,7 @@ from app.scodoc.sco_exceptions import ScoValueError -class ApoEtapeVDI(object): +class ApoEtapeVDI: """Classe stockant le VDI avec le code étape (noms de fichiers maquettes et code semestres)""" _ETAPE_VDI_SEP = "!" @@ -118,7 +118,7 @@ class ApoEtapeVDI(object): else: return etape_vdi, "" - def concat_etape_vdi(self, etape, vdi=""): + def concat_etape_vdi(self, etape: str, vdi: str = "") -> str: if vdi: return self._ETAPE_VDI_SEP.join([etape, vdi]) else: From af557f9c934aedc005deb2ac22e934d411e636bb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jun 2024 20:40:13 +0200 Subject: [PATCH 3/8] Import admission: mode avec etudid --- app/scodoc/sco_import_etuds.py | 152 +++++++++++++++++++++------------ app/views/scolar.py | 62 +++++++++----- sco_version.py | 2 +- 3 files changed, 137 insertions(+), 79 deletions(-) diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index 0a569047..74362f92 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -28,7 +28,6 @@ """ Importation des étudiants à partir de fichiers CSV """ -import collections import io import os import re @@ -64,6 +63,7 @@ import app.scodoc.sco_utils as scu FORMAT_FILE = "format_import_etudiants.txt" # Champs modifiables via "Import données admission" +# (nom/prénom modifiables en mode "avec etudid") ADMISSION_MODIFIABLE_FIELDS = ( "code_nip", "code_ine", @@ -132,19 +132,27 @@ def sco_import_format(with_codesemestre=True): return r -def sco_import_format_dict(with_codesemestre=True): +def sco_import_format_dict(with_codesemestre=True, use_etudid: bool = False): """Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }""" fmt = sco_import_format(with_codesemestre=with_codesemestre) - R = collections.OrderedDict() + formats = {} for l in fmt: - R[l[0]] = { + formats[l[0]] = { "type": l[1], "table": l[2], "allow_nulls": l[3], "description": l[4], "aliases": l[5], } - return R + if use_etudid: + formats["etudid"] = { + "type": "int", + "table": "identite", + "allow_nulls": False, + "description": "", + "aliases": ["etudid", "id"], + } + return formats def sco_import_generate_excel_sample( @@ -188,8 +196,7 @@ def sco_import_generate_excel_sample( groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids) members = groups_infos.members log( - "sco_import_generate_excel_sample: group_ids=%s %d members" - % (group_ids, len(members)) + f"sco_import_generate_excel_sample: group_ids={group_ids}, {len(members)} members" ) titles = ["etudid"] + titles titles_styles = [style] + titles_styles @@ -234,21 +241,26 @@ def students_import_excel( exclude_cols=["photo_filename"], ) if return_html: - if formsemestre_id: - dest = url_for( + dest_url = ( + url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ) - else: - dest = url_for("notes.index_html", scodoc_dept=g.scodoc_dept) + if formsemestre_id + else url_for("notes.index_html", scodoc_dept=g.scodoc_dept) + ) H = [html_sco_header.sco_header(page_title="Import etudiants")] H.append("
    ") for d in diag: - H.append("
  • %s
  • " % d) - H.append("
") - H.append("

Import terminé !

") - H.append('

Continuer

' % dest) + H.append(f"
  • {d}
  • ") + H.append( + f""" + ) +

    Import terminé !

    +

    Continuer

    + """ + ) return "\n".join(H) + html_sco_header.sco_footer() @@ -308,13 +320,13 @@ def scolars_import_excel_file( titleslist = [] for t in fs: if t not in titles: - raise ScoValueError('Colonne invalide: "%s"' % t) + raise ScoValueError(f'Colonne invalide: "{t}"') titleslist.append(t) # # ok, same titles # Start inserting data, abort whole transaction in case of error created_etudids = [] np_imported_homonyms = 0 - GroupIdInferers = {} + group_id_inferer = {} try: # --- begin DB transaction linenum = 0 for line in data[1:]: @@ -429,7 +441,7 @@ def scolars_import_excel_file( _import_one_student( formsemestre_id, values, - GroupIdInferers, + group_id_inferer, annee_courante, created_etudids, linenum, @@ -496,13 +508,14 @@ def scolars_import_excel_file( def students_import_admission( - csvfile, type_admission="", formsemestre_id=None, return_html=True -): + csvfile, type_admission="", formsemestre_id=None, return_html=True, use_etudid=False +) -> str: "import donnees admission from Excel file (v2016)" diag = scolars_import_admission( csvfile, formsemestre_id=formsemestre_id, type_admission=type_admission, + use_etudid=use_etudid, ) if return_html: H = [html_sco_header.sco_header(page_title="Import données admissions")] @@ -524,6 +537,7 @@ def students_import_admission( ) return "\n".join(H) + html_sco_header.sco_footer() + return "" def _import_one_student( @@ -599,13 +613,15 @@ def _is_new_ine(cnx, code_ine): # ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB) -def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None): +def scolars_import_admission( + datafile, formsemestre_id=None, type_admission=None, use_etudid=False +): """Importe données admission depuis un fichier Excel quelconque - par exemple ceux utilisés avec APB + par exemple ceux utilisés avec APB, avec ou sans etudid Cherche dans ce fichier les étudiants qui correspondent à des inscrits du semestre formsemestre_id. - Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait + Si le fichier n'a pas d'etudid (use_etudid faux), la correspondance se fait via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux étant ignorés). @@ -617,23 +633,24 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None dans le fichier importé) du champ type_admission. Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré. - TODO: - - choix onglet du classeur """ - log(f"scolars_import_admission: formsemestre_id={formsemestre_id}") + diag: list[str] = [] members = sco_groups.get_group_members( sco_groups.get_default_group(formsemestre_id) ) etuds_by_nomprenom = {} # { nomprenom : etud } - diag = [] - for m in members: - np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"])) - if np in etuds_by_nomprenom: - msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"]) - log(msg) - diag.append(msg) - etuds_by_nomprenom[np] = m + etuds_by_etudid = {} # { etudid : etud } + if use_etudid: + etuds_by_etudid = {m["etudid"]: m for m in members} + else: + for m in members: + np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"])) + if np in etuds_by_nomprenom: + msg = f"""Attention: hononymie pour {m["nom"]} {m["prenom"]}""" + log(msg) + diag.append(msg) + etuds_by_nomprenom[np] = m exceldata = datafile.read() diag2, data = sco_excel.excel_bytes_to_list(exceldata) @@ -644,19 +661,29 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None titles = data[0] # idx -> ('field', convertor) - fields = adm_get_fields(titles, formsemestre_id) - idx_nom = None - idx_prenom = None + fields = adm_get_fields(titles, formsemestre_id, use_etudid=use_etudid) + idx_nom = idx_prenom = idx_etudid = None for idx, field in fields.items(): - if field[0] == "nom": - idx_nom = idx - if field[0] == "prenom": - idx_prenom = idx - if (idx_nom is None) or (idx_prenom is None): + match field[0]: + case "nom": + idx_nom = idx + case "prenom": + idx_prenom = idx + case "etudid": + idx_etudid = idx + + if (not use_etudid and ((idx_nom is None) or (idx_prenom is None))) or ( + use_etudid and idx_etudid is None + ): log("fields indices=" + ", ".join([str(x) for x in fields])) - log("fields titles =" + ", ".join([fields[x][0] for x in fields])) + log("fields titles =" + ", ".join([x[0] for x in fields.values()])) raise ScoFormatError( - "scolars_import_admission: colonnes nom et prenom requises", + ( + """colonne etudid requise + (si l'option "Utiliser l'identifiant d'étudiant ScoDoc" est cochée)""" + if use_etudid + else "colonnes nom et prenom requises" + ), dest_url=url_for( "scolar.form_students_import_infos_admissions", scodoc_dept=g.scodoc_dept, @@ -665,18 +692,31 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None ) modifiable_fields = set(ADMISSION_MODIFIABLE_FIELDS) + if use_etudid: + modifiable_fields |= {"nom", "prenom"} nline = 2 # la premiere ligne de donnees du fichier excel est 2 n_import = 0 for line in data[1:]: - # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) - nom = adm_normalize_string(line[idx_nom]) - prenom = adm_normalize_string(line[idx_prenom]) - if (nom, prenom) not in etuds_by_nomprenom: - msg = f"""Étudiant {line[idx_nom]} {line[idx_prenom]} inexistant""" - diag.append(msg) + if use_etudid: + try: + etud = etuds_by_etudid.get(int(line[idx_etudid])) + except ValueError: + etud = None + if not etud: + msg = f"""Étudiant avec code etudid={line[idx_etudid]} inexistant""" + diag.append(msg) else: - etud = etuds_by_nomprenom[(nom, prenom)] + # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) + nom = adm_normalize_string(line[idx_nom]) + prenom = adm_normalize_string(line[idx_prenom]) + etud = etuds_by_nomprenom.get((nom, prenom)) + if not etud: + msg = ( + f"""Étudiant {line[idx_nom]} {line[idx_prenom]} inexistant""" + ) + diag.append(msg) + if etud: cur_adm = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0] # peuple les champs presents dans le tableau args = {} @@ -758,19 +798,19 @@ def adm_normalize_string(s): ) -def adm_get_fields(titles, formsemestre_id): +def adm_get_fields(titles, formsemestre_id: int, use_etudid: bool = False): """Cherche les colonnes importables dans les titres (ligne 1) du fichier excel return: { idx : (field_name, convertor) } """ - format_dict = sco_import_format_dict() + format_dict = sco_import_format_dict(use_etudid=use_etudid) fields = {} idx = 0 for title in titles: title_n = adm_normalize_string(title) - for k in format_dict: - for v in format_dict[k]["aliases"]: + for k, fmt in format_dict.items(): + for v in fmt["aliases"]: if adm_normalize_string(v) == title_n: - typ = format_dict[k]["type"] + typ = fmt["type"] if typ == "real": convertor = adm_convert_real elif typ == "integer" or typ == "int": diff --git a/app/views/scolar.py b/app/views/scolar.py index 0cb128ae..34e7384e 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -2365,28 +2365,36 @@ def form_students_import_infos_admissions(formsemestre_id=None): Les données sont affichées sur les fiches individuelles des étudiants.

    -

    - Importer ici la feuille excel utilisée pour envoyer le classement Parcoursup. - Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, - les autres lignes de la feuille seront ignorées. - Et seules les colonnes intéressant ScoDoc - seront importées: il est inutile d'éliminer les autres. -
    - Seules les données "admission" seront modifiées - (et pas l'identité de l'étudiant). -
    - Les colonnes "nom" et "prenom" sont requises, ou bien une colonne "etudid". -

    -

    - Avant d'importer vos données, il est recommandé d'enregistrer - les informations actuelles: - exporter les données actuelles de ScoDoc - (ce fichier peut être ré-importé après d'éventuelles modifications) -

    - """, +
    +

    + Vous pouvez importer ici la feuille excel utilisée pour envoyer + le classement Parcoursup. + Seuls les étudiants actuellement inscrits dans ce semestre ScoDoc seront affectés, + les autres lignes de la feuille seront ignorées. + Et seules les colonnes intéressant ScoDoc + seront importées: il est inutile d'éliminer les autres. +

    +

    + Seules les données "admission" seront modifiées + (et pas l'identité de l'étudiant). +

    +

    + Les colonnes "nom" et "prenom" sont requises, + ou bien une colonne "etudid" si la case + "Utiliser l'identifiant d'étudiant ScoDoc" est cochée. + +

    +

    + Avant d'importer vos données, il est recommandé d'enregistrer + les informations actuelles: + exporter les données actuelles de ScoDoc + (ce fichier peut être ré-importé après d'éventuelles modifications) +

    +
    + """, ] tf = TrivialFormulator( @@ -2397,6 +2405,15 @@ def form_students_import_infos_admissions(formsemestre_id=None): "csvfile", {"title": "Fichier Excel:", "input_type": "file", "size": 40}, ), + ( + "use_etudid", + { + "input_type": "boolcheckbox", + "title": "Utiliser l'identifiant d'étudiant ScoDoc (etudid)", + "explanation": """si cochée, utilise le code pour retrouver dans ScoDoc + les étudiants du fichier excel. Sinon, utilise les noms/prénoms.""", + }, + ), ( "type_admission", { @@ -2436,6 +2453,7 @@ def form_students_import_infos_admissions(formsemestre_id=None): tf[2]["csvfile"], type_admission=tf[2]["type_admission"], formsemestre_id=formsemestre_id, + use_etudid=tf[2]["use_etudid"], ) diff --git a/sco_version.py b/sco_version.py index 2d37b22b..69426191 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.976" +SCOVERSION = "9.6.977" SCONAME = "ScoDoc" From e877e04cc625aa08964648cc03c3cd3894ad4ad1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 18 Jun 2024 21:00:56 +0200 Subject: [PATCH 4/8] =?UTF-8?q?Export=20Apog=C3=A9e:=20corrige=20cas=20san?= =?UTF-8?q?s=20d=C3=A9cisions=20de=20jury?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/sco_apogee_csv.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 80d1e8e4..2bf9685b 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -569,8 +569,7 @@ class ApoEtud(dict): # prend le plus récent avec décision for formsemestre in cur_formsemestres: res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - has_decision = res.etud_has_decision(self.etud.id) - if has_decision: + if apo_data.export_res_sdj or res.etud_has_decision(self.etud.id): cur_formsemestre = formsemestre self.cur_res = res break @@ -639,7 +638,7 @@ class ApoEtud(dict): has_decision = res.etud_has_decision(self.etud.id) else: has_decision = res.get_etud_decision_sem(self.etud.id) - if has_decision: + if has_decision or apo_data.export_res_sdj: autre_formsemestre = formsemestre break if autre_formsemestre is None: From 6224f37e4a565517e405f9d035d08a2903bff6e8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jun 2024 16:34:45 +0200 Subject: [PATCH 5/8] =?UTF-8?q?Export=20table=20comptes=20crois=C3=A9s:=20?= =?UTF-8?q?fix=20#934?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/scodoc/gen_tables.py | 7 +++++-- app/scodoc/sco_report.py | 16 +++++++++------- sco_version.py | 2 +- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/scodoc/gen_tables.py b/app/scodoc/gen_tables.py index a58125f9..83752b5e 100644 --- a/app/scodoc/gen_tables.py +++ b/app/scodoc/gen_tables.py @@ -313,9 +313,12 @@ class GenTable: T.append(l + [self.bottom_titles.get(cid, "") for cid in self.columns_ids]) return T - def get_titles_list(self): + def get_titles_list(self, with_lines_titles=True): "list of titles" - return [self.titles.get(cid, "") for cid in self.columns_ids] + titles = [self.titles.get(cid, "") for cid in self.columns_ids] + if with_lines_titles: + titles.insert(0, "") + return titles def gen(self, fmt="html", columns_ids=None): """Build representation of the table in the specified format. diff --git a/app/scodoc/sco_report.py b/app/scodoc/sco_report.py index ab960406..db037e70 100644 --- a/app/scodoc/sco_report.py +++ b/app/scodoc/sco_report.py @@ -248,8 +248,6 @@ def formsemestre_report( result="codedecision", category_name="", result_name="", - title="Statistiques", - only_primo=None, ): """ Tableau sur résultats (result) par type de category bac @@ -277,9 +275,6 @@ def formsemestre_report( f"Répartition des résultats par {category_name}, semestre {sem['titreannee']}" ) tab.html_caption = f"Répartition des résultats par {category_name}." - tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id) - if only_primo: - tab.base_url += "&only_primo=on" return tab @@ -326,8 +321,15 @@ def formsemestre_report_counts( category=category, result=result, category_name=category_name, - title=title, - only_primo=only_primo, + ) + tab.base_url = url_for( + "notes.formsemestre_report_counts", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + category=category, + only_primo=int(bool(only_primo)), + result=result, + group_ids=group_ids, ) if len(formsemestre.inscriptions) == 0: F = ["""

    Aucun étudiant

    """] diff --git a/sco_version.py b/sco_version.py index 69426191..0001c080 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.6.977" +SCOVERSION = "9.6.978" SCONAME = "ScoDoc" From 070c9ea36f7fe745b181336349282e0804157a55 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jun 2024 18:28:23 +0200 Subject: [PATCH 6/8] Diplome BUT: PV et lettre indiv. #929 --- app/but/cursus_but.py | 160 ++++++++++++----------- app/but/jury_but.py | 4 +- app/but/jury_but_pv.py | 12 +- app/models/but_validations.py | 1 + app/scodoc/sco_pv_lettres_inviduelles.py | 9 +- 5 files changed, 103 insertions(+), 83 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index c72938a8..b9a09f0b 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -29,7 +29,7 @@ from app.models.but_refcomp import ( ApcReferentielCompetences, ) from app.models.ues import UEParcours -from app.models.but_validations import ApcValidationRCUE +from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE from app.models.etudiants import Identite from app.models.formations import Formation from app.models.formsemestre import FormSemestre @@ -42,7 +42,7 @@ from app.scodoc import sco_cursus_dut class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): - """Pour compat ScoDoc 7: à revoir pour le BUT""" + """Pour compat ScoDoc 7""" def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT): super().__init__(etud, formsemestre_id, res) @@ -54,8 +54,16 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): return False def parcours_validated(self): - "True si le parcours est validé" - return False # XXX TODO + "True si le parcours (ici diplôme BUT) est validé" + # Si année 3 validée, ok + return any( + sco_codes.code_annee_validant(v.code) + for v in ApcValidationAnnee.query.filter_by( + etudid=self.etud.id, + ordre=3, + referentiel_competence_id=self.cur_sem.formation.referentiel_competence_id, + ) + ) class EtudCursusBUT: @@ -287,81 +295,81 @@ class FormSemestreCursusBUT: ) return niveaux_by_annee - def get_etud_validation_par_competence_et_annee(self, etud: Identite): - """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }""" - validation_par_competence_et_annee = {} - for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): - # On s'assurer qu'elle concerne notre cursus ! - ue = validation_rcue.ue2 - if ue.id not in self.ue_ids: - if ( - ue.formation.referentiel_competences_id - == self.referentiel_competences_id - ): - self.ue_ids = ue.id - else: - continue # skip this validation - niveau = validation_rcue.niveau() - if not niveau.competence.id in validation_par_competence_et_annee: - validation_par_competence_et_annee[niveau.competence.id] = {} - previous_validation = validation_par_competence_et_annee.get( - niveau.competence.id - ).get(validation_rcue.annee()) - # prend la "meilleure" validation - if (not previous_validation) or ( - sco_codes.BUT_CODES_ORDER[validation_rcue.code] - > sco_codes.BUT_CODES_ORDER[previous_validation["code"]] - ): - self.validation_par_competence_et_annee[niveau.competence.id][ - niveau.annee - ] = validation_rcue - return validation_par_competence_et_annee + # def get_etud_validation_par_competence_et_annee(self, etud: Identite): + # """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }""" + # validation_par_competence_et_annee = {} + # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): + # # On s'assurer qu'elle concerne notre cursus ! + # ue = validation_rcue.ue2 + # if ue.id not in self.ue_ids: + # if ( + # ue.formation.referentiel_competences_id + # == self.referentiel_competences_id + # ): + # self.ue_ids = ue.id + # else: + # continue # skip this validation + # niveau = validation_rcue.niveau() + # if not niveau.competence.id in validation_par_competence_et_annee: + # validation_par_competence_et_annee[niveau.competence.id] = {} + # previous_validation = validation_par_competence_et_annee.get( + # niveau.competence.id + # ).get(validation_rcue.annee()) + # # prend la "meilleure" validation + # if (not previous_validation) or ( + # sco_codes.BUT_CODES_ORDER[validation_rcue.code] + # > sco_codes.BUT_CODES_ORDER[previous_validation["code"]] + # ): + # self.validation_par_competence_et_annee[niveau.competence.id][ + # niveau.annee + # ] = validation_rcue + # return validation_par_competence_et_annee - def list_etud_inscriptions(self, etud: Identite): - "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)" - self.niveaux_by_annee = {} - "{ annee : liste des niveaux à valider }" - self.niveaux: dict[int, ApcNiveau] = {} - "cache les niveaux" - for annee in (1, 2, 3): - niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours( - annee, [self.parcour] if self.parcour else None # XXX WIP - )[1] - # groupe les niveaux de tronc commun et ceux spécifiques au parcour - self.niveaux_by_annee[annee] = niveaux_d["TC"] + ( - niveaux_d[self.parcour.id] if self.parcour else [] - ) - self.niveaux.update( - {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]} - ) + # def list_etud_inscriptions(self, etud: Identite): + # "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)" + # self.niveaux_by_annee = {} + # "{ annee : liste des niveaux à valider }" + # self.niveaux: dict[int, ApcNiveau] = {} + # "cache les niveaux" + # for annee in (1, 2, 3): + # niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours( + # annee, [self.parcour] if self.parcour else None # XXX WIP + # )[1] + # # groupe les niveaux de tronc commun et ceux spécifiques au parcour + # self.niveaux_by_annee[annee] = niveaux_d["TC"] + ( + # niveaux_d[self.parcour.id] if self.parcour else [] + # ) + # self.niveaux.update( + # {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]} + # ) - self.validation_par_competence_et_annee = {} - """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }""" - for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): - niveau = validation_rcue.niveau() - if not niveau.competence.id in self.validation_par_competence_et_annee: - self.validation_par_competence_et_annee[niveau.competence.id] = {} - previous_validation = self.validation_par_competence_et_annee.get( - niveau.competence.id - ).get(validation_rcue.annee()) - # prend la "meilleure" validation - if (not previous_validation) or ( - sco_codes.BUT_CODES_ORDER[validation_rcue.code] - > sco_codes.BUT_CODES_ORDER[previous_validation["code"]] - ): - self.validation_par_competence_et_annee[niveau.competence.id][ - niveau.annee - ] = validation_rcue + # self.validation_par_competence_et_annee = {} + # """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }""" + # for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud): + # niveau = validation_rcue.niveau() + # if not niveau.competence.id in self.validation_par_competence_et_annee: + # self.validation_par_competence_et_annee[niveau.competence.id] = {} + # previous_validation = self.validation_par_competence_et_annee.get( + # niveau.competence.id + # ).get(validation_rcue.annee()) + # # prend la "meilleure" validation + # if (not previous_validation) or ( + # sco_codes.BUT_CODES_ORDER[validation_rcue.code] + # > sco_codes.BUT_CODES_ORDER[previous_validation["code"]] + # ): + # self.validation_par_competence_et_annee[niveau.competence.id][ + # niveau.annee + # ] = validation_rcue - self.competences = { - competence.id: competence - for competence in ( - self.parcour.query_competences() - if self.parcour - else self.formation.referentiel_competence.get_competences_tronc_commun() - ) - } - "cache { competence_id : competence }" + # self.competences = { + # competence.id: competence + # for competence in ( + # self.parcour.query_competences() + # if self.parcour + # else self.formation.referentiel_competence.get_competences_tronc_commun() + # ) + # } + # "cache { competence_id : competence }" def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float: diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 5bd71c7e..8c19239f 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -1034,8 +1034,8 @@ class DecisionsProposeesAnnee(DecisionsProposees): return messages def valide_diplome(self) -> bool: - "Vrai si l'étudiant à validé son diplôme" - return False # TODO XXX + "Vrai si l'étudiant a validé son diplôme (décision enregistrée)" + return self.annee_but == 3 and sco_codes.code_annee_validant(self.code_valide) def list_ue_parcour_etud( diff --git a/app/but/jury_but_pv.py b/app/but/jury_but_pv.py index 56e45d41..c20000fb 100644 --- a/app/but/jury_but_pv.py +++ b/app/but/jury_but_pv.py @@ -155,6 +155,7 @@ def pvjury_table_but( deca = None ects_but_valides = but_ects_valides(etud, referentiel_competence_id) + has_diplome = deca.valide_diplome() row = { "nom_pv": ( etud.code_ine or etud.code_nip or etud.id @@ -181,10 +182,15 @@ def pvjury_table_but( ), "decision_but": deca.code_valide if deca else "", "devenir": ( - ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]) - if deca - else "" + "Diplôme obtenu" + if has_diplome + else ( + ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]) + if deca + else "" + ) ), + "diplome": "ADM" if has_diplome else "", # pour exports excel seulement: "civilite": etud.civilite_etat_civil_str, "nom": etud.nom, diff --git a/app/models/but_validations.py b/app/models/but_validations.py index adf0d203..8ec9a110 100644 --- a/app/models/but_validations.py +++ b/app/models/but_validations.py @@ -219,6 +219,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict: dec_rcue["code"]}""" ) decisions["descr_decisions_rcue"] = ", ".join(titres_rcues) + decisions["descr_decisions_rcue_list"] = titres_rcues decisions["descr_decisions_niveaux"] = ( "Niveaux de compétences: " + decisions["descr_decisions_rcue"] ) diff --git a/app/scodoc/sco_pv_lettres_inviduelles.py b/app/scodoc/sco_pv_lettres_inviduelles.py index c3d069e7..3517c9a6 100644 --- a/app/scodoc/sco_pv_lettres_inviduelles.py +++ b/app/scodoc/sco_pv_lettres_inviduelles.py @@ -257,7 +257,9 @@ def pdf_lettre_individuelle(sem, decision, etud: Identite, params, signature=Non else: params["autorisations_txt"] = "" - if decision["decision_sem"] and situation_etud.parcours_validated(): + if ( + formsemestre.formation.is_apc() or decision["decision_sem"] + ) and situation_etud.parcours_validated(): params["diplome_txt"] = ( """Vous avez donc obtenu le diplôme : %(titre_formation)s""" % params ) @@ -357,5 +359,8 @@ def add_apc_infos(formsemestre: FormSemestre, params: dict, decision: dict): params[ "decision_ue_txt" ] = f"""{params["decision_ue_txt"]}
    - Niveaux de compétences:
    {decision.get("descr_decisions_rcue") or ""} + Niveaux de compétences: +
        - { + '
        - '.join( decision.get("descr_decisions_rcue_list", []) ) + } """ From 3163e00ff589d7726dbfc17cf7186a734f4c96c9 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jun 2024 18:51:02 +0200 Subject: [PATCH 7/8] =?UTF-8?q?Dipl=C3=B4me=20BUT=20sur=20tableau=20recap.?= =?UTF-8?q?=20jury.=20Closes=20#929.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/but/cursus_but.py | 22 ++++++++++++++-------- app/tables/jury_recap.py | 38 ++++++++++++++++++++++++++------------ 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/app/but/cursus_but.py b/app/but/cursus_but.py index b9a09f0b..45535545 100644 --- a/app/but/cursus_but.py +++ b/app/but/cursus_but.py @@ -55,17 +55,23 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): def parcours_validated(self): "True si le parcours (ici diplôme BUT) est validé" - # Si année 3 validée, ok - return any( - sco_codes.code_annee_validant(v.code) - for v in ApcValidationAnnee.query.filter_by( - etudid=self.etud.id, - ordre=3, - referentiel_competence_id=self.cur_sem.formation.referentiel_competence_id, - ) + return but_parcours_validated( + self.etud.id, self.cur_sem.formation.referentiel_competence_id ) +def but_parcours_validated(etudid: int, referentiel_competence_id: int) -> bool: + """Détermine si le parcours BUT est validé: + ne regarde que si une validation BUT3 est enregistrée + """ + return any( + sco_codes.code_annee_validant(v.code) + for v in ApcValidationAnnee.query.filter_by( + etudid=etudid, ordre=3, referentiel_competence_id=referentiel_competence_id + ) + ) + + class EtudCursusBUT: """L'état de l'étudiant dans son cursus BUT Liste des niveaux validés/à valider diff --git a/app/tables/jury_recap.py b/app/tables/jury_recap.py index 3b890db9..96933a21 100644 --- a/app/tables/jury_recap.py +++ b/app/tables/jury_recap.py @@ -84,6 +84,7 @@ class TableJury(TableRecap): autorisations = res.get_autorisations_inscription() if res.is_apc: validations_annee = res.get_validations_annee() + for row in self.rows: etud = row.etud if not res.is_apc: @@ -103,18 +104,31 @@ class TableJury(TableRecap): self.foot_title_row.cells["jury_code_sem"].target_attrs[ "title" ] = """Code jury sur le semestre""" - # Autorisations inscription - row.add_cell( - "autorisations_inscription", - "Passage", - ( - ", ".join("S" + str(i) for i in sorted(autorisations[etud.id])) - if etud.id in autorisations - else "" - ), - group="jury_code_sem", - classes=["recorded_code"], - ) + # Autorisations inscription ou diplôme BUT S6 + if res.is_apc and res.formsemestre.semestre_id == 6: + # on ne vérifie le diplôme que dans ce cas pour ne pas ralentir + if cursus_but.but_parcours_validated( + etud.id, res.formsemestre.formation.referentiel_competence_id + ): + row.add_cell( + "autorisations_inscription", + "Passage", + "Diplôme obtenu", + group="jury_code_sem", + classes=["recorded_code"], + ) + else: + row.add_cell( + "autorisations_inscription", + "Passage", + ( + ", ".join("S" + str(i) for i in sorted(autorisations[etud.id])) + if etud.id in autorisations + else "" + ), + group="jury_code_sem", + classes=["recorded_code"], + ) if res.is_apc: # BUT validation_annee = validations_annee.get(etud.id, None) row.add_cell( From c41307c63795ad9d1257c92e2ad0b31038e903c2 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 19 Jun 2024 19:14:12 +0200 Subject: [PATCH 8/8] Edition en ligne des codes Apogee des UEs de BUT --- app/scodoc/sco_edit_ue.py | 2 +- app/templates/pn/form_ues.j2 | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 9d8df2d4..0b1ea30b 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -1514,7 +1514,7 @@ def edit_ue_set_code_apogee(id=None, value=None): ue_id = id value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque - log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value)) + log(f"edit_ue_set_code_apogee: ue_id={ue_id} code_apogee={value}") ues = ue_list(args={"ue_id": ue_id}) if not ues: diff --git a/app/templates/pn/form_ues.j2 b/app/templates/pn/form_ues.j2 index 437d4da2..3581cd7e 100644 --- a/app/templates/pn/form_ues.j2 +++ b/app/templates/pn/form_ues.j2 @@ -52,10 +52,12 @@ else 'aucun'|safe }} ECTS {%- endif -%} - {%- if ue.code_apogee -%} - {{ virg() }} Apo {{ue.code_apogee}} - {%- endif -%} - ) + {{ virg() }} Apo: + + {{ue.code_apogee or '' + }})