diff --git a/app/__init__.py b/app/__init__.py index d6e0be82f..976908844 100755 --- a/app/__init__.py +++ b/app/__init__.py @@ -28,6 +28,9 @@ from flask_migrate import Migrate from flask_sqlalchemy import SQLAlchemy from jinja2 import select_autoescape +import numpy as np +import psycopg2 +from psycopg2.extensions import AsIs as psycopg2_AsIs import sqlalchemy as sa import werkzeug.debug from wtforms.fields import HiddenField @@ -68,6 +71,19 @@ cache = Cache( ) +# NumPy & Psycopg2 (necessary with Numpy 2.0) +# probablement à changer quand on passera à psycopg3.2 +def adapt_numpy_scalar(numpy_scalar): + """Adapt numeric types for psycopg2""" + return psycopg2_AsIs(numpy_scalar if not np.isnan(numpy_scalar) else "'NaN'") + + +psycopg2.extensions.register_adapter(np.float32, adapt_numpy_scalar) +psycopg2.extensions.register_adapter(np.float64, adapt_numpy_scalar) +psycopg2.extensions.register_adapter(np.int32, adapt_numpy_scalar) +psycopg2.extensions.register_adapter(np.int64, adapt_numpy_scalar) + + def handle_sco_value_error(exc): "page d'erreur avec message" return render_template("sco_value_error.j2", exc=exc), 404 diff --git a/app/api/__init__.py b/app/api/__init__.py index 8a2054249..a098d7e93 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -7,7 +7,7 @@ from flask_json import as_json from flask import Blueprint from flask import current_app, g, request from flask_login import current_user -from app import db +from app import db, log from app.decorators import permission_required from app.scodoc import sco_utils as scu from app.scodoc.sco_exceptions import AccessDenied, ScoException @@ -47,6 +47,7 @@ def api_permission_required(permission): @api_bp.errorhandler(404) def api_error_handler(e): "erreurs API => json" + log(f"api_error_handler: {e}") return scu.json_error(404, message=str(e)) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 7dc8ccf17..7834c4cb1 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -41,6 +41,10 @@ from app.models import ( from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json from app.scodoc import sco_edt_cal +from app.scodoc.sco_formsemestre_inscriptions import ( + do_formsemestre_inscription_with_modules, + do_formsemestre_desinscription, +) from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import ModuleType @@ -64,10 +68,7 @@ def formsemestre_get(formsemestre_id: int): ------- /formsemestre/1 """ - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) return formsemestre.to_dict_api() @@ -400,12 +401,7 @@ def bulletins(formsemestre_id: int, version: str = "long"): ------- /formsemestre/1/bulletins """ - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first() - if formsemestre is None: - return json_error(404, "formsemestre non trouve") + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) data = [] @@ -432,10 +428,7 @@ def formsemestre_programme(formsemestre_id: int): ------- /formsemestre/1/programme """ - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) ues = formsemestre.get_ues() m_list = { ModuleType.RESSOURCE: [], @@ -508,10 +501,7 @@ def formsemestre_etudiants( /formsemestre/1/etudiants/query; """ - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) if with_query: etat = request.args.get("etat") if etat is not None: @@ -543,6 +533,63 @@ def formsemestre_etudiants( return sorted(etuds, key=itemgetter("sort_key")) +@bp.post("/formsemestre//etudid//inscrit") +@api_web_bp.post("/formsemestre//etudid//inscrit") +@login_required +@scodoc +@permission_required(Permission.EtudInscrit) +@as_json +def formsemestre_etud_inscrit(formsemestre_id: int, etudid: int): + """Inscrit l'étudiant à ce formsemestre et TOUS ses modules STANDARDS + (donc sauf les modules bonus sport). + + DATA + ---- + ```json + { + "dept_id" : int, # le département + "etape" : string, # optionnel: l'étape Apogée d'inscription + "group_ids" : [int], # optionnel: liste des groupes où inscrire l'étudiant (doivent exister) + } + ``` + """ + data = request.get_json(force=True) if request.data else {} + dept_id = data.get("dept_id", g.scodoc_dept_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id) + app.set_sco_dept(formsemestre.departement.acronym) + etud = Identite.get_etud(etudid) + + group_ids = data.get("group_ids", []) + etape = data.get("etape", None) + do_formsemestre_inscription_with_modules( + formsemestre.id, etud.id, dept_id=dept_id, etape=etape, group_ids=group_ids + ) + app.log(f"formsemestre_etud_inscrit: {etud} inscrit à {formsemestre}") + return ( + FormSemestreInscription.query.filter_by( + formsemestre_id=formsemestre.id, etudid=etud.id + ) + .first() + .to_dict() + ) + + +@bp.post("/formsemestre//etudid//desinscrit") +@api_web_bp.post("/formsemestre//etudid//desinscrit") +@login_required +@scodoc +@permission_required(Permission.EtudInscrit) +@as_json +def formsemestre_etud_desinscrit(formsemestre_id: int, etudid: int): + """Désinscrit l'étudiant de ce formsemestre et TOUS ses modules""" + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + app.set_sco_dept(formsemestre.departement.acronym) + etud = Identite.get_etud(etudid) + do_formsemestre_desinscription(etud.id, formsemestre.id) + app.log(f"formsemestre_etud_desinscrit: {etud} désinscrit de {formsemestre}") + return {"status": "ok"} + + @bp.route("/formsemestre//etat_evals") @api_web_bp.route("/formsemestre//etat_evals") @login_required @@ -649,10 +696,7 @@ def formsemestre_resultat(formsemestre_id: int): return json_error(API_CLIENT_ERROR, "invalid format specification") convert_values = format_spec != "raw" - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) # Ajoute le groupe de chaque partition, @@ -690,10 +734,7 @@ def formsemestre_resultat(formsemestre_id: int): @as_json def groups_get_auto_assignment(formsemestre_id: int): """Rend les données stockées par `groups_save_auto_assignment`.""" - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) response = make_response(formsemestre.groups_auto_assignment_data or b"") response.headers["Content-Type"] = scu.JSON_MIMETYPE return response @@ -713,11 +754,7 @@ def groups_save_auto_assignment(formsemestre_id: int): """Enregistre les données, associées à ce formsemestre. Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs. """ - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) - + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) if not formsemestre.can_change_groups(): return json_error(403, "non autorisé (can_change_groups)") @@ -726,6 +763,7 @@ def groups_save_auto_assignment(formsemestre_id: int): formsemestre.groups_auto_assignment_data = request.data db.session.add(formsemestre) db.session.commit() + return {"status": "ok"} @bp.route("/formsemestre//edt") @@ -746,10 +784,7 @@ def formsemestre_edt(formsemestre_id: int): group_ids : string (optionnel) filtre sur les groupes ScoDoc. show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code. """ - query = FormSemestre.query.filter_by(id=formsemestre_id) - if g.scodoc_dept: - query = query.filter_by(dept_id=g.scodoc_dept_id) - formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + formsemestre = FormSemestre.get_formsemestre(formsemestre_id) group_ids = request.args.getlist("group_ids", int) show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False)) return sco_edt_cal.formsemestre_edt_dict( diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 4a7f3649b..e5bdab494 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -195,7 +195,7 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): """ # Récupération du département et des étudiants du département - dept: Departement = Departement.query.get(dept_id) + dept: Departement = db.session.get(Departement, dept_id) if dept is None: return json_error(404, "Assiduité non existante") etuds: list[int] = [etud.id for etud in dept.etudiants] @@ -765,7 +765,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None): current_user.has_permission(Permission.AbsJustifView) or justificatif_unique.user_id == current_user.id ): - return json_error(401, "non autorisé à voir ce fichier") + return json_error(403, "non autorisé à voir ce fichier") # On récupère l'archive concernée archive_name: str = justificatif_unique.fichier diff --git a/app/api/moduleimpl.py b/app/api/moduleimpl.py index b5dea29b8..0ee6b2261 100644 --- a/app/api/moduleimpl.py +++ b/app/api/moduleimpl.py @@ -16,12 +16,15 @@ from flask_json import as_json from flask_login import login_required import app +from app import db from app.api import api_bp as bp, api_web_bp from app.api import api_permission_required as permission_required from app.decorators import scodoc -from app.models import ModuleImpl -from app.scodoc import sco_liste_notes +from app.models import Identite, ModuleImpl, ModuleImplInscription +from app.scodoc import sco_cache, sco_liste_notes +from app.scodoc.sco_moduleimpl import do_moduleimpl_inscrit_etuds from app.scodoc.sco_permissions import Permission +from app.scodoc.sco_utils import json_error @bp.route("/moduleimpl/") @@ -63,6 +66,60 @@ def moduleimpl_inscriptions(moduleimpl_id: int): return [i.to_dict() for i in modimpl.inscriptions] +@bp.post("/moduleimpl//etudid//inscrit") +@api_web_bp.post("/moduleimpl//etudid//inscrit") +@login_required +@scodoc +@permission_required(Permission.ScoView) +@as_json +def moduleimpl_etud_inscrit(moduleimpl_id: int, etudid: int): + """Inscrit l'étudiant à ce moduleimpl. + + SAMPLES + ------- + /moduleimpl/1/etudid/2/inscrit + """ + modimpl = ModuleImpl.get_modimpl(moduleimpl_id) + if not modimpl.can_change_inscriptions(): + return json_error(403, "opération non autorisée") + etud = Identite.get_etud(etudid) + do_moduleimpl_inscrit_etuds(modimpl.id, modimpl.formsemestre_id, [etud.id]) + app.log(f"moduleimpl_etud_inscrit: {etud} inscrit à {modimpl}") + return ( + ModuleImplInscription.query.filter_by(moduleimpl_id=modimpl.id, etudid=etud.id) + .first() + .to_dict() + ) + + +@bp.post("/moduleimpl//etudid//desinscrit") +@api_web_bp.post("/moduleimpl//etudid//desinscrit") +@login_required +@scodoc +@permission_required(Permission.ScoView) +@as_json +def moduleimpl_etud_desinscrit(moduleimpl_id: int, etudid: int): + """Désinscrit l'étudiant de ce moduleimpl. + + SAMPLES + ------- + /moduleimpl/1/etudid/2/desinscrit + """ + modimpl = ModuleImpl.get_modimpl(moduleimpl_id) + if not modimpl.can_change_inscriptions(): + return json_error(403, "opération non autorisée") + etud = Identite.get_etud(etudid) + inscription = ModuleImplInscription.query.filter_by( + etudid=etud.id, moduleimpl_id=modimpl.id + ).first() + if inscription: + db.session.delete(inscription) + db.session.commit() + sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) + app.log(f"moduleimpl_etud_desinscrit: {etud} inscrit à {modimpl}") + return {"status": "ok"} + + @bp.route("/moduleimpl//notes") @api_web_bp.route("/moduleimpl//notes") @login_required diff --git a/app/api/partitions.py b/app/api/partitions.py index 515615015..d9ac1cef1 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -169,7 +169,7 @@ def group_set_etudiant(group_id: int, etudid: int): if not group.partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not group.partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return json_error(404, "etud non inscrit au formsemestre du groupe") @@ -202,7 +202,7 @@ def group_remove_etud(group_id: int, etudid: int): if not group.partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not group.partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") group.remove_etud(etud) @@ -232,7 +232,7 @@ def partition_remove_etud(partition_id: int, etudid: int): if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") db.session.execute( sa.text( """DELETE FROM group_membership @@ -289,7 +289,7 @@ def group_create(partition_id: int): # partition-group-create if not partition.groups_editable: return json_error(403, "partition non editable") if not partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") args = request.get_json(force=True) # may raise 400 Bad Request group_name = args.get("group_name") @@ -337,7 +337,7 @@ def group_delete(group_id: int): if not group.partition.groups_editable: return json_error(403, "partition non editable") if not group.partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") formsemestre_id = group.partition.formsemestre_id log(f"deleting {group}") db.session.delete(group) @@ -378,7 +378,7 @@ def group_edit(group_id: int): if not group.partition.groups_editable: return json_error(403, "partition non editable") if not group.partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") args = request.get_json(force=True) # may raise 400 Bad Request if "group_name" in args: @@ -423,7 +423,7 @@ def group_set_edt_id(group_id: int, edt_id: str): ) group: GroupDescr = query.first_or_404() if not group.partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") log(f"group_set_edt_id( {group_id}, '{edt_id}' )") group.edt_id = edt_id db.session.add(group) @@ -461,7 +461,7 @@ def partition_create(formsemestre_id: int): if not formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") data = request.get_json(force=True) # may raise 400 Bad Request partition_name = data.get("partition_name") if partition_name is None: @@ -523,7 +523,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int): if not formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") partition_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(partition_ids, list) and not all( isinstance(x, int) for x in partition_ids @@ -569,7 +569,7 @@ def partition_order_groups(partition_id: int): if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") group_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(group_ids, list) and not all( isinstance(x, int) for x in group_ids @@ -623,7 +623,7 @@ def partition_edit(partition_id: int): if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") data = request.get_json(force=True) # may raise 400 Bad Request modified = False partition_name = data.get("partition_name") @@ -689,7 +689,7 @@ def partition_delete(partition_id: int): if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not partition.formsemestre.can_change_groups(): - return json_error(401, "opération non autorisée") + return json_error(403, "opération non autorisée") if not partition.partition_name: return json_error( API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut" diff --git a/app/auth/models.py b/app/auth/models.py index 7e9642ddf..9e0049ac7 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -445,9 +445,10 @@ class User(UserMixin, ScoDocModel): def set_roles(self, roles, dept): "set roles in the given dept" - self.user_roles = [ - UserRole(user=self, role=r, dept=dept) for r in roles if isinstance(r, Role) - ] + self.user_roles = [] + for r in roles: + if isinstance(r, Role): + self.add_role(r, dept) def get_roles(self): "iterator on my roles" diff --git a/app/but/change_refcomp.py b/app/but/change_refcomp.py index dc91d717b..4f4a8e23f 100644 --- a/app/but/change_refcomp.py +++ b/app/but/change_refcomp.py @@ -44,7 +44,9 @@ def formation_change_referentiel( ue.niveau_competence_id = niveaux_map[ue.niveau_competence_id] db.session.add(ue) if ue.parcours: - new_list = [ApcParcours.query.get(parcours_map[p.id]) for p in ue.parcours] + new_list = [ + db.session.get(ApcParcours, parcours_map[p.id]) for p in ue.parcours + ] ue.parcours.clear() ue.parcours.extend(new_list) db.session.add(ue) @@ -52,7 +54,7 @@ def formation_change_referentiel( for module in formation.modules: if module.parcours: new_list = [ - ApcParcours.query.get(parcours_map[p.id]) for p in module.parcours + db.session.get(ApcParcours, parcours_map[p.id]) for p in module.parcours ] module.parcours.clear() module.parcours.extend(new_list) @@ -76,7 +78,8 @@ def formation_change_referentiel( # FormSemestre / parcours_formsemestre for formsemestre in formation.formsemestres: new_list = [ - ApcParcours.query.get(parcours_map[p.id]) for p in formsemestre.parcours + db.session.get(ApcParcours, parcours_map[p.id]) + for p in formsemestre.parcours ] formsemestre.parcours.clear() formsemestre.parcours.extend(new_list) diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 8ccb06d6b..4f3fb5c9c 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -1557,8 +1557,8 @@ class DecisionsProposeesUE(DecisionsProposees): res: ResultatsSemestreBUT = ( self.rcue.res_pair if paire else self.rcue.res_impair ) - self.moy_ue = np.NaN - self.moy_ue_with_cap = np.NaN + self.moy_ue = np.nan + self.moy_ue_with_cap = np.nan self.ue_status = {} if self.ue.type != sco_codes.UE_STANDARD: diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py index 7f33065b1..20d8cb8bc 100644 --- a/app/comp/moy_mod.py +++ b/app/comp/moy_mod.py @@ -541,17 +541,16 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: ue_ids = [ue.id for ue in ues] evaluation_ids = [evaluation.id for evaluation in modimpl.evaluations] evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) - if ( - modimpl.module.module_type == ModuleType.RESSOURCE - or modimpl.module.module_type == ModuleType.SAE - ): + if modimpl.module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE): for ue_poids in EvaluationUEPoids.query.join( EvaluationUEPoids.evaluation ).filter_by(moduleimpl_id=moduleimpl_id): - try: - evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids - except KeyError: - pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre... + if ( + ue_poids.evaluation_id in evals_poids.index + and ue_poids.ue_id in evals_poids.columns + ): + evals_poids.at[ue_poids.evaluation_id, ue_poids.ue_id] = ue_poids.poids + # ignore poids vers des UEs qui n'existent plus ou sont dans un autre semestre... # Initialise poids non enregistrés: default_poids = ( @@ -564,7 +563,7 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]: if np.isnan(evals_poids.values.flat).any(): ue_coefs = modimpl.module.get_ue_coef_dict() for ue in ues: - evals_poids[ue.id][evals_poids[ue.id].isna()] = ( + evals_poids.loc[evals_poids[ue.id].isna(), ue.id] = ( 1 if ue_coefs.get(ue.id, default_poids) > 0 else 0 ) diff --git a/app/comp/moy_sem.py b/app/comp/moy_sem.py index 20d9752ce..540b78668 100644 --- a/app/comp/moy_sem.py +++ b/app/comp/moy_sem.py @@ -82,7 +82,7 @@ def compute_sem_moys_apc_using_ects( moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / ects.sum(axis=1) except ZeroDivisionError: # peut arriver si aucun module... on ignore - moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index) + moy_gen = pd.Series(np.nan, index=etud_moy_ue_df.index) except TypeError: if None in ects: formation = db.session.get(Formation, formation_id) @@ -93,7 +93,7 @@ def compute_sem_moys_apc_using_ects( scodoc_dept=g.scodoc_dept, formation_id=formation_id)}">{formation.get_titre_version()})""" ) ) - moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index) + moy_gen = pd.Series(np.nan, index=etud_moy_ue_df.index) else: raise return moy_gen diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 24fdbd468..0a64928bb 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -92,7 +92,7 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data for mod_coef in query: if mod_coef.module_id in module_coefs_df: - module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef + module_coefs_df.at[mod_coef.ue_id, mod_coef.module_id] = mod_coef.coef # silently ignore coefs associated to other modules (ie when module_type is changed) # Initialisation des poids non fixés: @@ -138,14 +138,16 @@ def df_load_modimpl_coefs( ) for mod_coef in mod_coefs: - try: - modimpl_coefs_df[mod2impl[mod_coef.module_id]][ - mod_coef.ue_id - ] = mod_coef.coef - except IndexError: + if ( + mod_coef.ue_id in modimpl_coefs_df.index + and mod2impl[mod_coef.module_id] in modimpl_coefs_df.columns + ): + modimpl_coefs_df.at[mod_coef.ue_id, mod2impl[mod_coef.module_id]] = ( + mod_coef.coef + ) # il peut y avoir en base des coefs sur des modules ou UE - # qui ont depuis été retirés de la formation - pass + # qui ont depuis été retirés de la formation : on ignore ces coefs + # Initialisation des poids non fixés: # 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse # sur toutes les UE) @@ -178,7 +180,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray: except ValueError: app.critical_error( f"""notes_sem_assemble_cube: shapes { - ", ".join([x.shape for x in modimpls_notes_arr])}""" + ", ".join([str(x.shape) for x in modimpls_notes_arr])}""" ) return modimpls_notes.swapaxes(0, 1) @@ -299,7 +301,11 @@ def compute_ue_moys_apc( ) # Les "dispenses" sont très peu nombreuses et traitées en python: for dispense_ue in dispense_ues: - etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0 + if ( + dispense_ue[0] in etud_moy_ue_df.columns + and dispense_ue[1] in etud_moy_ue_df.index + ): + etud_moy_ue_df.at[dispense_ue[1], dispense_ue[0]] = 0.0 return etud_moy_ue_df diff --git a/app/forms/main/config_logos.py b/app/forms/main/config_logos.py index fcea427cc..6529907d8 100644 --- a/app/forms/main/config_logos.py +++ b/app/forms/main/config_logos.py @@ -47,9 +47,9 @@ from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_logos import find_logo -JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [] +JAVASCRIPTS = html_sco_header.BOOTSTRAP_JS + [] -CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS +CSSSTYLES = html_sco_header.BOOTSTRAP_CSS # class ItemForm(FlaskForm): # """Unused Generic class to document common behavior for classes diff --git a/app/forms/multiselect.py b/app/forms/multiselect.py new file mode 100644 index 000000000..77b13626b --- /dev/null +++ b/app/forms/multiselect.py @@ -0,0 +1,118 @@ +""" +Simplification des multiselect HTML/JS +""" + + +class MultiSelect: + """ + Classe pour faciliter l'utilisation du multi-select HTML/JS + + Les values sont représentées en dict { + value: "...", + label:"...", + selected: True/False (default to False), + single: True/False (default to False) + } + + Args: + values (dict[str, list[dict]]): Dictionnaire des valeurs + génère des pour chaque clef du dictionnaire + génère des