diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index cc51e2a28..9441c0316 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -71,7 +71,7 @@ class BulletinBUT(ResultatsSemestreBUT): "bonus": fmt_note(self.bonus_ues[ue.id][etud.id]) if self.bonus_ues is not None and ue.id in self.bonus_ues else fmt_note(0.0), - "malus": None, # XXX TODO voir ce qui est ici + "malus": self.malus[ue.id][etud.id], "capitalise": None, # "AAAA-MM-JJ" TODO "ressources": self.etud_ue_mod_results(etud, ue, self.ressources), "saes": self.etud_ue_mod_results(etud, ue, self.saes), diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index e7f97c13d..60361c32c 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -104,7 +104,6 @@ class BonusSport: # sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport) # ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus) nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2] - nb_ues = len(ues) # Enlève les NaN du numérateur: sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0) @@ -124,7 +123,7 @@ class BonusSport: # Annule les coefs des modules où l'étudiant n'est pas inscrit: modimpl_coefs_etuds = np.where( modimpl_inscr_spo_stacked, - np.stack([modimpl_coefs_spo.T] * nb_etuds), + np.stack([modimpl_coefs_spo] * nb_etuds), 0.0, ) else: @@ -162,7 +161,7 @@ class BonusSport: """ raise NotImplementedError("méthode virtuelle") - def get_bonus_ues(self) -> pd.Series: + def get_bonus_ues(self) -> pd.DataFrame: """Les bonus à appliquer aux UE Résultat: DataFrame de float, index etudid, columns: ue.id """ diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index bf4afe7a0..2e2298dbe 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -43,6 +43,8 @@ from app.scodoc import sco_utils as scu from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_exceptions import ScoValueError +from app.scodoc.sco_utils import ModuleType + @dataclass class EvaluationEtat: @@ -233,6 +235,8 @@ class ModuleImplResultsAPC(ModuleImplResults): assert evals_poids_df.shape[0] == nb_evals # compat notes/poids if nb_etuds == 0: return pd.DataFrame(index=[], columns=evals_poids_df.columns) + if nb_ues == 0: + return pd.DataFrame(index=self.evals_notes.index, columns=[]) evals_coefs = self.get_evaluations_coefs(moduleimpl) evals_poids = evals_poids_df.values * evals_coefs # -> evals_poids shape : (nb_evals, nb_ues) @@ -289,7 +293,12 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... # Initialise poids non enregistrés: - default_poids = 1.0 if modimpl.module.ue.type == UE_SPORT else 0.0 + default_poids = ( + 1.0 + if modimpl.module.ue.type == UE_SPORT + or modimpl.module.module_type == ModuleType.MALUS + else 0.0 + ) if np.isnan(evals_poids.values.flat).any(): ue_coefs = modimpl.module.get_ue_coef_dict() diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index d7ed47667..8a8057ac3 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -27,6 +27,7 @@ """Fonctions de calcul des moyennes d'UE (classiques ou BUT) """ +from re import X import numpy as np import pandas as pd @@ -263,9 +264,10 @@ def compute_ue_moys_apc( # # Version vectorisée # - etud_moy_ue = np.sum( - modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 - ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) + with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) + etud_moy_ue = np.sum( + modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 + ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) return pd.DataFrame( etud_moy_ue, index=modimpl_inscr_df.index, # les etudids @@ -379,3 +381,42 @@ def compute_ue_moys_classic( etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index) return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df + + +def compute_malus( + formsemestre: FormSemestre, + sem_modimpl_moys: np.array, + ues: list[UniteEns], + modimpl_inscr_df: pd.DataFrame, +) -> pd.DataFrame: + """Calcul le malus sur les UE + Dans chaque UE, on peut avoir un ou plusieurs modules de MALUS. + Leurs notes sont positives ou négatives. leur somme sera _soustraite_ à la moyenne + de chaque UE. + Arguments: + - sem_modimpl_moys : + notes moyennes aux modules (tous les étuds x tous les modimpls) + floats avec des NaN. + En classique: sem_matrix, ndarray (etuds x modimpls) + En APC: sem_cube, ndarray (etuds x modimpls x UEs non bonus) + - ues: les ues du semestre (incluant le bonus sport) + - modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl) + + Résultat: DataFrame de float, index etudid, columns: ue.id (sans NaN) + """ + ues_idx = [ue.id for ue in ues] + malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float) + for ue in ues: + if ue.type != UE_SPORT: + modimpl_mask = np.array( + [ + (m.module.module_type == ModuleType.MALUS) + and (m.module.ue.id == ue.id) + for m in formsemestre.modimpls_sorted + ] + ) + malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1) + malus[ue.id] = malus_moys + + malus.fillna(0.0, inplace=True) + return malus diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 4423f3fa1..c4697238d 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -68,6 +68,12 @@ class ResultatsSemestreBUT(NotesTableCompat): 1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns ) + # --- Modules de MALUS sur les UEs + self.malus = moy_ue.compute_malus( + self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df + ) + self.etud_moy_ue -= self.malus + # --- Bonus Sport & Culture if len(modimpls_sport) > 0: bonus_class = ScoDocSiteConfig.get_bonus_sport_class() diff --git a/app/comp/res_classic.py b/app/comp/res_classic.py index 8d52d0c32..58c505ee5 100644 --- a/app/comp/res_classic.py +++ b/app/comp/res_classic.py @@ -71,6 +71,16 @@ class ResultatsSemestreClassic(NotesTableCompat): self.modimpl_coefs, modimpl_standards_mask, ) + # --- Modules de MALUS sur les UEs et la moyenne générale + self.malus = moy_ue.compute_malus( + self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df + ) + self.etud_moy_ue -= self.malus + # ajuste la moyenne générale (à l'aide des coefs d'UE) + self.etud_moy_gen -= (self.etud_coef_ue_df * self.malus).sum( + axis=1 + ) / self.etud_coef_ue_df.sum(axis=1) + # --- Bonus Sport & Culture bonus_class = ScoDocSiteConfig.get_bonus_sport_class() if bonus_class is not None: diff --git a/app/forms/main/config_apo.py b/app/forms/main/config_apo.py index 6151f2f31..6ebb6a8b3 100644 --- a/app/forms/main/config_apo.py +++ b/app/forms/main/config_apo.py @@ -70,7 +70,7 @@ class CodesDecisionsForm(FlaskForm): ATT = _build_code_field("ATT") CMP = _build_code_field("CMP") DEF = _build_code_field("DEF") - DEM = _build_code_field("DEF") + DEM = _build_code_field("DEM") NAR = _build_code_field("NAR") RAT = _build_code_field("RAT") submit = SubmitField("Valider") diff --git a/app/models/config.py b/app/models/config.py index 9c9c56385..8a56d3879 100644 --- a/app/models/config.py +++ b/app/models/config.py @@ -178,7 +178,7 @@ class ScoDocSiteConfig(db.Model): return getattr(bonus_sport, func_name) except AttributeError: raise ScoValueError( - f"""Fonction de calcul maison inexistante: {func_name}. + f"""Fonction de calcul de l'UE bonus inexistante: "{func_name}". (contacter votre administrateur local).""" ) diff --git a/app/scodoc/TrivialFormulator.py b/app/scodoc/TrivialFormulator.py index 23b500393..25442194f 100644 --- a/app/scodoc/TrivialFormulator.py +++ b/app/scodoc/TrivialFormulator.py @@ -276,14 +276,24 @@ class TF(object): ) ok = 0 if typ[:3] == "int" or typ == "float" or typ == "real": - if "min_value" in descr and val < descr["min_value"]: + if ( + val != "" + and val != None + and "min_value" in descr + and val < descr["min_value"] + ): msg.append( "La valeur (%d) du champ '%s' est trop petite (min=%s)" % (val, field, descr["min_value"]) ) ok = 0 - if "max_value" in descr and val > descr["max_value"]: + if ( + val != "" + and val != None + and "max_value" in descr + and val > descr["max_value"] + ): msg.append( "La valeur (%s) du champ '%s' est trop grande (max=%s)" % (val, field, descr["max_value"]) diff --git a/app/scodoc/bonus_sport.py b/app/scodoc/bonus_sport.py index 5fc3b8b4d..b49b6159b 100644 --- a/app/scodoc/bonus_sport.py +++ b/app/scodoc/bonus_sport.py @@ -456,6 +456,11 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None): return bonus +def bonus_iutlemans(notes_sport, coefs, infos=None): + "fake: formule inutilisée en ScoDoc 9.2 mais doiut être présente" + return 0.0 + + def bonus_iutlr(notes_sport, coefs, infos=None): """Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point diff --git a/app/scodoc/sco_apogee_csv.py b/app/scodoc/sco_apogee_csv.py index 565d1168b..04a2fe2c5 100644 --- a/app/scodoc/sco_apogee_csv.py +++ b/app/scodoc/sco_apogee_csv.py @@ -125,8 +125,8 @@ APO_NEWLINE = "\r\n" def _apo_fmt_note(note): "Formatte une note pour Apogée (séparateur décimal: ',')" - if not note and isinstance(note, float): - return "" + # if not note and isinstance(note, float): changé le 31/1/2022, étrange ? + # return "" try: val = float(note) except ValueError: diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index 6bcb8cc32..823dd19fc 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -141,7 +141,7 @@ BUG = "BUG" ALL = "ALL" -# Explication des codes (de demestre ou d'UE) +# Explication des codes (de semestre ou d'UE) CODES_EXPL = { ADC: "Validé par compensation", ADJ: "Validé par le Jury", @@ -154,6 +154,7 @@ CODES_EXPL = { DEF: "Défaillant", NAR: "Échec, non autorisé à redoubler", RAT: "En attente d'un rattrapage", + DEM: "Démission", } # Nota: ces explications sont personnalisables via le fichier # de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index ae143cbc0..7bcfcd78d 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -115,22 +115,30 @@ def do_module_create(args) -> int: return r -def module_create(matiere_id=None, module_type=None, semestre_id=None): - """Création d'un module""" +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). + """ from app.scodoc import sco_formations from app.scodoc import sco_edit_ue - matiere = Matiere.query.get_or_404(matiere_id) - if matiere is None: - raise ScoValueError("invalid matiere !") - ue = matiere.ue - parcours = ue.formation.get_parcours() + if matiere_id: + matiere = Matiere.query.get_or_404(matiere_id) + ue = matiere.ue + formation = ue.formation + else: + formation = Formation.query.get_or_404(formation_id) + parcours = formation.get_parcours() is_apc = parcours.APC_SAE - ues = ue.formation.ues.order_by( + ues = formation.ues.order_by( UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme ).all() # cherche le numero adéquat (pour placer le module en fin de liste) - modules = matiere.ue.formation.modules.all() + modules = formation.modules.all() if modules: default_num = max([m.numero or 0 for m in modules]) + 10 else: @@ -143,9 +151,11 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): H = [ html_sco_header.sco_header(page_title=f"Création {object_name}"), ] - if is_apc: + if not matiere_id: H += [ - f"""

