From 29defb6f00a293fb61e3e4425633f4dfdd8f26b6 Mon Sep 17 00:00:00 2001 From: ilona Date: Mon, 14 Oct 2024 16:40:05 +0200 Subject: [PATCH] =?UTF-8?q?Backend=20'Modules':=20utilise=20uniquement=20m?= =?UTF-8?q?od=C3=A8les.=20+=20code=20modernization.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/formations/edit_module.py | 284 +++---------- app/formations/edit_ue.py | 26 +- app/formations/formation_io.py | 31 +- app/models/formations.py | 7 +- app/models/moduleimpls.py | 29 +- app/models/modules.py | 128 +++++- app/pe/moys/pe_ressemtag.py | 11 +- app/scodoc/sco_bulletins_json.py | 6 +- app/scodoc/sco_formsemestre_edit.py | 26 +- app/scodoc/sco_formsemestre_status.py | 1 - app/scodoc/sco_moduleimpl.py | 12 - app/scodoc/sco_placement.py | 9 +- app/scodoc/sco_ue_external.py | 13 +- app/views/notes.py | 16 +- app/views/scolar.py | 1 - sco_version.py | 2 + tests/api/setup_test_api.py | 5 +- tests/api/test_api_formations.py | 2 +- tests/api/tools_test_api.py | 434 ++++++++++---------- tests/scenarios/test_scenario1_formation.py | 11 +- tests/unit/sco_fake_gen.py | 18 +- tests/unit/test_formations.py | 26 +- 22 files changed, 538 insertions(+), 560 deletions(-) diff --git a/app/formations/edit_module.py b/app/formations/edit_module.py index 71fd3ade..b8b389d9 100644 --- a/app/formations/edit_module.py +++ b/app/formations/edit_module.py @@ -33,168 +33,28 @@ from flask import flash, url_for, render_template from flask import g, request from flask_login import current_user -from app import db, log +from app import db from app import models -from app.formations import edit_matiere from app.models import APO_CODE_STR_LEN from app.models import Formation, Matiere, Module, UniteEns from app.models import FormSemestre, ModuleImpl -from app.models import ScolarNews from app.models.but_refcomp import ApcAppCritique, ApcParcours -import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.sco_permissions import Permission from app.scodoc.sco_exceptions import ( ScoValueError, - ScoLockedFormError, - ScoGenError, ScoNonEmptyFormationObject, ) from app.scodoc import codes_cursus -from app.scodoc import sco_moduleimpl - -_moduleEditor = ndb.EditableTable( - "notes_modules", - "module_id", - ( - "module_id", - "titre", - "code", - "abbrev", - "heures_cours", - "heures_td", - "heures_tp", - "coefficient", - "ue_id", - "matiere_id", - "formation_id", - "semestre_id", - "numero", - "code_apogee", - "module_type", - "edt_id", - #'ects' - ), - sortkey="numero, code, titre", - output_formators={ - "heures_cours": ndb.float_null_is_zero, - "heures_td": ndb.float_null_is_zero, - "heures_tp": ndb.float_null_is_zero, - "numero": ndb.int_null_is_zero, - "coefficient": ndb.float_null_is_zero, - "module_type": ndb.int_null_is_zero, - #'ects' : ndb.float_null_is_null - }, -) - - -def module_list(*args, **kw): - "list modules" - cnx = ndb.GetDBConnexion() - return _moduleEditor.list(cnx, *args, **kw) - - -def do_module_create(args) -> int: - "Create a module. Returns id of new object." - formation = db.session.get(Formation, args["formation_id"]) - # refuse de créer un module APC avec semestres incohérents: - if formation.is_apc(): - ue = db.session.get(UniteEns, args["ue_id"]) - if int(args.get("semestre_id", 0)) != ue.semestre_idx: - raise ScoValueError("Formation incompatible: contacter le support ScoDoc") - # create - module = Module.create_from_dict(args) - db.session.commit() - log(f"do_module_create: created {module.id} with {args}") - - # news - ScolarNews.add( - typ=ScolarNews.NEWS_FORM, - obj=formation.id, - text=f"Modification de la formation {formation.acronyme}", - ) - formation.invalidate_cached_sems() - return module.id - - -def module_create( - matiere_id=None, module_type=None, semestre_id=None, formation_id=None -): - """Formulaire de création d'un module - Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal). - Sinon, donne le choix de l'UE de rattachement et utilise la première - matière de cette UE (si elle n'existe pas, la crée). - """ - return module_edit( - create=True, - matiere_id=matiere_id, - module_type=module_type, - semestre_id=semestre_id, - formation_id=formation_id, - ) - - -def can_delete_module(module): - "True si le module n'est pas utilisée dans des formsemestre" - return len(module.modimpls.all()) == 0 - - -def do_module_delete(oid): - "delete module" - module = Module.query.get_or_404(oid) - mod = module_list({"module_id": oid})[0] # sco7 - if module_is_locked(module.id): - raise ScoLockedFormError() - if not can_delete_module(module): - raise ScoNonEmptyFormationObject( - "Module", - msg=module.titre, - dest_url=url_for( - "notes.ue_table", - scodoc_dept=g.scodoc_dept, - formation_id=module.formation_id, - semestre_idx=module.ue.semestre_idx, - ), - ) - - # S'il y a des moduleimpls, on ne peut pas detruire le module ! - mods = sco_moduleimpl.moduleimpl_list(module_id=oid) - if mods: - err_page = f""" -

Destruction du module impossible car il est utilisé dans des - semestres existants !

-

Il faut d'abord supprimer le semestre (ou en retirer - ce module). - Mais il est peut être préférable de laisser ce programme intact et - d'en créer une nouvelle version pour la modifier sans affecter - les semestres déjà en place. -

- reprendre - """ - raise ScoGenError(err_page) - # delete - cnx = ndb.GetDBConnexion() - _moduleEditor.delete(cnx, oid) - - # news - formation = module.formation - ScolarNews.add( - typ=ScolarNews.NEWS_FORM, - obj=mod["formation_id"], - text=f"Modification de la formation {formation.acronyme}", - ) - formation.invalidate_cached_sems() def module_delete(module_id=None): - """Delete a module""" + """Formulaire suppression d'un module""" module = Module.query.get_or_404(module_id) - mod = module_list(args={"module_id": module_id})[0] # sco7 - if not can_delete_module(module): + if not module.can_be_deleted(): raise ScoNonEmptyFormationObject( "Module", msg=module.titre, @@ -221,7 +81,7 @@ def module_delete(module_id=None): request.base_url, scu.get_request_args(), (("module_id", {"input_type": "hidden"}),), - initvalues=mod, + initvalues=module.to_dict(), submitlabel="Confirmer la suppression", cancelbutton="Annuler", ) @@ -231,37 +91,38 @@ def module_delete(module_id=None): title="Suppression d'un module", content="\n".join(H) + tf[1], ) - elif tf[0] == -1: - return flask.redirect(dest_url) - else: - do_module_delete(module_id) + if tf[0] == -1: # cancel return flask.redirect(dest_url) + module.delete() + return flask.redirect(dest_url) + def do_module_edit(vals: dict) -> None: "edit a module" # check - mod = module_list({"module_id": vals["module_id"]})[0] - if module_is_locked(mod["module_id"]): - # formation verrouillée: empeche de modifier certains champs: - vals = vals.copy() - protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id") - for f in protected_fields: - if f in vals: - del vals[f] + module = Module.get_instance(vals["module_id"]) # edit - cnx = ndb.GetDBConnexion() - _moduleEditor.edit(cnx, vals) - db.session.get(Formation, mod["formation_id"]).invalidate_cached_sems() + modif = module.from_dict(vals) + if modif: + module.formation.invalidate_cached_sems() -def check_module_code_unicity(code, field, formation_id, module_id=None): - "true si code module unique dans la formation" - modules = module_list(args={"code": code, "formation_id": formation_id}) - if module_id: # edition: supprime le module en cours - modules = [m for m in modules if m["module_id"] != module_id] - - return len(modules) == 0 +def module_create( + matiere_id=None, module_type=None, semestre_id=None, formation_id=None +): + """Formulaire de création d'un module + Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal). + Sinon, donne le choix de l'UE de rattachement et utilise la première + matière de cette UE (si elle n'existe pas, la crée). + """ + return module_edit( + create=True, + matiere_id=matiere_id, + module_type=module_type, + semestre_id=semestre_id, + formation_id=formation_id, + ) def module_edit( @@ -278,14 +139,12 @@ def module_edit( Sinon, donne le choix de l'UE de rattachement et utilise la première matière de cette UE (si elle n'existe pas, la crée). """ - from app.scodoc import sco_tag_module - # --- Détermination de la formation orig_semestre_idx = semestre_id ue = None if create: if matiere_id: - matiere = Matiere.query.get_or_404(matiere_id) + matiere = Matiere.get_instance(matiere_id) ue = matiere.ue formation = ue.formation orig_semestre_idx = ue.semestre_idx if semestre_id is None else semestre_id @@ -300,7 +159,7 @@ def module_edit( ue = module.ue module_dict = module.to_dict() formation = module.formation - unlocked = not module_is_locked(module_id) + unlocked = not module.is_locked() parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) is_apc = parcours.APC_SAE # BUT @@ -326,17 +185,14 @@ def module_edit( if (module and module.matiere and (module.matiere.id == mat.id)) or (mat.id == mat.ue.matieres.first().id) ] - mat_names = [ - "S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres - ] + mat_names = [f"S{mat.ue.semestre_idx} / {mat.ue.acronyme}" for mat in matieres] else: - mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] + mat_names = ["{mat.ue.acronyme} / {mat.titre or ''}" for mat in matieres] if module: # edition - ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres] - module_dict["ue_matiere_id"] = "%s!%s" % ( - module_dict["ue_id"], - module_dict["matiere_id"], + ue_mat_ids = [f"{mat.ue.id}!{mat.id}" for mat in matieres] + module_dict["ue_matiere_id"] = ( + f"{module_dict['ue_id']}!{module_dict['matiere_id']}" ) semestres_indices = list(range(1, parcours.NB_SEM + 1)) @@ -433,8 +289,8 @@ def module_edit( "explanation": """code du module (issu du programme, exemple M1203, R2.01, ou SAÉ 3.4. Doit être unique dans la formation)""", "allow_null": False, - "validator": lambda val, field, formation_id=formation.id: check_module_code_unicity( - val, field, formation_id, module_id=module.id if module else None + "validator": lambda val, _, formation_id=formation.id: Module.check_module_code_unicity( + val, formation_id, module_id=module.id if module else None ), }, ), @@ -602,7 +458,8 @@ def module_edit( "title": "UE de rattachement", "explanation": "utilisée notamment pour les malus", "labels": [ - f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}" + f"""S{u.semestre_idx if u.semestre_idx is not None else '.' + } / {u.acronyme} {u.titre}""" for u in ues ], "allowed_values": [u.id for u in ues], @@ -631,7 +488,8 @@ def module_edit( "input_type": "menu", "type": "int", "title": parcours.SESSION_NAME.capitalize(), - "explanation": f"{parcours.SESSION_NAME} de début du module dans la formation standard", + "explanation": f"""{parcours.SESSION_NAME + } de début du module dans la formation standard""", "labels": [str(x) for x in semestres_indices], "allowed_values": semestres_indices, "enabled": unlocked, @@ -846,8 +704,7 @@ def module_edit( tf[2]["matiere_id"] = matiere.id tf[2]["semestre_id"] = ue.semestre_idx - module_id = do_module_create(tf[2]) - module = db.session.get(Module, module_id) + module = Module.create_from_dict(tf[2], news=True) else: # EDITION MODULE # l'UE de rattachement peut changer tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") @@ -892,6 +749,7 @@ def module_edit( ] db.session.add(module) db.session.commit() + module.formation.invalidate_cached_sems() return flask.redirect( url_for( "notes.ue_table", @@ -904,28 +762,36 @@ def module_edit( def module_table(formation_id): """Liste des modules de la formation - (XXX inutile ou a revoir) + (affichage debug) """ - if not formation_id: - raise ScoValueError("invalid formation !") - formation: Formation = Formation.query.get_or_404(formation_id) + formation = Formation.get_formation(formation_id) + editable = current_user.has_permission(Permission.EditFormation) + H = [ - f"""

Listes des modules dans la formation {formation.titre} ({formation.acronyme}

+ f"""

Listes des modules dans la formation + {formation.titre} ({formation.acronyme} (debug) +

") @@ -936,23 +802,6 @@ def module_table(formation_id): ) -def module_is_locked(module_id): - """True if module should not be modified - (used in a locked formsemestre) - """ - r = ndb.SimpleDictFetch( - """SELECT mi.id - FROM notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi - WHERE mi.module_id = mod.id - AND mi.formsemestre_id = sem.id - AND mi.module_id = %(module_id)s - AND sem.etat = false - """, - {"module_id": module_id}, - ) - return len(r) > 0 - - def formation_add_malus_modules( formation_id: int, semestre_id: int = None, titre=None, redirect=True ): @@ -966,7 +815,7 @@ def formation_add_malus_modules( ues = ues.filter_by(semestre_idx=semestre_id) for ue in ues: if ue.type == codes_cursus.UE_STANDARD: - if ue_add_malus_module(ue, titre=titre) != None: + if ue_add_malus_module(ue, titre=titre) is not None: nb += 1 flash(f"Modules de malus ajoutés dans {nb} UEs du S{semestre_id}") @@ -1002,7 +851,8 @@ def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int: # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # le semestre ? ou affecter le malus au semestre 1 ??? raise ScoValueError( - "Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre et ne comporte pas d'autres modules" + """Impossible d'ajouter un malus si l'UE n'a pas de numéro de semestre + et ne comporte pas d'autres modules""" ) else: semestre_id = ue.semestre_idx diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py index dbf2a1db..3712a854 100644 --- a/app/formations/edit_ue.py +++ b/app/formations/edit_ue.py @@ -37,7 +37,6 @@ from flask_login import current_user from app import db, log from app.but import apc_edit_ue -from app.formations import edit_matiere, edit_module from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import ( Formation, @@ -66,7 +65,6 @@ from app.scodoc import codes_cursus from app.scodoc import sco_edit_apc from app.scodoc import sco_groups from app.scodoc import sco_moduleimpl -from app.scodoc import sco_tag_module _ueEditor = ndb.EditableTable( "notes_ue", @@ -375,7 +373,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "type": "float", "min_value": 0, "title": "Coef. RCUE", - "explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations. + "explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. + Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations. """, "defaut": 1.0, "allow_null": False, @@ -421,7 +420,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No { "title": "Code Apogée", "size": 25, - "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "explanation": """(optionnel) code élément pédagogique Apogée + ou liste de codes ELP séparés par des virgules""", "max_length": APO_CODE_STR_LEN, }, ), @@ -445,7 +445,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "input_type": "boolcheckbox", "title": "UE externe", "readonly": not create, # ne permet pas de transformer une UE existante en externe - "explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement", + "explanation": """réservé pour les capitalisations d'UEs + effectuées à l'extérieur de l'établissement""", }, ), ( @@ -465,7 +466,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "input_type": "boolcheckbox", "default": True, "title": "Créer matière identique", - "explanation": "créer immédiatement une matière dans cette UE (utile si on n'utilise pas de matières)", + "explanation": """créer immédiatement une matière dans cette UE + (utile si on n'utilise pas de matières)""", }, ) ) @@ -553,7 +555,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No matiere_id = matiere.id if cursus.UE_IS_MODULE: # dans ce mode, crée un (unique) module dans l'UE: - _ = edit_module.do_module_create( + _ = Module.create_from_dict( { "titre": tf[2]["titre"], "code": tf[2]["acronyme"], @@ -565,6 +567,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No "semestre_id": tf[2]["semestre_idx"], }, ) + db.session.commit() ue = db.session.get(UniteEns, ue_id) flash(f"UE créée (code {ue.ue_code})") else: @@ -608,9 +611,10 @@ def _add_ue_semestre_id(ues: list[dict], is_apc): ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT else: # était le comportement ScoDoc7 - modules = edit_module.module_list(args={"ue_id": ue["ue_id"]}) - if modules: - ue["semestre_id"] = modules[0]["semestre_id"] + ue = UniteEns.get_ue(ue["ue_id"]) + module = ue.modules.first() + if module: + ue["semestre_id"] = module.semestre_id else: ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT @@ -1323,7 +1327,6 @@ def _ue_table_modules( arrow_none, delete_icon, delete_disabled_icon, - unit_name="matière", add_suppress_link=True, # lien "supprimer cette matière" empty_list_msg="Aucun élément dans cette matière", create_element_msg="créer un module", @@ -1361,7 +1364,6 @@ def _ue_table_modules( H.append("") mod_editable = editable - # and not edit_module.module_is_locked(Mod['module_id']) if mod_editable: H.append( f""" 2: - module: Module = db.session.get(Module, mod_id) tag_names = [] ue_coef_dict = {} for child in mod_info[2]: diff --git a/app/models/formations.py b/app/models/formations.py index 5ae12d6f..559b6fe7 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -66,7 +66,8 @@ class Formation(ScoDocModel): def html(self) -> str: "titre complet pour affichage" - return f"""Formation {self.titre} ({self.acronyme}) version {self.version} code {self.formation_code}""" + return f"""Formation {self.titre} ({self.acronyme}) version {self.version + } code {self.formation_code}""" @classmethod def get_formation(cls, formation_id: int | str, dept_id: int = None) -> "Formation": @@ -334,7 +335,7 @@ class Matiere(ScoDocModel): return e def is_locked(self) -> bool: - """True if matiere cannot be be modified + """True if matiere can't modified because it contains modules used in a locked formsemestre. """ from app.models.formsemestre import FormSemestre @@ -361,8 +362,6 @@ class Matiere(ScoDocModel): def delete(self): "Delete matière. News, inval cache." - from app.models import ScolarNews - formation = self.ue.formation log(f"matiere.delete: matiere_id={self.id}") if not self.can_be_deleted(): diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 281d28c3..c3a3c269 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -7,14 +7,15 @@ from flask_login import current_user from flask_sqlalchemy.query import Query import app -from app import db +from app import db, log from app.auth.models import User from app.comp import df_cache from app.models import APO_CODE_STR_LEN, ScoDocModel from app.models.etudiants import Identite from app.models.evaluations import Evaluation from app.models.modules import Module -from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError +from app.scodoc import sco_cache +from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError, ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu @@ -65,6 +66,30 @@ class ModuleImpl(ScoDocModel): def __repr__(self): return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>" + @classmethod + def create_from_dict(cls, data: dict) -> "ModuleImpl": + """Create modimpl from dict. Log, inval. cache. + data must include valid formsemestre_id, module_id and responsable_id + Commit session. + """ + from app.models import FormSemestre + + # check required args + for required_arg in ("formsemestre_id", "module_id", "responsable_id"): + if required_arg not in data: + raise ScoValueError(f"missing argument: {required_arg}") + _ = FormSemestre.get_formsemestre(data["formsemestre_id"]) + _ = Module.get_instance(data["module_id"]) + if not db.session.get(User, data["responsable_id"]): + abort(404, "responsable_id invalide") + + modimpl = super().create_from_dict(data) + db.session.commit() + db.session.refresh(modimpl) + log(f"ModuleImpl.create: created {modimpl.id} with {data}") + sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) + return modimpl + def get_codes_apogee(self) -> set[str]: """Les codes Apogée (codés en base comme "VRT1,VRT2"). (si non renseigné, ceux du module) diff --git a/app/models/modules.py b/app/models/modules.py index 50ff5633..37cded35 100644 --- a/app/models/modules.py +++ b/app/models/modules.py @@ -2,7 +2,7 @@ """ import http -from flask import current_app, g +from flask import current_app, g, url_for from app import db, log from app import models @@ -13,9 +13,14 @@ from app.models.but_refcomp import ( app_critiques_modules, parcours_modules, ) +from app.models.events import ScolarNews from app.scodoc import sco_utils as scu from app.scodoc.codes_cursus import UE_SPORT -from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_exceptions import ( + ScoValueError, + ScoLockedFormError, + ScoNonEmptyFormationObject, +) from app.scodoc.sco_utils import ModuleType @@ -115,13 +120,39 @@ class Module(models.ScoDocModel): # on ne peut pas affecter directement parcours return super().filter_model_attributes(args, (excluded or set()) | {"parcours"}) + @classmethod + def check_module_code_unicity(cls, code, formation_id, module_id=None) -> bool: + "true si code module unique dans la formation" + from app.models import Formation + + formation = Formation.get_formation(formation_id) + query = formation.modules.filter_by(code=code) + if module_id is not None: # edition: supprime le module en cours + query = query.filter(Module.id != module_id) + return query.count() == 0 + def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool: """Update object's fields given in dict. Add to session but don't commit. True if modification. - can't change ue nor formation - can change matiere_id, iff new matiere in same ue - can change parcours: parcours list of ApcParcour id or instances. + Ne modifie pas les coefficients APC ue_coefs """ + args = args.copy() + if "ue_coefs" in args: + del args["ue_coefs"] + if self.is_locked(): + # formation verrouillée: empeche de modifier coefficient, matiere, and semestre_id + protected_fields = ("coefficient", "matiere_id", "semestre_id") + for f in protected_fields: + if f in args: + del args[f] + # Unicité du code + if "code" in args and not Module.check_module_code_unicity( + args["code"], self.formation_id, self.id + ): + raise ScoValueError("code module déjà utilisé") # Vérifie les changements de matiere new_matiere_id = args.get("matiere_id", self.matiere_id) if new_matiere_id != self.matiere_id: @@ -139,20 +170,103 @@ class Module(models.ScoDocModel): existing_parcours = {p.id for p in self.parcours} new_parcours = args.get("parcours", []) or [] if existing_parcours != set(new_parcours): - self._set_parcours_from_list(new_parcours) + self.set_parcours_from_list(new_parcours) return True return modified @classmethod - def create_from_dict(cls, data: dict) -> "Module": + def create_from_dict( + cls, + data: dict, + inval_cache=False, + news=False, + ) -> "Module": """Create from given dict, add parcours. - Flush session.""" + Flush session. + Si news, commit and log news. + """ + from app.models.formations import Formation + + # check required arguments + for required_arg in ("code", "formation_id", "ue_id"): + if required_arg not in data: + raise ScoValueError(f"missing argument: {required_arg}") + if not data["code"]: + raise ScoValueError("module code must be non empty") + # Check formation + formation = Formation.get_formation(data["formation_id"]) + ue = UniteEns.get_ue(data["ue_id"]) + # refuse de créer un module APC avec semestres semestre du module != semestre de l'UE + if formation.is_apc(): + if int(data.get("semestre_id", 1)) != ue.semestre_idx: + raise ScoValueError( + "Formation incompatible: indices UE et module différents" + ) module = super().create_from_dict(data) db.session.flush() - module._set_parcours_from_list(data.get("parcours", []) or []) + module.set_parcours_from_list(data.get("parcours", []) or []) + log(f"module_create: created {module.id} with {data}") + if news: + db.session.commit() + db.session.refresh(module) + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=formation.id, + text=f"Modification de la formation {formation.acronyme}", + ) + if inval_cache: + formation.invalidate_cached_sems() + return module - def _set_parcours_from_list(self, parcours: list[ApcParcours | int]): + def is_locked(self) -> bool: + """True if module cannot be modified + because it is used in a locked formsemestre. + """ + from app.models import FormSemestre, ModuleImpl + + mods = ( + db.session.query(Module) + .filter_by(id=self.id) + .join(ModuleImpl) + .join(FormSemestre) + .filter_by(etat=False) + .all() + ) + return bool(mods) + + def can_be_deleted(self) -> bool: + """True if module can be deleted""" + return self.modimpls.count() == 0 + + def delete(self): + "Delete module. News, inval cache." + if self.is_locked(): + raise ScoLockedFormError() + if not self.can_be_deleted(): + raise ScoNonEmptyFormationObject( + "Module", + msg=self.titre or self.code, + dest_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=self.formation_id, + semestre_idx=self.ue.semestre_idx, + ), + ) + formation = self.formation + db.session.delete(self) + log(f"Module.delete({self.id})") + db.session.commit() + # news + ScolarNews.add( + typ=ScolarNews.NEWS_FORM, + obj=formation.id, + text=f"Modification de la formation {formation.acronyme}", + ) + formation.invalidate_cached_sems() + + def set_parcours_from_list(self, parcours: list[ApcParcours | int]): """Ajoute ces parcours à la liste des parcours du module. Chaque élément est soit un objet parcours soit un id. S'assure que chaque parcours est dans le référentiel de compétence diff --git a/app/pe/moys/pe_ressemtag.py b/app/pe/moys/pe_ressemtag.py index e424e8f3..acd59ad2 100644 --- a/app/pe/moys/pe_ressemtag.py +++ b/app/pe/moys/pe_ressemtag.py @@ -41,8 +41,8 @@ from app import ScoValueError from app import comp from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre, UniteEns -import app.pe.pe_affichage as pe_affichage -import app.pe.pe_etudiant as pe_etudiant +from app.pe import pe_affichage +from app.pe import pe_etudiant from app.pe.moys import pe_tabletags, pe_moytag from app.scodoc import sco_tag_module from app.scodoc import codes_cursus as sco_codes @@ -59,13 +59,18 @@ class ResSemBUTTag(ResultatsSemestreBUT, pe_tabletags.TableTag): def __init__( self, formsemestre: FormSemestre, - options={"moyennes_tags": True, "moyennes_ue_res_sae": False}, + options: dict | None = None, ): """ Args: formsemestre: le ``FormSemestre`` sur lequel il se base options: Un dictionnaire d'options """ + options = ( + {"moyennes_tags": True, "moyennes_ue_res_sae": False} + if options is None + else options + ) ResultatsSemestreBUT.__init__(self, formsemestre) pe_tabletags.TableTag.__init__(self) diff --git a/app/scodoc/sco_bulletins_json.py b/app/scodoc/sco_bulletins_json.py index 17461457..1352ea65 100644 --- a/app/scodoc/sco_bulletins_json.py +++ b/app/scodoc/sco_bulletins_json.py @@ -491,9 +491,9 @@ def dict_decision_jury( ] d["decision_ue"] = [] - if decision[ - "decisions_ue" - ]: # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): always publish (car utile pour export Apogee) + if decision["decisions_ue"]: + # and sco_preferences.get_preference( 'bul_show_uevalid', formsemestre_id): + # always publish (car utile pour export Apogee) for ue_id in decision["decisions_ue"].keys(): ue = edit_ue.ue_list({"ue_id": ue_id})[0] d["decision_ue"].append( diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 5c5b541a..17f013ef 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -34,7 +34,6 @@ import sqlalchemy as sa from app import db from app.auth.models import User -from app.formations import edit_module from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import ( ApcValidationAnnee, @@ -939,7 +938,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N "formsemestre_id": formsemestre_id, "responsable_id": tf[2][f"MI{module_id}"], } - _ = sco_moduleimpl.do_moduleimpl_create(modargs) + _ = ModuleImpl.create_from_dict(modargs) else: # Modification du semestre: # on doit creer les modules nouvellement selectionnés @@ -971,27 +970,23 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N "formsemestre_id": formsemestre.id, "responsable_id": tf[2]["MI" + str(module_id)], } - moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs) - mod = edit_module.module_list({"module_id": module_id})[0] - msg += [ - "création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?") - ] + modimpl = ModuleImpl.create_from_dict(modargs) + assert modimpl.module_id == module_id + mod = modimpl.module + msg += [f"""création de {mod.code or "?"} ({mod.titre or "?"})"""] # INSCRIPTIONS DES ETUDIANTS - log( - 'inscription module: %s = "%s"' - % ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id]) - ) - group_id = tf[2]["%s!group_id" % module_id] + group_id = tf[2][f"{module_id}!group_id"] + log(f"""inscription module: {module_id}!group_id = '{group_id}'""") if group_id: etudids = [ x["etudid"] for x in sco_groups.get_group_members(group_id) ] log( "inscription module:module_id=%s,moduleimpl_id=%s: %s" - % (module_id, moduleimpl_id, etudids) + % (module_id, modimpl.id, etudids) ) sco_moduleimpl.do_moduleimpl_inscrit_etuds( - moduleimpl_id, + modimpl.id, formsemestre.id, etudids, ) @@ -1002,7 +997,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N else: log( "inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit" - % (module_id, moduleimpl_id) + % (module_id, modimpl.id) ) # ok, diag = formsemestre_delete_moduleimpls( @@ -1022,7 +1017,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N sco_moduleimpl.do_moduleimpl_edit( modargs, formsemestre_id=formsemestre.id ) - mod = edit_module.module_list({"module_id": module_id})[0] # --- Association des parcours if formsemestre is None: formsemestre = FormSemestre.get_formsemestre(formsemestre_id) diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 17907c1a..bb8d3376 100755 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -40,7 +40,6 @@ from app.but.cursus_but import formsemestre_warning_apc_setup from app.comp import res_sem from app.comp.res_common import ResultatsSemestre from app.comp.res_compat import NotesTableCompat -from app.formations import formation_io from app.models import ( Evaluation, Formation, diff --git a/app/scodoc/sco_moduleimpl.py b/app/scodoc/sco_moduleimpl.py index c315477c..6891f8d6 100644 --- a/app/scodoc/sco_moduleimpl.py +++ b/app/scodoc/sco_moduleimpl.py @@ -51,18 +51,6 @@ _moduleimplEditor = ndb.EditableTable( ) -def do_moduleimpl_create(args): - "create a moduleimpl" - # TODO remplacer par une methode de ModuleImpl qui appelle - # super().create_from_dict() puis invalide le formsemestre - cnx = ndb.GetDBConnexion() - r = _moduleimplEditor.create(cnx, args) - sco_cache.invalidate_formsemestre( - formsemestre_id=args["formsemestre_id"] - ) # > creation moduleimpl - return r - - def do_moduleimpl_delete(oid, formsemestre_id=None): "delete moduleimpl (desinscrit tous les etudiants)" cnx = ndb.GetDBConnexion() diff --git a/app/scodoc/sco_placement.py b/app/scodoc/sco_placement.py index 67fe3156..23eca4ae 100644 --- a/app/scodoc/sco_placement.py +++ b/app/scodoc/sco_placement.py @@ -48,8 +48,7 @@ from wtforms import ( HiddenField, SelectMultipleField, ) -from app.formations import edit_module -from app.models import Evaluation, ModuleImpl +from app.models import Evaluation, Module, ModuleImpl import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc import sco_preferences @@ -243,9 +242,9 @@ class PlacementRunner: self.moduleimpl_data = sco_moduleimpl.moduleimpl_list( moduleimpl_id=self.moduleimpl_id )[0] - self.module_data = edit_module.module_list( - args={"module_id": self.moduleimpl_data["module_id"]} - )[0] + self.module_data = Module.get_module( + self.moduleimpl_data["module_id"] + ).to_dict() self.sem = sco_formsemestre.get_formsemestre( self.moduleimpl_data["formsemestre_id"] ) diff --git a/app/scodoc/sco_ue_external.py b/app/scodoc/sco_ue_external.py index 97f97259..fa85beeb 100644 --- a/app/scodoc/sco_ue_external.py +++ b/app/scodoc/sco_ue_external.py @@ -56,12 +56,12 @@ Solution proposée (nov 2014): import flask from flask import flash, g, request, render_template, url_for from flask_login import current_user -from app.formations import edit_module, edit_ue +from app.formations import edit_ue from app.models.formsemestre import FormSemestre from app import db, log -from app.models import Evaluation, Identite, Matiere, ModuleImpl, UniteEns +from app.models import Evaluation, Identite, Matiere, Module, ModuleImpl, UniteEns from app.scodoc import codes_cursus from app.scodoc import sco_moduleimpl from app.scodoc import sco_saisie_notes @@ -114,7 +114,7 @@ def external_ue_create( {"ue_id": ue_id, "titre": titre or acronyme, "numero": 1} ) - module_id = edit_module.do_module_create( + module = Module.create_from_dict( { "titre": "UE extérieure", "code": acronyme, @@ -125,11 +125,13 @@ def external_ue_create( "semestre_id": formsemestre.semestre_id, "module_type": scu.ModuleType.STANDARD, }, + news=True, + inval_cache=True, ) - moduleimpl_id = sco_moduleimpl.do_moduleimpl_create( + modimpl = ModuleImpl.create_from_dict( { - "module_id": module_id, + "module_id": module.id, "formsemestre_id": formsemestre_id, # affecte le 1er responsable du semestre comme resp. du module "responsable_id": ( @@ -139,7 +141,6 @@ def external_ue_create( ), }, ) - modimpl = db.session.get(ModuleImpl, moduleimpl_id) assert modimpl return modimpl diff --git a/app/views/notes.py b/app/views/notes.py index 855eceed..6100e45c 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2575,21 +2575,21 @@ def check_sem_integrity(formsemestre_id, fix=False): bad_sem = [] formations_set = set() # les formations mentionnées dans les UE et modules for modimpl in modimpls: - mod = edit_module.module_list({"module_id": modimpl["module_id"]})[0] - formations_set.add(mod["formation_id"]) - ue = UniteEns.query.get_or_404(mod["ue_id"]) + mod = Module.get_instance(modimpl["module_id"]) + formations_set.add(mod.formation_id) + ue = mod.ue ue_dict = ue.to_dict() formations_set.add(ue_dict["formation_id"]) - if ue_dict["formation_id"] != mod["formation_id"]: - modimpl["mod"] = mod + if ue_dict["formation_id"] != mod.formation_id: + modimpl["mod"] = mod.to_dict() modimpl["ue"] = ue_dict bad_ue.append(modimpl) - if sem["formation_id"] != mod["formation_id"]: + if sem["formation_id"] != mod.formation_id: bad_sem.append(modimpl) - modimpl["mod"] = mod + modimpl["mod"] = mod.to_dict() H = [ - "

formation_id=%s" % sem["formation_id"], + f"""

formation_id={sem["formation_id"]}""", ] if bad_ue: H += [ diff --git a/app/views/scolar.py b/app/views/scolar.py index 59cdba47..5ddaf4c9 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -86,7 +86,6 @@ from app.scodoc import ( sco_archives_etud, sco_bug_report, sco_cache, - sco_debouche, sco_dept, sco_dump_db, sco_etud, diff --git a/sco_version.py b/sco_version.py index e9ebfa7c..92963199 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,6 +1,8 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- +"Infos sur version ScoDoc" + SCOVERSION = "9.7.28" SCONAME = "ScoDoc" diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 097877e0..9b0e38c8 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -117,7 +117,7 @@ def GET( print("reply", reply.text) raise APIError( errmsg or f"""erreur get {url} !""", - reply.json(), + reply if reply.status_code == 404 else reply.json(), status_code=reply.status_code, ) if raw: @@ -220,7 +220,8 @@ def check_failure_get(path: str, headers: dict, err: str = None): # ^ Renvoi un 404 except APIError as api_err: if err is not None: - assert api_err.payload["message"] == err + if "message" in api_err.payload: + assert api_err.payload["message"] == err else: raise APIError("Le GET n'aurait pas du fonctionner") diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index ae7b1e49..8192961b 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -177,7 +177,7 @@ def test_formation_export(api_headers): assert isinstance(module["heures_td"], float) assert isinstance(module["heures_tp"], float) assert isinstance(module["coefficient"], float) - assert isinstance(module["ects"], str) + assert isinstance(module["ects"], str) if "ects" in module else True assert isinstance(module["semestre_id"], int) assert isinstance(module["numero"], int) assert isinstance(module["code_apogee"], str) diff --git a/tests/api/tools_test_api.py b/tests/api/tools_test_api.py index 66c3cfc0..2d0486e5 100644 --- a/tests/api/tools_test_api.py +++ b/tests/api/tools_test_api.py @@ -1,5 +1,6 @@ """Utilitaires pour les tests de l'API """ + import json @@ -37,11 +38,11 @@ def verify_occurences_ids_etuds(json_response) -> bool: DEPARTEMENT_FIELDS = [ - "id", "acronym", - "description", - "visible", "date_creation", + "description", + "id", + "visible", ] # Champs "données personnelles" @@ -67,17 +68,17 @@ ETUD_FIELDS = { FORMATION_FIELDS = { - "dept_id", "acronyme", - "titre_officiel", - "formation_code", "code_specialite", - "id", - "titre", - "version", - "type_parcours", - "referentiel_competence_id", + "dept_id", + "formation_code", "formation_id", + "id", + "referentiel_competence_id", + "titre_officiel", + "titre", + "type_parcours", + "version", } FORMATION_EXPORT_FIELDS = { @@ -95,39 +96,42 @@ FORMATION_EXPORT_FIELDS = { FORMATION_EXPORT_UE_FIELDS = { "acronyme", + "code_apogee", + "coefficient", + "color", + "ects", + "is_external", + "matiere", "numero", + "reference", + "semestre_idx", "titre", "type", "ue_code", - "ects", - "is_external", - "code_apogee", - "coefficient", - "semestre_idx", - "color", - "reference", - "matiere", } FORMATION_EXPORT_UE_MATIERE_FIELDS = { - "titre", - "numero", "module", + "numero", + "titre", } FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS = { - "titre", "abbrev", + "app_critiques", + "code_apogee", "code", + "coefficient", + "coefficients", + "edt_id", "heures_cours", "heures_td", - "coefficient", - "ects", - "semestre_id", - "numero", - "code_apogee", + "heures_tp", "module_type", - "coefficients", + "numero", + "parcours", + "semestre_id", + "titre", } FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = { @@ -136,42 +140,42 @@ FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = { } FORMSEMESTRE_FIELDS = [ - "titre", - "gestion_semestrielle", - "scodoc7_id", - "date_debut", + "block_moyenne_generale", + "block_moyennes", "bul_bgcolor", + "bul_hide_xml", + "date_debut_iso", + "date_debut", + "date_fin_iso", "date_fin", - "resp_can_edit", + "departement", "dept_id", + "elt_annee_apo", + "elt_sem_apo", + "ens_can_edit_eval", + "etape_apo", "etat", - "resp_can_change_ens", + "formation_id", + "formation", + "formsemestre_id", + "gestion_compensation", + "gestion_semestrielle", "id", "modalite", - "ens_can_edit_eval", - "formation_id", - "gestion_compensation", - "elt_sem_apo", - "semestre_id", - "bul_hide_xml", - "elt_annee_apo", - "block_moyenne_generale", - "formsemestre_id", - "titre_num", - "titre_formation", - "date_debut_iso", - "date_fin_iso", - "responsables", "parcours", - "departement", - "formation", - "etape_apo", - "block_moyennes", + "resp_can_change_ens", + "resp_can_edit", + "responsables", + "scodoc7_id", + "semestre_id", + "titre_formation", + "titre_num", + "titre", ] FSEM_FIELDS = { - "block_moyennes", "block_moyenne_generale", + "block_moyennes", "bul_bgcolor", "bul_hide_xml", "date_debut_iso", @@ -198,123 +202,123 @@ FSEM_FIELDS = { } MODIMPL_FIELDS = { - "id", - "formsemestre_id", "computation_expr", - "module_id", - "responsable_id", - "moduleimpl_id", "ens", + "formsemestre_id", + "id", + "module_id", "module", + "moduleimpl_id", + "responsable_id", } MODULE_FIELDS = { - "heures_tp", - "code_apogee", - "titre", - "coefficient", - "module_type", - "id", - "ects", "abbrev", - "ue_id", + "code_apogee", "code", + "coefficient", + "ects", "formation_id", "heures_cours", - "matiere_id", "heures_td", - "semestre_id", - "numero", + "heures_tp", + "id", + "matiere_id", "module_id", + "module_type", + "numero", + "semestre_id", + "titre", + "ue_id", } UE_FIELDS = { - "semestre_idx", - "type", - "formation_id", - "ue_code", - "id", - "ects", "acronyme", - "is_external", - "numero", "code_apogee", - "titre", "coefficient", "color", + "ects", + "formation_id", + "id", + "is_external", + "numero", + "semestre_idx", + "titre", + "type", + "ue_code", "ue_id", } BULLETIN_FIELDS = { - "version", - "type", "date", - "publie", + "etat_inscription", "etudiant", "formation", "formsemestre_id", - "etat_inscription", "options", + "publie", "ressources", "saes", - "ues", "semestre", + "type", + "ues", + "version", } BULLETIN_ETUDIANT_FIELDS = { + "boursier", "civilite", "code_ine", "code_nip", + "codepostaldomicile", "date_naissance", - "dept_id", "dept_acronym", + "dept_id", + "dept_naissance", + "description", + "domicile", "email", "emailperso", "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", - "fiche_url", - "photo_url", - "id", - "domicile", - "villedomicile", - "telephone", "fax", - "description", - "codepostaldomicile", + "fiche_url", + "id", + "lieu_naissance", + "nationalite", + "nom", + "nomprenom", "paysdomicile", + "photo_url", + "prenom", + "telephone", "telephonemobile", "typeadresse", + "villedomicile", } BULLETIN_FORMATION_FIELDS = {"id", "acronyme", "titre_officiel", "titre"} BULLETIN_OPTIONS_FIELDS = { - "show_abs", "show_abs_modules", - "show_ects", + "show_abs", "show_codemodules", + "show_coef", + "show_date_inscr", + "show_ects", "show_matieres", - "show_rangs", - "show_ue_rangs", + "show_minmax_eval", + "show_minmax_mod", + "show_minmax", "show_mod_rangs", "show_moypromo", - "show_minmax", - "show_minmax_mod", - "show_minmax_eval", - "show_coef", - "show_ue_cap_details", - "show_ue_cap_current", + "show_rangs", "show_temporary", - "temporary_txt", + "show_ue_cap_current", + "show_ue_cap_details", + "show_ue_rangs", "show_uevalid", - "show_date_inscr", + "temporary_txt", } BULLETIN_RESSOURCES_FIELDS = { @@ -346,23 +350,23 @@ BULLETIN_SAES_FIELDS = { ########### RESSOURCES ET SAES ########### BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_FIELDS = { - "id", - "titre", "code_apogee", - "url", - "moyenne", "evaluations", + "id", + "moyenne", + "titre", + "url", } BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_FIELDS = { - "id", - "description", + "coef", "date", + "description", "heure_debut", "heure_fin", - "coef", - "poids", + "id", "note", + "poids", "url", } @@ -373,10 +377,10 @@ BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_POIDS_FIELDS = { } BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = { - "value", - "min", "max", + "min", "moy", + "value", } @@ -384,19 +388,19 @@ BULLETIN_RESSOURCES_ET_SAES_RESSOURCE_ET_SAE_EVALUATION_NOTE_FIELDS = { BULLETIN_UES_FIELDS = {"RT1.1", "RT2.1", "RT3.1"} BULLETIN_UES_UE_FIELDS = { - "id", - "titre", - "numero", - "type", + "bonus", + "capitalise", "color", "competence", - "moyenne", - "bonus", + "ECTS", + "id", "malus", - "capitalise", + "moyenne", + "numero", "ressources", "saes", - "ECTS", + "titre", + "type", } BULLETIN_UES_UE_MOYENNE_FIELDS = {"value", "min", "max", "moy", "rang", "total"} @@ -461,16 +465,16 @@ BULLETIN_UES_UE_ECTS_FIELDS = {"acquis", "total"} ########### SEMESTRE ########### BULLETIN_SEMESTRE_FIELDS = { - "etapes", + "absences", + "annee_universitaire", "date_debut", "date_fin", - "annee_universitaire", - "numero", - "inscription", - "groupes", - "absences", "ECTS", + "etapes", + "groupes", + "inscription", "notes", + "numero", "rang", } @@ -484,78 +488,78 @@ BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"} EVAL_FIELDS = { - "id", - "description", + "coefficient", "date_debut", "date_fin", - "coefficient", + "description", + "etat", "evaluation_type", + "id", "moduleimpl_id", + "nb_inscrits", + "nb_notes_abs", + "nb_notes_att", + "nb_notes_exc", + "nb_notes_manquantes", "note_max", "numero", "poids", "publish_incomplete", - "visibulletin", - "etat", - "nb_inscrits", - "nb_notes_manquantes", - "nb_notes_abs", - "nb_notes_att", - "nb_notes_exc", "saisie_notes", + "visibulletin", } SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"} REF_COMP_FIELDS = { - "dept_id", "annexe", - "specialite", - "specialite_long", - "type_structure", - "type_departement", - "type_titre", - "version_orebut", + "competences", + "dept_id", + "parcours", "scodoc_date_loaded", "scodoc_orig_filename", - "competences", - "parcours", + "specialite_long", + "specialite", + "type_departement", + "type_structure", + "type_titre", + "version_orebut", } ABSENCES_FIELDS = { - "jour", - "matin", + "begin", + "description", + "end", "estabs", "estjust", - "description", - "begin", - "end", + "jour", + "matin", } ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"} FORMSEMESTRE_ETUD_FIELDS = { - "id", - "code_nip", - "code_ine", - "nom", - "nom_usuel", - "prenom", "civilite", + "code_ine", + "code_nip", "groups", + "id", + "nom_usuel", + "nom", + "prenom", } FORMSEMESTRE_ETUS_GROUPS_FIELDS = { - "partition_id", - "id", - "formsemestre_id", - "partition_name", - "numero", "bul_show_rank", - "show_in_lists", + "formsemestre_id", "group_id", "group_name", + "id", + "numero", + "partition_id", + "partition_name", + "show_in_lists", } EVALUATIONS_FIELDS = { @@ -573,107 +577,107 @@ EVALUATIONS_FIELDS = { } NOTES_FIELDS = { - "etudid", - "evaluation_id", - "value", "comment", "date", + "etudid", + "evaluation_id", "uid", + "value", } PARTITIONS_FIELDS = { - "id", - "formsemestre_id", - "partition_name", - "numero", "bul_show_rank", + "formsemestre_id", + "id", + "numero", + "partition_name", "show_in_lists", } PARTITION_GROUPS_ETUD_FIELDS = { - "id", + "civilite", + "code_ine", + "code_nip", "dept_id", + "id", + "nom_usuel", "nom", "prenom", - "nom_usuel", - "civilite", - "code_nip", - "code_ine", } FORMSEMESTRE_BULLETINS_FIELDS = { - "version", - "type", "date", - "publie", + "etat_inscription", "etudiant", "formation", "formsemestre_id", - "etat_inscription", "options", + "publie", "ressources", "saes", - "ues", "semestre", + "type", + "ues", + "version", } FORMSEMESTRE_BULLETINS_ETU_FIELDS = { + "boursier", "civilite", "code_ine", "code_nip", + "codepostaldomicile", "date_naissance", - "dept_id", "dept_acronym", + "dept_id", + "dept_naissance", + "description", + "domicile", "email", "emailperso", "etudid", - "nom", - "prenom", - "nomprenom", - "lieu_naissance", - "dept_naissance", - "nationalite", - "boursier", + "fax", "fiche_url", - "photo_url", "id", - "codepostaldomicile", + "lieu_naissance", + "nationalite", + "nom", + "nomprenom", "paysdomicile", + "photo_url", + "prenom", + "telephone", "telephonemobile", "typeadresse", - "domicile", "villedomicile", - "telephone", - "fax", - "description", } FORMSEMESTRE_BULLETINS_FORMATION_FIELDS = { - "id", "acronyme", + "id", "titre_officiel", "titre", } FORMSEMESTRE_BULLETINS_OPT_FIELDS = { - "show_abs", "show_abs_modules", - "show_ects", + "show_abs", "show_codemodules", + "show_coef", + "show_date_inscr", + "show_ects", "show_matieres", - "show_rangs", - "show_ue_rangs", + "show_minmax_eval", + "show_minmax_mod", + "show_minmax", "show_mod_rangs", "show_moypromo", - "show_minmax", - "show_minmax_mod", - "show_minmax_eval", - "show_coef", - "show_ue_cap_details", - "show_ue_cap_current", + "show_rangs", "show_temporary", - "temporary_txt", + "show_ue_cap_current", + "show_ue_cap_details", + "show_ue_rangs", "show_uevalid", - "show_date_inscr", + "temporary_txt", } diff --git a/tests/scenarios/test_scenario1_formation.py b/tests/scenarios/test_scenario1_formation.py index eb93f3d6..e1903da8 100644 --- a/tests/scenarios/test_scenario1_formation.py +++ b/tests/scenarios/test_scenario1_formation.py @@ -12,7 +12,8 @@ Usage: pytest tests/scenarios/test_scenario1_formation.py # code écrit par Fares Amer, mai 2021 et porté sur ScoDoc 8 en août 2021 from tests.unit import sco_fake_gen -from app.formations import edit_module, formation_io +from app.formations import formation_io +from app.models import Formation @pytest.mark.skip # test obsolete @@ -52,11 +53,11 @@ def run_scenario1(): ] # --- Implémentation des modules - modules = edit_module.module_list({"formation_id": formation_id}) + formation = Formation.get_formation(formation_id) mods_imp = [] - for mod in modules: + for mod in formation.modules: mi = G.create_moduleimpl( - module_id=mod["module_id"], - formsemestre_id=sems[mod["semestre_id"] - 1]["formsemestre_id"], + module_id=mod.id, + formsemestre_id=sems[mod.semestre_id - 1]["formsemestre_id"], ) mods_imp.append(mi) diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index ef397682..d9545c96 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -16,7 +16,7 @@ import typing from app import db, log from app.auth.models import User -from app.formations import edit_matiere, edit_module, edit_ue +from app.formations import edit_ue from app.models import ( Departement, Evaluation, @@ -24,13 +24,13 @@ from app.models import ( FormationModalite, Identite, Matiere, + Module, ModuleImpl, ) from app.scodoc import codes_cursus from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_validation -from app.scodoc import sco_moduleimpl from app.scodoc import sco_saisie_notes from app.scodoc import sco_synchro_etuds from app.scodoc import sco_utils as scu @@ -228,11 +228,8 @@ class ScoFake(object): matiere = db.session.get(Matiere, matiere_id) ue_id = matiere.ue.id formation_id = matiere.ue.formation.id - oid = edit_module.do_module_create(locals()) - oids = edit_module.module_list(args={"module_id": oid}) - if not oids: - raise ScoValueError(f"module not created ! (oid={oid})") - return oid + module = Module.create_from_dict(locals(), news=True, inval_cache=True) + return module.id @logging_meth def create_formsemestre( @@ -276,11 +273,8 @@ class ScoFake(object): ) -> int: if not responsable_id: responsable_id = self.default_user.id - oid = sco_moduleimpl.do_moduleimpl_create(locals()) - oids = sco_moduleimpl.moduleimpl_list(moduleimpl_id=oid) # API inconsistency - if not oids: - raise ScoValueError("moduleimpl not created !") - return oid + modimpl = ModuleImpl.create_from_dict(locals()) + return modimpl.id @logging_meth def inscrit_etudiant(self, formsemestre_id: int, etud: dict): diff --git a/tests/unit/test_formations.py b/tests/unit/test_formations.py index e234e1ff..a6811489 100644 --- a/tests/unit/test_formations.py +++ b/tests/unit/test_formations.py @@ -34,8 +34,7 @@ # - moduleimpl_list # - do_module_impl_with_module_list # - do_formsemestre_delete -# - module_list -# - do_module_delete +# - Module.delete # - ue_list # - do_ue_delete # - do_formation_delete @@ -52,7 +51,7 @@ from app.formations import ( edit_ue, formation_io, ) -from app.models import Formation, Matiere, ModuleImpl +from app.models import Formation, Matiere, Module, ModuleImpl from app.scodoc import sco_formsemestre from app.scodoc import sco_exceptions from app.scodoc import sco_formsemestre_edit @@ -259,7 +258,7 @@ def test_formations(test_client): formsemestre_id=sem2["formsemestre_id"] ) - li_module = edit_module.module_list() + li_module = Module.query.all() assert len(li_module) == 4 # Suppression impossible car utilisé dans le semestre formsemestre_idt: module3 = db.session.get(ModuleImpl, mi3).module @@ -268,13 +267,14 @@ def test_formations(test_client): sco_formsemestre_edit.do_formsemestre_delete(formsemestre_idt) - li_module2_before = edit_module.module_list() + li_module2_before = Module.query.all() - edit_module.do_module_delete(module3.id) - edit_module.do_module_delete(module_id_t) + module3.delete() + module_t = db.session.get(Module, module_id_t) + module_t.delete() # deuxieme methode de supression d'un module - li_module2_after = edit_module.module_list() + li_module2_after = Module.query.all() assert ( len(li_module2_after) == len(li_module2_before) - 2 @@ -340,14 +340,14 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"): ) ] # et les modules - modules = edit_module.module_list({"formation_id": formation_id}) - for mod in modules: + formation = Formation.get_formation(formation_id) + for mod in formation.modules: moduleimpl_id = G.create_moduleimpl( - module_id=mod["module_id"], - formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1], + module_id=mod.id, + formsemestre_id=formsemestre_ids[mod.semestre_id - 1], ) mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - assert mi["module_id"] == mod["module_id"] + assert mi["module_id"] == mod.id # --- Export formation en XML doc1 = formation_io.formation_export(formation_id, fmt="xml").get_data(as_text=True)