From 3b984ea823fb9abc89eba451339d639e5e58efa8 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet <emmanuel.viennet@gmail.com> Date: Thu, 17 Oct 2024 23:44:54 +0200 Subject: [PATCH] =?UTF-8?q?Edition=20UEs:=20assouplie=20verrouillage=20en?= =?UTF-8?q?=20BUT.=20+=20d=C3=A9but=20modernisation=20code.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/formations/edit_ue.py | 218 ++++++++++++++------------------------ app/models/ues.py | 60 ++++++++++- 2 files changed, 134 insertions(+), 144 deletions(-) diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py index 3712a854..1b9e3e33 100644 --- a/app/formations/edit_ue.py +++ b/app/formations/edit_ue.py @@ -45,7 +45,6 @@ from app.models import ( FormSemestreUECoef, Matiere, Module, - ModuleImpl, UniteEns, ) from app.models import ApcValidationRCUE, ScolarFormSemestreValidation, ScolarEvent @@ -546,6 +545,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No formation_id, int(tf[2]["semestre_idx"]) ) ue_id = do_ue_create(tf[2]) + matiere_id = None if is_apc or cursus.UE_IS_MODULE or tf[2]["create_matiere"]: # rappel: en APC, toutes les UE ont une matière, créée ici # (inutilisée mais à laquelle les modules sont rattachés) @@ -597,46 +597,21 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No ) -def _add_ue_semestre_id(ues: list[dict], is_apc): - """ajoute semestre_id dans les ue, en regardant - semestre_idx ou à défaut, pour les formations non APC, le premier module - de chacune. - Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), - qui les place à la fin de la liste. - """ - for ue in ues: - if ue["semestre_idx"] is not None: - ue["semestre_id"] = ue["semestre_idx"] - elif is_apc: - ue["semestre_id"] = codes_cursus.UE_SEM_DEFAULT - else: - # était le comportement ScoDoc7 - 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 - - -def next_ue_numero(formation_id, semestre_id=None): +def next_ue_numero(formation_id, semestre_id=None) -> int: """Numero d'une nouvelle UE dans cette formation. Si le semestre est specifie, cherche les UE ayant des modules de ce semestre """ formation = db.session.get(Formation, formation_id) - ues = ue_list(args={"formation_id": formation_id}) + ues = formation.ues.all() if not ues: return 0 if semestre_id is None: - return ues[-1]["numero"] + 1000 - else: - # Avec semestre: (prend le semestre du 1er module de l'UE) - _add_ue_semestre_id(ues, formation.get_cursus().APC_SAE) - ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id] - if ue_list_semestre: - return ue_list_semestre[-1]["numero"] + 10 - else: - return ues[-1]["numero"] + 1000 + return ues[-1].numero + 1000 + # Avec semestre: (prend le semestre du 1er module de l'UE) + ue_list_semestre = [ue for ue in ues if ue.get_semestre_id() == semestre_id] + if ue_list_semestre: + return ue_list_semestre[-1].numero + 10 + return ues[-1].numero + 1000 def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): @@ -698,20 +673,22 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): show_tags = scu.to_bool(request.args.get("show_tags", 0)) locked = formation.has_locked_sems(semestre_idx) semestre_ids = range(1, parcours.NB_SEM + 1) - # transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7 - # basées sur des dicts - ues_obj = UniteEns.query.filter_by( - formation_id=formation_id, is_external=False - ).order_by(UniteEns.semestre_idx, UniteEns.numero) + + ues = ( + formation.ues.filter_by(is_external=False) + .order_by(UniteEns.semestre_idx, UniteEns.numero) + .all() + ) + # safety check: renumérote les ue s'il en manque ou s'il y a des ex-aequo. # cela facilite le travail de la passerelle ! - numeros = {ue.numero for ue in ues_obj} - if (None in numeros) or len(numeros) < ues_obj.count(): - scu.objects_renumber(db, ues_obj) + numeros = {ue.numero for ue in ues} + if (None in numeros) or len(numeros) < len(ues): + scu.objects_renumber(db, ues) - ues_externes_obj = UniteEns.query.filter_by( + ues_externes = UniteEns.query.filter_by( formation_id=formation_id, is_external=True - ) + ).all() # liste ordonnée des formsemestres de cette formation: formsemestres = sorted( FormSemestre.query.filter_by(formation_id=formation_id).all(), @@ -721,29 +698,25 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): if is_apc: # Pour faciliter la transition des anciens programmes non APC - for ue in ues_obj: + for ue in ues: ue.guess_semestre_idx() # vérifie qu'on a bien au moins une matière dans chaque UE if ue.matieres.count() < 1: mat = Matiere(ue_id=ue.id) db.session.add(mat) # donne des couleurs aux UEs crées avant - colorie_anciennes_ues(ues_obj) + colorie_anciennes_ues(ues) db.session.commit() - ues = [ue.to_dict() for ue in ues_obj] - ues_externes = [ue.to_dict() for ue in ues_externes_obj] # tri par semestre et numero: - _add_ue_semestre_id(ues, is_apc) - _add_ue_semestre_id(ues_externes, is_apc) - ues.sort(key=lambda u: (u["semestre_id"], u["numero"])) - ues_externes.sort(key=lambda u: (u["semestre_id"], u["numero"])) + ues.sort(key=lambda u: (u.get_semestre_id(), u.numero)) + ues_externes.sort(key=lambda u: (u.get_semestre_id(), u.numero)) # Codes dupliqués (pour aider l'utilisateur) seen = set() duplicated_codes = { - ue["ue_code"] for ue in ues if ue["ue_code"] in seen or seen.add(ue["ue_code"]) + ue.ue_code for ue in ues if ue.ue_code in seen or seen.add(ue.ue_code) } - ues_with_duplicated_code = [ue for ue in ues if ue["ue_code"] in duplicated_codes] + ues_with_duplicated_code = [ue for ue in ues if ue.ue_code in duplicated_codes] has_perm_change = current_user.has_permission(Permission.EditFormation) # editable = (not locked) and has_perm_change @@ -799,8 +772,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); formation ont le même code : <tt>{ ', '.join([ '<a class="stdlink" href="' + url_for( "notes.ue_edit", - scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"] ) - + '">' + ue["acronyme"] + " (code " + ue["ue_code"] + ")</a>" + scodoc_dept=g.scodoc_dept, ue_id=ue.id ) + + '">' + ue.acronyme + " (code " + ue.ue_code + ")</a>" for ue in ues_with_duplicated_code ]) }</tt>. Il faut corriger cela, sinon les capitalisations et ECTS seront @@ -1115,7 +1088,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx): def _ue_table_ues( parcours, - ues: list[dict], + ues: list[UniteEns], editable, tag_editable, has_perm_change, @@ -1132,33 +1105,25 @@ def _ue_table_ues( cur_ue_semestre_id = None iue = 0 for ue in ues: - if ue["ects"] is None: - ue["ects_str"] = "" - else: - ue["ects_str"] = ", %g ECTS" % ue["ects"] - if editable: - klass = "span_apo_edit" - else: - klass = "" + ects_str = "" if ue.ects is None else f", {ue.ects:g} ECTS" + klass = "span_apo_edit" if editable else "" edit_url = url_for( "apiweb.ue_set_code_apogee", scodoc_dept=g.scodoc_dept, - ue_id=ue["ue_id"], + ue_id=ue.id, ) - ue[ - "code_apogee_str" - ] = f""", Apo: <span - class="{klass}" data-url="{edit_url}" id="{ue['ue_id']}" + code_apogee_str = f""", Apo: <span + class="{klass}" data-url="{edit_url}" id="{ue.id}" data-placeholder="{scu.APO_MISSING_CODE_STR}">{ - ue["code_apogee"] or "" + ue.code_apogee or "" }</span>""" - if cur_ue_semestre_id != ue["semestre_id"]: - cur_ue_semestre_id = ue["semestre_id"] - if ue["semestre_id"] == codes_cursus.UE_SEM_DEFAULT: + if cur_ue_semestre_id != ue.semestre_id: + cur_ue_semestre_id = ue.semestre_id + if ue.semestre_id == codes_cursus.UE_SEM_DEFAULT: lab = "Pas d'indication de semestre:" else: - lab = f"""Semestre {ue["semestre_id"]}:""" + lab = f"""Semestre {ue.semestre_id}:""" H.append( f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>' ) @@ -1166,52 +1131,55 @@ def _ue_table_ues( H.append('<li class="notes_ue_list">') if iue != 0 and editable: H.append( - '<a href="ue_move?ue_id=%s&after=0" class="aud">%s</a>' - % (ue["ue_id"], arrow_up) + f"""<a href="{ + url_for( 'notes.ue_move', + scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0)}" + class="aud">{arrow_up}</a>""" ) else: H.append(arrow_none) if iue < len(ues) - 1 and editable: H.append( - '<a href="ue_move?ue_id=%s&after=1" class="aud">%s</a>' - % (ue["ue_id"], arrow_down) + f"""<a href="{ + url_for( 'notes.ue_move', + scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1)}" + class="aud">{arrow_down}</a>""" ) else: H.append(arrow_none) - ue["acro_titre"] = str(ue["acronyme"]) - if ue["titre"] != ue["acronyme"]: - ue["acro_titre"] += " " + str(ue["titre"]) + acro_titre = ue.acronyme + if ue.titre != ue.acronyme: + acro_titre += " " + (ue.titre or "") H.append( - """%(acro_titre)s <span class="ue_code">(code %(ue_code)s%(ects_str)s, coef. %(coefficient)3.2f%(code_apogee_str)s)</span> + f"""acro_titre <span class="ue_code">(code {ue.ue_code}{ects_str}, coef. { + (ue.coefficient or 0):3.2f}{code_apogee_str})</span> <span class="ue_coef"></span> """ - % ue ) - if ue["type"] != codes_cursus.UE_STANDARD: + if ue.type != codes_cursus.UE_STANDARD: H.append( - '<span class="ue_type">%s</span>' - % codes_cursus.UE_TYPE_NAME[ue["type"]] + f"""<span class="ue_type">{codes_cursus.UE_TYPE_NAME[ue.type]}</span>""" ) - if ue["is_external"]: + if ue.is_external: # Cas spécial: si l'UE externe a plus d'un module, c'est peut être une UE # qui a été déclarée externe par erreur (ou suite à un bug d'import/export xml) # Dans ce cas, propose de changer le type (même si verrouillée) - if len(sco_moduleimpl.moduleimpls_in_external_ue(ue["ue_id"])) > 1: + if len(sco_moduleimpl.moduleimpls_in_external_ue(ue.id)) > 1: H.append('<span class="ue_is_external">') if has_perm_change: H.append( f"""<a class="stdlink" href="{ url_for("notes.ue_set_internal", - scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"]) + scodoc_dept=g.scodoc_dept, ue_id=ue.id) }">transformer en UE ordinaire</a> """ ) H.append("</span>") - ue_locked, ue_locked_reason = ue_is_locked(ue["ue_id"]) + ue_locked, ue_locked_reason = ue.is_locked() ue_editable = editable and not ue_locked if ue_editable: H.append( f"""<a class="stdlink" href="{ - url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue["ue_id"]) + url_for("notes.ue_edit", scodoc_dept=g.scodoc_dept, ue_id=ue.id) }">modifier</a>""" ) else: @@ -1231,11 +1199,14 @@ def _ue_table_ues( delete_disabled_icon, ) ) - if (iue >= len(ues) - 1) or ue["semestre_id"] != ues[iue + 1]["semestre_id"]: + if (iue >= len(ues) - 1) or ( + ue.get_semestre_id() != ues[iue + 1].get_semestre_id() + ): H.append( - f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, - formation_id=ue['formation_id'], semestre_idx=ue['semestre_id']) - }">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul> + f"""</ul><ul><li><a href="{ + url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, + formation_id=ue.formation_id, semestre_idx=ue.semestre_id) + }">Ajouter une UE dans le semestre {ue.semestre_id or ''}</a></li></ul> </div> """ ) @@ -1246,7 +1217,7 @@ def _ue_table_ues( def _ue_table_matieres( parcours, - ue_dict: dict, + ue: UniteEns, editable, tag_editable, arrow_up, @@ -1256,7 +1227,6 @@ def _ue_table_matieres( delete_disabled_icon, ): """Édition de programme: liste des matières (et leurs modules) d'une UE.""" - ue = UniteEns.get_ue(ue_dict["ue_id"]) H = [] if not parcours.UE_IS_MODULE: H.append('<ul class="notes_matiere_list">') @@ -1503,16 +1473,18 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): "edit an UE" # check ue_id = args["ue_id"] - ue = ue_list({"ue_id": ue_id})[0] + ue = UniteEns.get_ue(ue_id) if not bypass_lock: - ue_locked, ue_locked_reason = ue_is_locked(ue["ue_id"]) + ue_locked, ue_locked_reason = ue.is_locked() if ue_locked: raise ScoLockedFormError(msg=f"UE verrouillée: {ue_locked_reason}") # check: acronyme unique dans cette formation if "acronyme" in args: new_acro = args["acronyme"] - ues = ue_list({"formation_id": ue["formation_id"], "acronyme": new_acro}) - if ues and ues[0]["ue_id"] != ue_id: + ues = UniteEns.query.filter_by( + formation_id=ue.formation_id, acronyme=new_acro + ).all() + if ues and ues[0].id != ue_id: raise ScoValueError( f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé ! (chaque UE doit avoir un acronyme unique dans la formation.)""" @@ -1521,48 +1493,12 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False): if "ue_code" in args and not args["ue_code"]: del args["ue_code"] - cnx = ndb.GetDBConnexion() - _ueEditor.edit(cnx, args) - - formation = db.session.get(Formation, ue["formation_id"]) + ue.from_dict(args) + db.session.commit() if not dont_invalidate_cache: # Invalide les semestres utilisant cette formation # ainsi que les poids et coefs - formation.invalidate_module_coefs() - - -def ue_is_locked(ue_id: int) -> tuple[bool, str]: - """True if UE should not be modified: - utilisée dans un formsemestre verrouillé ou validations de jury de cette UE. - Renvoie aussi une explication. - """ - # before 9.7.23: contains modules used in a locked formsemestre - # starting from 9.7.23: + existence de validations de jury de cette UE - ue = UniteEns.query.get(ue_id) - if not ue: - return True, "inexistante" - if ue.formation.is_apc(): - # en APC, interdit toute modification d'UE si utilisée dans un semestre verrouillé - if False in [formsemestre.etat for formsemestre in ue.formation.formsemestres]: - return True, "utilisée dans un semestre verrouillé" - else: - # en classique: interdit si contient des modules utilisés dans des semestres verrouillés - # en effet, dans certaines (très anciennes) formations, une UE peut avoir des modules de - # différents semestre - if ( - Module.query.filter(Module.ue_id == ue_id) - .join(Module.modimpls) - .join(ModuleImpl.formsemestre) - .filter_by(etat=False) - .count() - ): - return True, "avec modules utilisés dans des semestres verrouillés" - - nb_validations = ScolarFormSemestreValidation.query.filter_by(ue_id=ue_id).count() - if nb_validations > 0: - return True, f"avec {nb_validations} validations de jury" - - return False, "" + ue.formation.invalidate_module_coefs() UE_PALETTE = [ diff --git a/app/models/ues.py b/app/models/ues.py index 3e74b88b..f7ad5ea7 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -10,6 +10,7 @@ from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models.but_refcomp import ApcNiveau, ApcParcours from app.models.modules import Module +from app.scodoc import codes_cursus from app.scodoc import sco_utils as scu @@ -185,10 +186,45 @@ class UniteEns(models.ScoDocModel): return 1 if self.semestre_idx is None else (self.semestre_idx - 1) // 2 + 1 def is_locked(self) -> tuple[bool, str]: - """True if UE should not be modified""" - from app.formations import edit_ue + """True if UE should not be modified: + utilisée dans un formsemestre verrouillé ou validations de jury de cette UE. + Renvoie aussi une explication. + """ + from app.models import FormSemestre, ModuleImpl, ScolarFormSemestreValidation + + # before 9.7.23: contains modules used in a locked formsemestre + # starting from 9.7.23: + existence de validations de jury de cette UE + if self.formation.is_apc(): + # en APC, interdit toute modification d'UE si il y a un formsemestre verrouillé + # de cette formation ayant le semestre de cette UE. + # (ne détaille pas les parcours, donc si un semestre Sn d'un parcours est verrouillé + # cela va verrouiller toutes les UE d'indice Sn, même si pas de ce parcours) + # modifié en 9.7.28 + locked_sems = self.formation.formsemestres.filter_by( + etat=False, semestre_id=self.semestre_idx + ) + if locked_sems.count(): + return True, "utilisée dans un semestre verrouillé" + else: + # en classique: interdit si contient des modules utilisés dans des semestres verrouillés + # en effet, dans certaines (très anciennes) formations, une UE peut avoir des modules de + # différents semestre + if ( + Module.query.filter(Module.ue_id == self.id) + .join(Module.modimpls) + .join(ModuleImpl.formsemestre) + .filter_by(etat=False) + .count() + ): + return True, "avec modules utilisés dans des semestres verrouillés" + + nb_validations = ScolarFormSemestreValidation.query.filter_by( + ue_id=self.id + ).count() + if nb_validations > 0: + return True, f"avec {nb_validations} validations de jury" - return edit_ue.ue_is_locked(self.id) + return False, "" def can_be_deleted(self) -> bool: """True si l'UE n'a pas de moduleimpl rattachés @@ -214,6 +250,24 @@ class UniteEns(models.ScoDocModel): db.session.commit() return self.semestre_idx + def get_semestre_id(self) -> int: + """L'indice du semestre de l'UE. + Regarde semestre_idx ou, pour les formations non APC, + le premier module de chacune. + Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), + qui les place à la fin de la liste. + Contrairement à guess_semestre_idx, ne modifie pas l'UE. + """ + if self.semestre_idx is not None: + return self.semestre_idx + if self.formation.is_apc(): + return codes_cursus.UE_SEM_DEFAULT + # était le comportement ScoDoc7 + module = self.modules.first() + if module: + return module.semestre_id + return codes_cursus.UE_SEM_DEFAULT + def get_ects(self, parcour: ApcParcours = None, only_parcours=False) -> float: """Crédits ECTS associés à cette UE. En BUT, cela peut quelquefois dépendre du parcours. -- GitLab