Création {object_name} dans la formation {ue.formation.acronyme}, Semestre {ue.semestre_idx}, {ue.acronyme}

""" + f"""

Création {object_name} dans la formation {formation.acronyme} +

+ """ ] else: H += [ @@ -158,7 +168,6 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): render_template( "scodoc/help/modules.html", is_apc=is_apc, - ue=ue, semestre_id=semestre_id, ) ] @@ -170,7 +179,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): "size": 10, "explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.", "allow_null": False, - "validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity( + "validator": lambda val, field, formation_id=formation_id: check_module_code_unicity( val, field, formation_id ), }, @@ -192,6 +201,15 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): ] semestres_indices = list(range(1, parcours.NB_SEM + 1)) + if is_apc: + module_types = scu.ModuleType # tous les types + else: + # ne propose pas SAE et Ressources: + module_types = set(scu.ModuleType) - { + scu.ModuleType.RESSOURCE, + scu.ModuleType.SAE, + } + descr += [ ( "module_type", @@ -199,8 +217,8 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): "input_type": "menu", "title": "Type", "explanation": "", - "labels": [x.name.capitalize() for x in scu.ModuleType], - "allowed_values": [str(int(x)) for x in scu.ModuleType], + "labels": [x.name.capitalize() for x in module_types], + "allowed_values": [str(int(x)) for x in module_types], }, ), ( @@ -256,11 +274,30 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): ), ] + if matiere_id: + descr += [ + ("ue_id", {"default": ue.id, "input_type": "hidden"}), + ("matiere_id", {"default": matiere_id, "input_type": "hidden"}), + ] + else: + # choix de l'UE de rattachement + descr += [ + ( + "ue_id", + { + "input_type": "menu", + "type": "int", + "title": "UE de rattachement", + "explanation": "utilisée notamment pour les malus", + "labels": [f"{u.acronyme} {u.titre}" for u in ues], + "allowed_values": [u.id for u in ues], + }, + ), + ] + descr += [ # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }), - ("formation_id", {"default": ue.formation_id, "input_type": "hidden"}), - ("ue_id", {"default": ue.id, "input_type": "hidden"}), - ("matiere_id", {"default": matiere.id, "input_type": "hidden"}), + ("formation_id", {"default": formation.id, "input_type": "hidden"}), ( "code_apogee", { @@ -290,6 +327,20 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): if tf[0] == 0: return "\n".join(H) + tf[1] + html_sco_header.sco_footer() else: + if not matiere_id: + # formulaire avec choix UE de rattachement + ue = UniteEns.query.get(tf[2]["ue_id"]) + if ue is None: + raise ValueError("UE invalide") + matiere = ue.matieres.first() + if matiere: + tf[2]["matiere_id"] = matiere.id + else: + matiere_id = sco_edit_matiere.do_matiere_create( + {"ue_id": ue.id, "titre": ue.titre, "numero": 1}, + ) + tf[2]["matiere_id"] = matiere_id + tf[2]["semestre_id"] = ue.semestre_idx _ = do_module_create(tf[2]) @@ -298,7 +349,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue.formation_id, + formation_id=formation.id, semestre_idx=tf[2]["semestre_id"], ) ) @@ -493,6 +544,13 @@ def module_edit(module_id=None): soyez prudents ! """ ) + if is_apc: + module_types = scu.ModuleType # tous les types + else: + # ne propose pas SAE et Ressources, sauf si déjà de ce type... + module_types = ( + set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} + ) | {a_module.module_type} descr = [ ( @@ -514,8 +572,8 @@ def module_edit(module_id=None): "input_type": "menu", "title": "Type", "explanation": "", - "labels": [x.name.capitalize() for x in scu.ModuleType], - "allowed_values": [str(int(x)) for x in scu.ModuleType], + "labels": [x.name.capitalize() for x in module_types], + "allowed_values": [str(int(x)) for x in module_types], "enabled": unlocked, }, ), diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 5b333a9fd..b5c3d4ab2 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -998,6 +998,7 @@ def _ue_table_matieres( H.append( _ue_table_modules( parcours, + ue, mat, modules, editable, @@ -1031,6 +1032,7 @@ def _ue_table_matieres( def _ue_table_modules( parcours, + ue, mat, modules, editable, @@ -1121,8 +1123,12 @@ def _ue_table_modules( tag_cls, ",".join(sco_tag_module.module_tag_list(mod["module_id"])), ) + if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]: + warning_semestre = ' incohérent ?' + else: + warning_semestre = "" H.append( - " %s %s" % (parcours.SESSION_NAME, mod["semestre_id"]) + " %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre) + " (%s)" % heurescoef + tag_edit ) diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index f1cf22644..6de171d16 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -546,7 +546,7 @@ def do_formsemestre_createwithmodules(edit=False): for mod in mods: if mod["semestre_id"] == semestre_id and ( (not edit) # creation => tous modules - or (not formation.is_apc()) # pas BUT, on peux mixer les semestres + or (not formation.is_apc()) # pas BUT, on peut mixer les semestres or (semestre_id == formsemestre.semestre_id) # module du semestre or (mod["module_id"] in module_ids_set) # module déjà présent ): diff --git a/app/scodoc/sco_moduleimpl_status.py b/app/scodoc/sco_moduleimpl_status.py index 3cf49e3b5..700115e52 100644 --- a/app/scodoc/sco_moduleimpl_status.py +++ b/app/scodoc/sco_moduleimpl_status.py @@ -219,7 +219,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None): page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}" ), f"""

{mod_type_name} - {Mod['code']} {Mod['titre']}

+ {Mod['code']} {Mod['titre']} + {"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""} +
diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html index e5ca79e5a..2be3cfe24 100644 --- a/app/templates/pn/form_mods.html +++ b/app/templates/pn/form_mods.html @@ -71,14 +71,22 @@ {% endfor %} - {% if editable and matiere_parent %} -
  • {{create_element_msg}} + )}}" + {% else %}"{{ + url_for("notes.module_create", + scodoc_dept=g.scodoc_dept, + module_type=module_type|int, + formation_id=formation.id + )}}" + {% endif %} + >{{create_element_msg}}
  • {% endif %} {% endif %} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 0b37da544..e60fd63ef 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -191,7 +191,7 @@ def get_etud_dept(): # le choix a peu d'importance... last_etud = etuds[-1] - return Departement.query.get(last_etud.dept_id).acronym + return Departement.query.get_or_404(last_etud.dept_id).acronym # Bricolage pour le portail IUTV avec ScoDoc 7: (DEPRECATED: NE PAS UTILISER !) diff --git a/app/views/users.py b/app/views/users.py index 0655da501..06f49979b 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -149,7 +149,7 @@ def user_info(user_name, format="json"): @scodoc @permission_required(Permission.ScoUsersAdmin) @scodoc7func -def create_user_form(user_name=None, edit=0, all_roles=1): +def create_user_form(user_name=None, edit=0, all_roles=False): "form. création ou édition utilisateur" if user_name is not None: # scodoc7func converti en int ! user_name = str(user_name) @@ -218,9 +218,11 @@ def create_user_form(user_name=None, edit=0, all_roles=1): } if current_user.is_administrator(): editable_roles_set |= { - (Role.get_named_role(r), "") + (Role.get_named_role(r), None) for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC } + # Un super-admin peut nommer d'autres super-admin: + editable_roles_set |= {(Role.get_named_role("SuperAdmin"), None)} # if not edit: submitlabel = "Créer utilisateur" @@ -251,16 +253,23 @@ def create_user_form(user_name=None, edit=0, all_roles=1): orig_roles_strings = {r.name + "_" + (dept or "") for (r, dept) in orig_roles} # add existing user roles displayed_roles = list(editable_roles_set.union(orig_roles)) - displayed_roles.sort(key=lambda x: (x[1] or "", x[0].name or "")) + displayed_roles.sort( + key=lambda x: ( + x[1] or "", + (x[0].name or "") if x[0].name != "SuperAdmin" else "A", + ) + ) displayed_roles_strings = [ r.name + "_" + (dept or "") for (r, dept) in displayed_roles ] - displayed_roles_labels = [f"{dept}: {r.name}" for (r, dept) in displayed_roles] + displayed_roles_labels = [ + f"{dept or 'tout dépt.'}: {r.name}" for (r, dept) in displayed_roles + ] disabled_roles = {} # pour désactiver les roles que l'on ne peut pas éditer for i in range(len(displayed_roles_strings)): if displayed_roles_strings[i] not in editable_roles_strings: disabled_roles[i] = True - + breakpoint() descr = [ ("edit", {"input_type": "hidden", "default": edit}), ("nom", {"title": "Nom", "size": 20, "allow_null": False}),