From 58a75080433630dd93ce72a9a3a9c8bac29c26c4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet <emmanuel.viennet@gmail.com> Date: Wed, 17 Nov 2021 10:28:51 +0100 Subject: [PATCH] WIP: PN BUT --- README.md | 14 +- app/comp/moy_mod.py | 75 ++++++ app/comp/moy_ue.py | 13 +- app/models/__init__.py | 1 - app/models/formations.py | 7 + app/models/formsemestre.py | 24 +- app/scodoc/sco_edit_apc.py | 122 ++++++++++ app/scodoc/sco_edit_module.py | 235 ++++++++++-------- app/scodoc/sco_edit_ue.py | 252 ++++++++++++-------- app/scodoc/sco_evaluation_edit.py | 8 +- app/static/css/scodoc.css | 44 +++- app/static/icons/scologo_img.png | Bin 29443 -> 8005 bytes app/templates/pn/form_mods.html | 82 +++++++ app/templates/pn/form_modules_ue_coefs.html | 28 ++- app/templates/pn/form_ues.html | 49 ++++ app/views/notes.py | 1 + app/views/pn_modules.py | 24 +- scodoc.py | 2 +- tests/unit/sco_fake_gen.py | 1 + tests/unit/test_but_modules.py | 88 ++++++- 20 files changed, 838 insertions(+), 232 deletions(-) create mode 100644 app/comp/moy_mod.py create mode 100644 app/scodoc/sco_edit_apc.py create mode 100644 app/templates/pn/form_mods.html create mode 100644 app/templates/pn/form_ues.html diff --git a/README.md b/README.md index b80ecd5b5..4b3eda5fd 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,17 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`. ### Tests unitaires +Les tests unitaires utilisent normalement la base postgresql `SCODOC_TEST`. +Avant le premier lancement, créer cette base ainsi: + + ./tools/create_database.sh SCODOC_TEST + export FLASK_ENV=test + flask db upgrade + +Cette commande n'est nécessaire que la première fois (le contenu de la base +est effacé au début de chaque test, mais son schéma reste) et aussi si des +migrations (changements de schéma) ont eu lieu dans le code. + Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les scripts de tests: Lancer au préalable: @@ -109,7 +120,8 @@ On peut aussi utiliser les tests unitaires pour mettre la base de données de développement dans un état connu, par exemple pour éviter de recréer à la main étudiants et semestres quand on développe. -Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests: +Il suffit de positionner une variable d'environnement indiquant la BD +utilisée par les tests: export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV diff --git a/app/comp/moy_mod.py b/app/comp/moy_mod.py new file mode 100644 index 000000000..d819beda5 --- /dev/null +++ b/app/comp/moy_mod.py @@ -0,0 +1,75 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ) + +Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une +évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la +moyenne générale d'une UE. +""" +import numpy as np +import pandas as pd + +from app import db +from app import models + + +def df_load_evaluations_poids(moduleimpl_id: int, default_poids=1.0) -> pd.DataFrame: + """Charge poids des évaluations d'un module et retourne un dataframe + rows = evaluations, columns = UE, value = poids (float). + Les valeurs manquantes (évaluations sans coef vers des UE) sont + remplies par default_poids. + """ + modimpl = models.ModuleImpl.query.get(moduleimpl_id) + evaluations = models.Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all() + ues = modimpl.formsemestre.query_ues().all() + ue_ids = [ue.id for ue in ues] + evaluation_ids = [evaluation.id for evaluation in evaluations] + df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float) + for eval_poids in models.EvaluationUEPoids.query.join( + models.EvaluationUEPoids.evaluation + ).filter_by(moduleimpl_id=moduleimpl_id): + df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids + if default_poids is not None: + df.fillna(value=default_poids, inplace=True) + return df + + +def check_moduleimpl_conformity( + moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame +) -> bool: + """Vérifie que les évaluations de ce moduleimpl sont bien conformes + au PN. + Un module est dit *conforme* si et seulement si la somme des poids de ses + évaluations vers une UE de coefficient non nul est non nulle. + """ + module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 + check = all( + (modules_coefficients[moduleimpl.module.id].to_numpy() != 0) + == module_evals_poids + ) + return check diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index b2eb29e06..6a0ca770a 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -34,21 +34,22 @@ from app import db from app import models -def df_load_ue_coefs(formation_id): +def df_load_ue_coefs(formation_id: int, semestre_idx: int) -> pd.DataFrame: """Load coefs of all modules in formation and returns a DataFrame - rows = modules, columns = UE, value = coef. + rows = UEs, columns = modules, value = coef. + On considère toutes les UE et modules du semestre. Unspecified coefs (not defined in db) are set to zero. """ - ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() - modules = models.Module.query.filter_by(formation_id=formation_id).all() + ues = models.UniteEns.query.filter_by(formation_id=formation_id) + modules = models.Module.query.filter_by(formation_id=formation_id) ue_ids = [ue.id for ue in ues] module_ids = [module.id for module in modules] - df = pd.DataFrame(columns=ue_ids, index=module_ids, dtype=float) + df = pd.DataFrame(columns=module_ids, index=ue_ids, dtype=float) for mod_coef in ( db.session.query(models.ModuleUECoef) .filter(models.UniteEns.formation_id == formation_id) .filter(models.ModuleUECoef.ue_id == models.UniteEns.id) ): - df[mod_coef.ue_id][mod_coef.module_id] = mod_coef.coef + df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef df.fillna(value=0, inplace=True) return df diff --git a/app/models/__init__.py b/app/models/__init__.py index 6f844602c..b07f12da6 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -37,7 +37,6 @@ from app.models.formations import ( Module, ModuleUECoef, NotesTag, - notes_modules_tags, ) from app.models.formsemestre import ( FormSemestre, diff --git a/app/models/formations.py b/app/models/formations.py index db154e500..38309a5b8 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -36,6 +36,7 @@ class Formation(db.Model): ues = db.relationship("UniteEns", backref="formation", lazy="dynamic") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") ues = db.relationship("UniteEns", lazy="dynamic", backref="formation") + modules = db.relationship("Module", lazy="dynamic", backref="formation") def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" @@ -133,6 +134,12 @@ class Module(db.Model): # Relations: modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True) + tags = db.relationship( + "NotesTag", + secondary="notes_modules_tags", + lazy=True, + backref=db.backref("modules", lazy=True), + ) def __init__(self, **kwargs): self.ue_coefs = [] diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 1207d6bfa..eef37102c 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -4,6 +4,8 @@ """ from typing import Any +import flask_sqlalchemy + from app import db from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN @@ -12,6 +14,8 @@ from app.models import UniteEns import app.scodoc.notesdb as ndb from app.scodoc import sco_evaluation_db +from app.models.formations import UniteEns, Module +from app.models.moduleimpls import ModuleImpl class FormSemestre(db.Model): @@ -87,8 +91,24 @@ class FormSemestre(db.Model): if self.modalite is None: self.modalite = FormationModalite.DEFAULT_MODALITE - def get_ues(self): - "UE des modules de ce semestre" + def query_ues(self) -> flask_sqlalchemy.BaseQuery: + """UE des modules de ce semestre. + - Formations classiques: les UEs auxquelles appartiennent + les modules mis en place dans ce semestre. + - Formations APC / BUT: les UEs de la formation qui ont + le même numéro de semestre que ce formsemestre. + """ + if self.formation.get_parcours().APC_SAE: + sem_ues = UniteEns.query.filter_by( + formation=self.formation, semestre_idx=self.semestre_id + ) + else: + sem_ues = db.session.query(UniteEns).filter( + ModuleImpl.formsemestre_id == self.id, + Module.id == ModuleImpl.module_id, + UniteEns.id == Module.ue_id, + ) + return sem_ues # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py new file mode 100644 index 000000000..2652c4f91 --- /dev/null +++ b/app/scodoc/sco_edit_apc.py @@ -0,0 +1,122 @@ +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""Édition formation APC (BUT) +""" +import flask +from flask import url_for, render_template +from flask import g, request +from flask_login import current_user +from app.models.formations import Formation, UniteEns, Matiere, Module + +import app.scodoc.notesdb as ndb +import app.scodoc.sco_utils as scu +from app.scodoc import sco_groups +from app.scodoc.sco_utils import ModuleType + + +def html_edit_formation_apc( + formation, + editable=True, + tag_editable=True, +): + """Formulaire html pour visualisation ou édition d'une formation APC. + - Les UEs + - Les ressources + - Les SAÉs + """ + parcours = formation.get_parcours() + assert parcours.APC_SAE + ressources = formation.modules.filter_by(module_type=ModuleType.RESSOURCE) + saes = formation.modules.filter_by(module_type=ModuleType.SAE) + other_modules = formation.modules.filter( + Module.module_type != ModuleType.SAE + and Module.module_type != ModuleType.RESSOURCE + ) + arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() + delete_icon = scu.icontag( + "delete_small_img", title="Supprimer (module inutilisé)", alt="supprimer" + ) + delete_disabled_icon = scu.icontag( + "delete_small_dis_img", title="Suppression impossible (module utilisé)" + ) + H = [ + render_template( + "pn/form_ues.html", + formation=formation, + editable=editable, + arrow_up=arrow_up, + arrow_down=arrow_down, + arrow_none=arrow_none, + delete_icon=delete_icon, + delete_disabled_icon=delete_disabled_icon, + ), + render_template( + "pn/form_mods.html", + formation=formation, + titre="Ressources", + create_element_msg="créer une nouvelle ressource", + modules=ressources, + module_type=ModuleType.RESSOURCE, + editable=editable, + arrow_up=arrow_up, + arrow_down=arrow_down, + arrow_none=arrow_none, + delete_icon=delete_icon, + delete_disabled_icon=delete_disabled_icon, + scu=scu, + ), + render_template( + "pn/form_mods.html", + formation=formation, + titre="Situations d'Apprentissage et d'Évaluation (SAÉs)", + create_element_msg="créer une nouvelle SAÉ", + modules=saes, + module_type=ModuleType.SAE, + editable=editable, + arrow_up=arrow_up, + arrow_down=arrow_down, + arrow_none=arrow_none, + delete_icon=delete_icon, + delete_disabled_icon=delete_disabled_icon, + scu=scu, + ), + render_template( + "pn/form_mods.html", + formation=formation, + titre="Autres modules (non BUT)", + create_element_msg="créer un nouveau module", + modules=other_modules, + module_type=ModuleType.STANDARD, + editable=editable, + arrow_up=arrow_up, + arrow_down=arrow_down, + arrow_none=arrow_none, + delete_icon=delete_icon, + delete_disabled_icon=delete_disabled_icon, + scu=scu, + ), + ] + + return "\n".join(H) diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 979ed3af7..b61854d28 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -32,6 +32,7 @@ import flask from flask import url_for, render_template from flask import g, request from flask_login import current_user +from app.models.formations import Matiere, UniteEns import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -106,80 +107,99 @@ def do_module_create(args) -> int: return r -def module_create(matiere_id=None): +def module_create(matiere_id=None, module_type=None, semestre_id=None): """Création d'un module""" from app.scodoc import sco_formations from app.scodoc import sco_edit_ue - if matiere_id is None: + matiere = Matiere.query.get(matiere_id) + if matiere is None: raise ScoValueError("invalid matiere !") - matiere = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0] - UE = sco_edit_ue.ue_list(args={"ue_id": matiere["ue_id"]})[0] - formation = sco_formations.formation_list( - args={"formation_id": UE["formation_id"]} - )[0] - parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) + ue = matiere.ue + parcours = ue.formation.get_parcours() is_apc = parcours.APC_SAE - semestres_indices = list(range(1, parcours.NB_SEM + 1)) + if is_apc and module_type is not None: + object_name = scu.MODULE_TYPE_NAMES[module_type] + else: + object_name = "Module" H = [ - html_sco_header.sco_header(page_title="Création d'un module"), - """<h2>Création d'un module dans la matière %(titre)s""" % matiere, - """ (UE %(acronyme)s)</h2>""" % UE, - render_template("scodoc/help/modules.html", is_apc=is_apc), + html_sco_header.sco_header(page_title=f"Création {object_name}"), + f"""<h2>Création {object_name} dans la matière {matiere.titre}, + (UE {ue.acronyme})</h2> + """, + render_template( + "scodoc/help/modules.html", + is_apc=is_apc, + ue=ue, + semestre_id=semestre_id, + ), ] # cherche le numero adéquat (pour placer le module en fin de liste) - modules = module_list(args={"matiere_id": matiere_id}) + modules = Matiere.query.get(1).modules.all() if modules: - default_num = max([m["numero"] for m in modules]) + 10 + default_num = max([m.numero for m in modules]) + 10 else: default_num = 10 - tf = TrivialFormulator( - request.base_url, - scu.get_request_args(), + descr = [ ( + "code", + { + "size": 10, + "explanation": "code du module (doit être unique dans la formation)", + "allow_null": False, + "validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity( + val, field, formation_id + ), + }, + ), + ("titre", {"size": 30, "explanation": "nom du module"}), + ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), + ( + "module_type", + { + "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], + }, + ), + ( + "heures_cours", + {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"}, + ), + ( + "heures_td", + { + "size": 4, + "type": "float", + "explanation": "nombre d'heures de Travaux Dirigés", + }, + ), + ( + "heures_tp", + { + "size": 4, + "type": "float", + "explanation": "nombre d'heures de Travaux Pratiques", + }, + ), + ] + if is_apc: + descr += [ ( - "code", - { - "size": 10, - "explanation": "code du module (doit être unique dans la formation)", - "allow_null": False, - "validator": lambda val, field, formation_id=formation[ - "formation_id" - ]: check_module_code_unicity(val, field, formation_id), - }, - ), - ("titre", {"size": 30, "explanation": "nom du module"}), - ("abbrev", {"size": 20, "explanation": "nom abrégé (pour bulletins)"}), - ( - "module_type", - { - "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], - }, - ), - ( - "heures_cours", - {"size": 4, "type": "float", "explanation": "nombre d'heures de cours"}, - ), - ( - "heures_td", - { - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Dirigés", - }, - ), - ( - "heures_tp", + "sep_ue_coefs", { - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques", + "input_type": "separator", + "title": """ + <div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>) + </div>""", }, ), + ] + else: + semestres_indices = list(range(1, parcours.NB_SEM + 1)) + descr += [ ( "coefficient", { @@ -189,10 +209,6 @@ def module_create(matiere_id=None): "allow_null": False, }, ), - # ('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": matiere["ue_id"], "input_type": "hidden"}), - ("matiere_id", {"default": matiere["matiere_id"], "input_type": "hidden"}), ( "semestre_id", { @@ -205,24 +221,35 @@ def module_create(matiere_id=None): "allowed_values": semestres_indices, }, ), - ( - "code_apogee", - { - "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", - }, - ), - ( - "numero", - { - "size": 2, - "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", - "type": "int", - "default": default_num, - }, - ), + ] + 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"}), + ( + "code_apogee", + { + "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", + }, + ), + ( + "numero", + { + "size": 2, + "explanation": "numéro (1,2,3,4...) pour ordre d'affichage", + "type": "int", + "default": default_num, + }, ), + ] + args = scu.get_request_args() + tf = TrivialFormulator( + request.base_url, + args, + descr, submitlabel="Créer ce module", ) if tf[0] == 0: @@ -233,7 +260,7 @@ def module_create(matiere_id=None): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=UE["formation_id"], + formation_id=ue.formation_id, ) ) @@ -344,6 +371,7 @@ def module_edit(module_id=None): if not modules: raise ScoValueError("invalid module !") module = modules[0] + a_module = models.Module.query.get(module_id) unlocked = not module_is_locked(module_id) formation_id = module["formation_id"] formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] @@ -434,7 +462,6 @@ def module_edit(module_id=None): ), ] if is_apc: - a_module = models.Module.query.get(module_id) coefs_descr = a_module.ue_coefs_descr() if coefs_descr: coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr]) @@ -479,19 +506,36 @@ def module_edit(module_id=None): "enabled": unlocked, }, ), - ( - "semestre_id", - { - "input_type": "menu", - "type": "int", - "title": parcours.SESSION_NAME.capitalize(), - "explanation": "%s de début du module dans la formation standard" - % parcours.SESSION_NAME, - "labels": [str(x) for x in semestres_indices], - "allowed_values": semestres_indices, - "enabled": unlocked, - }, - ), + ] + if is_apc: + # le semestre du module est toujours celui de son UE + descr += [ + ( + "semestre_id", + { + "input_type": "hidden", + "type": "int", + "readonly": True, + }, + ) + ] + else: + descr += [ + ( + "semestre_id", + { + "input_type": "menu", + "type": "int", + "title": parcours.SESSION_NAME.capitalize(), + "explanation": "%s de début du module dans la formation standard" + % parcours.SESSION_NAME, + "labels": [str(x) for x in semestres_indices], + "allowed_values": semestres_indices, + "enabled": unlocked, + }, + ) + ] + descr += [ ( "code_apogee", { @@ -509,7 +553,8 @@ def module_edit(module_id=None): }, ), ] - + # force module semestre_idx to its UE + module["semestre_id"] = a_module.ue.semestre_idx tf = TrivialFormulator( request.base_url, scu.get_request_args(), @@ -538,7 +583,7 @@ def module_edit(module_id=None): def edit_module_set_code_apogee(id=None, value=None): "Set UE code apogee" module_id = id - value = value.strip("-_ \t") + value = str(value).strip("-_ \t") log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value)) modules = module_list(args={"module_id": module_id}) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 1a46be44a..77b89b1d0 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -45,6 +45,7 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours +from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_formation from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_module @@ -382,27 +383,32 @@ def ue_edit(ue_id=None, create=False, formation_id=None): ) -def _add_ue_semestre_id(ues): +def _add_ue_semestre_id(ues: list[dict], is_apc): """ajoute semestre_id dans les ue, en regardant - semestre_idx ou à défaut le premier module de chacune. - Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), + 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"] = sco_codes_parcours.UE_SEM_DEFAULT else: + # était le comportement ScoDoc7 modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) if modules: ue["semestre_id"] = modules[0]["semestre_id"] else: - ue["semestre_id"] = 1000000 + ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT def next_ue_numero(formation_id, semestre_id=None): """Numero d'une nouvelle UE dans cette formation. Si le semestre est specifie, cherche les UE ayant des modules de ce semestre """ + formation = Formation.query.get(formation_id) ues = ue_list(args={"formation_id": formation_id}) if not ues: return 0 @@ -410,7 +416,7 @@ def next_ue_numero(formation_id, semestre_id=None): return ues[-1]["numero"] + 1000 else: # Avec semestre: (prend le semestre du 1er module de l'UE) - _add_ue_semestre_id(ues) + _add_ue_semestre_id(ues, formation.get_parcours().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 @@ -447,27 +453,27 @@ def ue_table(formation_id=None, msg=""): # was ue_list from app.scodoc import sco_formations from app.scodoc import sco_formsemestre_validation - F = sco_formations.formation_list(args={"formation_id": formation_id}) - if not F: + formation = Formation.query.get(formation_id) + if not formation: raise ScoValueError("invalid formation_id") - F = F[0] - parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"]) + parcours = formation.get_parcours() is_apc = parcours.APC_SAE locked = sco_formations.formation_has_locked_sems(formation_id) ues = ue_list(args={"formation_id": formation_id, "is_external": False}) ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True}) # tri par semestre et numero: - _add_ue_semestre_id(ues) - _add_ue_semestre_id(ues_externes) + _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"])) has_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues) has_perm_change = current_user.has_permission(Permission.ScoChangeFormation) # editable = (not locked) and has_perm_change - # On autorise maintanant la modification des formations qui ont des semestres verrouillés, - # sauf si cela affect les notes passées (verrouillées): + # On autorise maintenant la modification des formations qui ont + # des semestres verrouillés, sauf si cela affect les notes passées + # (verrouillées): # - pas de modif des modules utilisés dans des semestres verrouillés # - pas de changement des codes d'UE utilisés dans des semestres verrouillés editable = has_perm_change @@ -496,12 +502,13 @@ def ue_table(formation_id=None, msg=""): # was ue_list "libjs/jQuery-tagEditor/jquery.caret.min.js", "js/module_tag_editor.js", ], - page_title="Programme %s" % F["acronyme"], + page_title=f"Programme {formation.acronyme}", ), - """<h2>Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s""" - % F, - lockicon, - "</h2>", + f"""<h2>Formation {formation.titre} ({formation.acronyme}) + [version {formation.version}] code {formation.formation_code} + {lockicon} + </h2> + """, ] if locked: H.append( @@ -533,41 +540,41 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); # Description de la formation H.append('<div class="formation_descr">') H.append( - '<div class="fd_d"><span class="fd_t">Titre:</span><span class="fd_v">%(titre)s</span></div>' - % F - ) - H.append( - '<div class="fd_d"><span class="fd_t">Titre officiel:</span><span class="fd_v">%(titre_officiel)s</span></div>' - % F - ) - H.append( - '<div class="fd_d"><span class="fd_t">Acronyme:</span><span class="fd_v">%(acronyme)s</span></div>' - % F - ) - H.append( - '<div class="fd_d"><span class="fd_t">Code:</span><span class="fd_v">%(formation_code)s</span></div>' - % F - ) - H.append( - '<div class="fd_d"><span class="fd_t">Version:</span><span class="fd_v">%(version)s</span></div>' - % F - ) - H.append( - '<div class="fd_d"><span class="fd_t">Type parcours:</span><span class="fd_v">%s</span></div>' - % parcours.__doc__ + f"""<div class="fd_d"><span class="fd_t">Titre: + </span><span class="fd_v">{formation.titre}</span> + </div> + <div class="fd_d"><span class="fd_t">Titre officiel:</span> + <span class="fd_v">{formation.titre_officiel}</span> + </div> + <div class="fd_d"><span class="fd_t">Acronyme:</span> + <span class="fd_v">{formation.acronyme}</span> + </div> + <div class="fd_d"><span class="fd_t">Code:</span> + <span class="fd_v">{formation.formation_code}</span> + </div> + <div class="fd_d"><span class="fd_t">Version:</span> + <span class="fd_v">{formation.version}</span> + </div> + <div class="fd_d"><span class="fd_t">Type parcours:</span> + <span class="fd_v">{parcours.__doc__}</span> + </div> + """ ) if parcours.UE_IS_MODULE: H.append( - '<div class="fd_d"><span class="fd_t"> </span><span class="fd_n">(Chaque module est une UE)</span></div>' + """<div class="fd_d"><span class="fd_t"> </span> + <span class="fd_n">(Chaque module est une UE)</span></div>""" ) if editable: H.append( - '<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>' - % F + f"""<div><a href="{ + url_for('notes.formation_edit', scodoc_dept=g.scodoc_dept, + formation_id=formation_id) + }" class="stdlink">modifier ces informations</a></div>""" ) - H.append("</div>") + # Formation APC (BUT) ? if is_apc: H.append( @@ -575,50 +582,67 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); <div class="ue_list_tit">Formation par compétences (BUT)</div> <ul> <li><a class="stdlink" href="{ - url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id) + url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=None) }">éditer les coefficients des ressources et SAÉs</a></li> </ul> </div>""" ) # Description des UE/matières/modules - H.append('<div class="formation_ue_list">') - H.append('<div class="ue_list_tit">Programme pédagogique:</div>') - - H.append( - '<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>' - ) H.append( - _ue_table_ues( - parcours, - ues, - editable, - tag_editable, - has_perm_change, - arrow_up, - arrow_down, - arrow_none, - delete_icon, - delete_disabled_icon, - ) + """ + <div class="formation_ue_list"> + <div class="ue_list_tit">Programme pédagogique:</div> + <form> + <input type="checkbox" class="sco_tag_checkbox">montrer les tags</input> + </form> + """ ) - if editable: + if is_apc: H.append( - '<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>' - % formation_id + sco_edit_apc.html_edit_formation_apc( + formation, editable=editable, tag_editable=tag_editable + ) ) + else: H.append( - '<li><a href="formation_add_malus_modules?formation_id=%(formation_id)s" class="stdlink">Ajouter des modules de malus dans chaque UE</a></li></ul>' - % F + _ue_table_ues( + parcours, + ues, + editable, + tag_editable, + has_perm_change, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, + ) ) + if editable: + H.append( + f"""<ul> + <li><a class="stdlink" href="{ + url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, formation_id=formation_id) + }">Ajouter une UE</a> + </li> + <li><a href="{ + url_for('notes.formation_add_malus_modules', + scodoc_dept=g.scodoc_dept, formation_id=formation_id) + }" class="stdlink">Ajouter des modules de malus dans chaque UE</a> + </li> + </ul> + """ + ) + H.append("</div>") # formation_ue_list if ues_externes: - H.append('<div class="formation_ue_list formation_ue_list_externes">') H.append( - '<div class="ue_list_tit">UE externes déclarées (pour information):</div>' - ) - H.append( - _ue_table_ues( + f""" + <div class="formation_ue_list formation_ue_list_externes"> + <div class="ue_list_tit">UE externes déclarées (pour information): + </div> + {_ue_table_ues( parcours, ues_externes, editable, @@ -629,30 +653,49 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); arrow_none, delete_icon, delete_disabled_icon, - ) + )} + </div> + """ ) - H.append("</div>") # formation_ue_list - H.append("<p><ul>") if editable: H.append( - """ -<li><a class="stdlink" href="formation_create_new_version?formation_id=%(formation_id)s">Créer une nouvelle version (non verrouillée)</a></li> -""" - % F + f""" + <li><a class="stdlink" href="{ + url_for('notes.formation_create_new_version', + scodoc_dept=g.scodoc_dept, formation_id=formation_id + ) + }">Créer une nouvelle version (non verrouillée)</a> + </li> + """ ) H.append( - """ -<li><a class="stdlink" href="formation_table_recap?formation_id=%(formation_id)s">Table récapitulative de la formation</a></li> - -<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</li> + f""" + <li><a class="stdlink" href="{ + url_for('notes.formation_table_recap', scodoc_dept=g.scodoc_dept, + formation_id=formation_id) + }">Table récapitulative de la formation</a> + </li> + <li><a class="stdlink" href="{ + url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, + formation_id=formation_id, format='xml') + }">Export XML de la formation</a> + (permet de la sauvegarder pour l'échanger avec un autre site) + </li> -<li><a class="stdlink" href="formation_export?formation_id=%(formation_id)s&format=json">Export JSON de la formation</a></li> + <li><a class="stdlink" href="{ + url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, + formation_id=formation_id, format='json') + }">Export JSON de la formation</a> + </li> -<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li> -</ul> -</p>""" - % F + <li><a class="stdlink" href="{ + url_for('notes.module_table', scodoc_dept=g.scodoc_dept, + formation_id=formation_id) + }">Liste détaillée des modules de la formation</a> (debug) + </li> + </ul> + </p>""" ) if has_perm_change: H.append( @@ -679,12 +722,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); if current_user.has_permission(Permission.ScoImplement): H.append( - """<ul> - <li><a class="stdlink" href="formsemestre_createwithmodules?formation_id=%(formation_id)s&semestre_id=1">Mettre en place un nouveau semestre de formation %(acronyme)s</a> - </li> - -</ul>""" - % F + f"""<ul> + <li><a class="stdlink" href="{ + url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept, + formation_id=formation_id, semestre_id=1) + }">Mettre en place un nouveau semestre de formation %(acronyme)s</a> + </li> + </ul>""" ) # <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li> @@ -707,7 +751,9 @@ def _ue_table_ues( delete_icon, delete_disabled_icon, ): - """Édition de programme: liste des UEs (avec leurs matières et modules).""" + """Édition de programme: liste des UEs (avec leurs matières et modules). + Pour les formations classiques (non APC/BUT) + """ H = [] cur_ue_semestre_id = None iue = 0 @@ -802,6 +848,7 @@ def _ue_table_ues( arrow_none, delete_icon, delete_disabled_icon, + module_type=module_type, ) ) return "\n".join(H) @@ -817,6 +864,7 @@ def _ue_table_matieres( arrow_none, delete_icon, delete_disabled_icon, + module_type=None, ): """Édition de programme: liste des matières (et leurs modules) d'une UE.""" H = [] @@ -883,6 +931,7 @@ def _ue_table_ressources_saes( arrow_none, delete_icon, delete_disabled_icon, + module_type=None, ): """Édition de programme: liste des ressources et SAÉs d'une UE. (pour les parcours APC_SAE) @@ -905,7 +954,7 @@ def _ue_table_ressources_saes( """ ] modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) - for titre, element_name, module_type in ( + for titre, element_name, element_type in ( ("Ressources", "ressource", scu.ModuleType.RESSOURCE), ("SAÉs", "SAÉ", scu.ModuleType.SAE), ("Autres modules", "xxx", None), @@ -914,9 +963,9 @@ def _ue_table_ressources_saes( elements = [ m for m in modules - if module_type == m["module_type"] + if element_type == m["module_type"] or ( - (module_type is None) + (element_type is None) and m["module_type"] not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE) ) @@ -933,6 +982,7 @@ def _ue_table_ressources_saes( arrow_none, delete_icon, delete_disabled_icon, + module_type=module_type, empty_list_msg="Aucune " + element_name, create_element_msg="créer une " + element_name, add_suppress_link=False, @@ -1273,5 +1323,5 @@ def ue_list_semestre_ids(ue): Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels, aussi ScoDoc laisse le choix. """ - Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) - return sorted(list(set([mod["semestre_id"] for mod in Modlist]))) + modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) + return sorted(list(set([mod["semestre_id"] for mod in modules]))) diff --git a/app/scodoc/sco_evaluation_edit.py b/app/scodoc/sco_evaluation_edit.py index a310ecdd5..36b12ca25 100644 --- a/app/scodoc/sco_evaluation_edit.py +++ b/app/scodoc/sco_evaluation_edit.py @@ -39,6 +39,7 @@ from flask import request from app import db from app import log from app import models +from app.models.formsemestre import FormSemestre import app.scodoc.sco_utils as scu from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_exceptions import AccessDenied, ScoValueError @@ -64,11 +65,8 @@ def evaluation_create_form( modimpl = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0] mod = modimpl["module"] formsemestre_id = modimpl["formsemestre_id"] - sem_ues = db.session.query(models.UniteEns).filter( - models.ModuleImpl.formsemestre_id == formsemestre_id, - models.Module.id == models.ModuleImpl.module_id, - models.UniteEns.id == models.Module.ue_id, - ) + sem = FormSemestre.query.get(formsemestre_id) + sem_ues = sem.query_ues().all() is_malus = mod["module_type"] == ModuleType.MALUS is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE) diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index e091b90a8..e9ae3f073 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -263,7 +263,8 @@ div.logo-logo { } div.logo-logo img { margin-top: 20px; - width: 100px; + width: 55px; /* 100px */ + padding-right: 50px; } div.sidebar-bottom { margin-top: 10px; @@ -1480,7 +1481,40 @@ div.formation_ue_list { margin-right: 12px; padding-left: 5px; } +div.formation_list_ues_titre { + padding-left: 24px; + padding-right: 24px; + font-size: 120%; +} +div.formation_list_modules { + border-radius: 18px; + margin-left: 10px; + margin-right: 10px; + margin-bottom: 10px; + padding-bottom: 1px; +} +div.formation_list_modules_titre { + padding-left: 24px; + padding-right: 24px; + font-weight: bold; + font-size: 120%; +} +div.formation_list_modules_RESSOURCE { + background-color: #f8c844; +} +div.formation_list_modules_SAE { + background-color: #c6ffab; +} +div.formation_list_modules_STANDARD { + background-color: #afafc2; +} +div.formation_list_modules ul.notes_module_list { + margin-top: 0px; + margin-bottom: -1px; + padding-top: 5px; + padding-bottom: 5px; +} li.module_malus span.formation_module_tit { color: red; font-weight: bold; @@ -1498,7 +1532,6 @@ div.ue_list_tit { ul.notes_ue_list { background-color: rgb(240,240,240); - font-weight: bold; margin-top: 4px; margin-right: 1em; } @@ -1519,7 +1552,10 @@ span.ue_type { margin-left: 1.5em; margin-right: 1.5em; } - +ul.notes_module_list span.ue_coefs_list { + color: blue; + font-size: 70%; +} div.formation_ue_list_externes { background-color: #98cc98; } @@ -1572,7 +1608,7 @@ div#ue_list_code { } ul.notes_module_list { - list-style-type: none; + list-style-type: none; } div#ue_list_etud_validations { diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png index b0871c44b810556e121e0ef4b2a0ffca5dbab65c..38e0da1a46650583f59431b38610acb25b5102d2 100644 GIT binary patch delta 7605 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 delta 29210 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 HoD!M<OkWYQ diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html new file mode 100644 index 000000000..830c3f672 --- /dev/null +++ b/app/templates/pn/form_mods.html @@ -0,0 +1,82 @@ +{# Édition liste modules APC (SAÉ ou ressources) #} + +<div class="formation_list_modules formation_list_modules_{{module_type.name}}"> +<div class="formation_list_modules_titre">{{titre}}</div> + +<ul class="notes_module_list"> + +{% if not formation.ues.count() %} + <li class="notes_module_list"><em>aucune UE</em></li> +{% else %} + {% for mod in modules %} + <li class="notes_module_list module_{{mod.type_name()}}"> + <span class="notes_module_list_buts"> + {% if editable and not loop.first %} + <a href="{{ url_for('notes.module_move', + scodoc_dept=g.scodoc_dept, module_id=mod.id, after=0 ) + }}" class="aud">{{arrow_up|safe}}</a> + {% else %} + {{arrow_none|safe}} + {% endif %} + {% if editable and not loop.last %} + <a href="{{ url_for('notes.module_move', + scodoc_dept=g.scodoc_dept, module_id=mod.id, after=1 ) + }}" class="aud">{{arrow_down|safe}}</a> + {% else %} + {{arrow_none|safe}} + {% endif %} + </span> + {% if editable and not mod.modimpls.count() %} + <a class="smallbutton" href="{{ url_for('notes.module_delete', + scodoc_dept=g.scodoc_dept, module_id=mod.id) + }}">{{delete_icon|safe}}</a> + {% else %} + {{delete_disabled_icon|safe}} + {% endif %} + + {% if editable %} + <a class="discretelink" title="Modifier le module {{mod.code}}, + utilisé par {{mod.modimpls.count()}} sessions" + href="{{ url_for('notes.module_edit', + scodoc_dept=g.scodoc_dept, module_id=mod.id) + }}"> + {% endif %} + <span class="formation_module_tit">{{mod.code}} {{mod.titre|default("", true)}}</span> + {% if editable %} + </a> + {% endif %} + + {{formation.get_parcours().SESSION_NAME}} {{mod.semestre_id}} + + ({{mod.heures_cours}}/{{mod.heures_td}}/{{mod.heures_tp}}, + + Apo:<span class="{% if editable %}span_apo_edit{% endif %}" + data-url="edit_module_set_code_apogee" + id="{{mod.id}}" + data-placeholder="{{scu.APO_MISSING_CODE_STR}}"> + {{mod.code_apogee|default("", true)}}</span>) + + <span class="ue_coefs_list"> + {% for coef in mod.ue_coefs %} + <span>{{coef.ue.acronyme}}:{{coef.coef}}</span> + {% endfor %} + </span> + + <span class="sco_tag_edit"><form><textarea data-module_id="{{mod.id}}" + class="{% if editable %}module_tag_editor{% else %}module_tag_editor_ro{% endif %}">{{mod.tags|join(', ', attribute='title')}}</textarea></form></span> + + </li> + {% endfor %} + + {% if editable %} + <li><a class="stdlink" href="{{ + url_for("notes.module_create", + scodoc_dept=g.scodoc_dept, + module_type=module_type|int, + )}}" + >{{create_element_msg}}</a> + </li> + {% endif %} +{% endif %} +</ul> +</div> \ No newline at end of file diff --git a/app/templates/pn/form_modules_ue_coefs.html b/app/templates/pn/form_modules_ue_coefs.html index b0116ba79..a93db230b 100644 --- a/app/templates/pn/form_modules_ue_coefs.html +++ b/app/templates/pn/form_modules_ue_coefs.html @@ -15,13 +15,35 @@ <h2>Formation {{formation.titre}} ({{formation.acronyme}}) [version {{formation.version}}] code {{formation.code}}</h2> + <form onchange="change_semestre()">Semestre: + <select name="semestre_idx" id="semestre_idx" > + {% for i in semestre_ids %} + <option value="{{i}}" {%if semestre_idx == i%}selected{%endif%}>{{i}}</option> + {% endfor %} + </select> + </form> + <div class="tableau"></div> <script> + function change_semestre() { + let semestre_idx = $("#semestre_idx")[0].value; + let url = window.location.href.replace( /\/[\-0-9]*$/, "/" + semestre_idx); + window.location.href = url; + }; + + $(function () { - $.getJSON("{{data_source}}", function (data) { - build_table(data); - }); + let semestre_idx = $("#semestre_idx")[0].value; + if (semestre_idx > -10) { + let base_url = "{{data_source}}"; + let data_url = base_url.replace( /\/[\-0-9]*$/, "/" + semestre_idx); + console.log("data_url=", data_url ); + $.getJSON(data_url, function (data) { + console.log("build_table") + build_table(data); + }); + } }); function save(obj) { var value = obj.innerText.trim(); diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html new file mode 100644 index 000000000..ac0486446 --- /dev/null +++ b/app/templates/pn/form_ues.html @@ -0,0 +1,49 @@ +{# Édition liste UEs APC #} +<div class="formation_list_ues"> + <div class="formation_list_ues_titre">Unités d'Enseignement (UEs)</div> + <ul class="notes_ue_list"> + {% if not formation.ues.count() %} + <li class="notes_ue_list"><em>aucune UE</em></li> + {% else %} + {% for ue in formation.ues %} + <li class="notes_ue_list"> + {% if editable and not loop.first %} + <a href="{{ url_for('notes.ue_move', + scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=0 ) + }}" class="aud">{{arrow_up|safe}}</a> + {% else %} + {{arrow_none|safe}} + {% endif %} + {% if editable and not loop.last %} + <a href="{{ url_for('notes.ue_move', + scodoc_dept=g.scodoc_dept, ue_id=ue.id, after=1 ) + }}" class="aud">{{arrow_down|safe}}</a> + {% else %} + {{arrow_none|safe}} + {% endif %} + </span> + {% if editable and not ue.modules.count() %} + <a class="smallbutton" href="{{ url_for('notes.ue_delete', + scodoc_dept=g.scodoc_dept, ue_id=ue.id) + }}">{{delete_icon|safe}}</a> + {% else %} + {{delete_disabled_icon|safe}} + {% endif %} + + <b>{{ue.acronyme}}</b> {{ue.titre}} + + </li> + {% endfor %} + {% endif %} + + {% if editable %} + <li><a class="stdlink" href="{{ + url_for("notes.ue_create", + scodoc_dept=g.scodoc_dept, + formation_id=formation.id, + )}}" + >ajouter une UE</a> + </li> + {% endif %} + </ul> +</div> \ No newline at end of file diff --git a/app/views/notes.py b/app/views/notes.py index ac67f0e11..1b1c97dd9 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -429,6 +429,7 @@ sco_publish( "/edit_module_set_code_apogee", sco_edit_module.edit_module_set_code_apogee, Permission.ScoChangeFormation, + methods=["GET", "POST"], ) sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py index b93852096..0eb0d46e4 100644 --- a/app/views/pn_modules.py +++ b/app/views/pn_modules.py @@ -66,16 +66,20 @@ from app.scodoc import html_sco_header from app.scodoc.sco_permissions import Permission -@bp.route("/table_modules_ue_coefs/<formation_id>") +@bp.route("/table_modules_ue_coefs/<int:formation_id>/<semestre_idx>") @scodoc @permission_required(Permission.ScoView) -def table_modules_ue_coefs(formation_id): +def table_modules_ue_coefs(formation_id, semestre_idx): """Description JSON de la table des coefs modules/UE dans une formation""" _ = models.Formation.query.get_or_404(formation_id) # check - df = moy_ue.df_load_ue_coefs(formation_id) - ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() - modules = models.Module.query.filter_by(formation_id=formation_id).all() + df = moy_ue.df_load_ue_coefs(formation_id, semestre_idx) + ues = models.UniteEns.query.filter_by( + formation_id=formation_id, semestre_idx=semestre_idx + ) + modules = models.Module.query.filter_by( + formation_id=formation_id, semestre_id=semestre_idx + ) # Titre des modules, en ligne col_titres_mods = [ { @@ -108,7 +112,7 @@ def table_modules_ue_coefs(formation_id): "x": col, "y": row, "style": "champs", - "data": df[ue.id][mod.id] or "", + "data": df[mod.id][ue.id] or "", "editable": True, "module_id": mod.id, "ue_id": ue.id, @@ -146,10 +150,11 @@ def set_module_ue_coef(): return scu.json_error("ok", success=True, status=201) -@bp.route("/edit_modules_ue_coefs/<formation_id>") +@bp.route("/edit_modules_ue_coefs/<formation_id>", defaults={"semestre_idx": -100}) +@bp.route("/edit_modules_ue_coefs/<formation_id>/<semestre_idx>") @scodoc @permission_required(Permission.ScoChangeFormation) -def edit_modules_ue_coefs(formation_id): +def edit_modules_ue_coefs(formation_id, semestre_idx=None): """Formulaire édition grille coefs EU/modules""" formation = models.Formation.query.filter_by( formation_id=formation_id @@ -161,9 +166,12 @@ def edit_modules_ue_coefs(formation_id): "notes.table_modules_ue_coefs", scodoc_dept=g.scodoc_dept, formation_id=formation_id, + semestre_idx=semestre_idx or "", ), data_save=url_for( "notes.set_module_ue_coef", scodoc_dept=g.scodoc_dept, ), + semestre_idx=int(semestre_idx), + semestre_ids=range(1, formation.get_parcours().NB_SEM + 1), ) diff --git a/scodoc.py b/scodoc.py index d15c3f7db..2a48a7943 100755 --- a/scodoc.py +++ b/scodoc.py @@ -281,7 +281,7 @@ def list_depts(depts=""): # list-dept "--name", is_flag=True, help="show database name instead of connexion string (required for " - "dropdb/createddb commands)", + "dropdb/createdb commands)", ) def scodoc_database(name): # list-dept """print the database connexion string""" diff --git a/tests/unit/sco_fake_gen.py b/tests/unit/sco_fake_gen.py index 19d6f0ecd..371cd21db 100644 --- a/tests/unit/sco_fake_gen.py +++ b/tests/unit/sco_fake_gen.py @@ -166,6 +166,7 @@ class ScoFake(object): is_external=None, code_apogee=None, coefficient=None, + semestre_idx=None, ): """Crée une UE""" if numero is None: diff --git a/tests/unit/test_but_modules.py b/tests/unit/test_but_modules.py index 74899a0ae..0edd198d0 100644 --- a/tests/unit/test_but_modules.py +++ b/tests/unit/test_but_modules.py @@ -1,10 +1,15 @@ """ Test modèles évaluations avec poids BUT """ +import numpy as np +import pandas as pd from tests.unit import sco_fake_gen from app import db from app import models +from app.comp import moy_mod +from app.comp import moy_ue +from app.scodoc import sco_codes_parcours """ mapp.set_sco_dept("RT") @@ -18,11 +23,24 @@ login_user(admin_user) def setup_formation_test(): G = sco_fake_gen.ScoFake(verbose=False) _f = G.create_formation( - acronyme="F3", titre="Formation 2", titre_officiel="Titre officiel 2" + acronyme="F3", + titre="Formation 2", + titre_officiel="Titre officiel 2", + type_parcours=sco_codes_parcours.ParcoursBUT.TYPE_PARCOURS, + ) + _ue1 = G.create_ue( + formation_id=_f["formation_id"], acronyme="UE1", titre="ue 1", semestre_idx=2 + ) + _ue2 = G.create_ue( + formation_id=_f["formation_id"], acronyme="UE2", titre="ue 2", semestre_idx=2 + ) + _ue3 = G.create_ue( + formation_id=_f["formation_id"], acronyme="UE3", titre="ue 3", semestre_idx=2 + ) + # une 4eme UE en dehors du semestre 2 + _ = G.create_ue( + formation_id=_f["formation_id"], acronyme="UE41", titre="ue 41", semestre_idx=4 ) - _ue1 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE1", titre="ue 1") - _ue2 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE2", titre="ue 2") - _ue3 = G.create_ue(formation_id=_f["formation_id"], acronyme="UE3", titre="ue 3") _mat = G.create_matiere(ue_id=_ue1["ue_id"], titre="matière test") _mod = G.create_module( matiere_id=_mat["matiere_id"], @@ -31,6 +49,7 @@ def setup_formation_test(): titre="module test", ue_id=_ue1["ue_id"], formation_id=_f["formation_id"], + semestre_id=2, ) return G, _f["id"], _ue1["id"], _ue2["id"], _ue3["id"], _mod["id"] @@ -66,7 +85,9 @@ def test_evaluation_poids(test_client): e1.set_ue_poids(ue1, p1) db.session.commit() assert e1.get_ue_poids_dict()[ue1_id] == p1 - ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() + ues = models.UniteEns.query.filter_by( + formation_id=formation_id, semestre_idx=2 + ).all() poids = [1.0, 2.0, 3.0] for (ue, p) in zip(ues, poids): e1.set_ue_poids(ue, p) @@ -109,3 +130,60 @@ def test_modules_coefs(test_client): mod.set_ue_coef(ue2, 0.0) db.session.commit() assert len(mod.ue_coefs) == 0 + + +def test_modules_conformity(test_client): + """Vérification coefficients module<->UE vs poids des évaluations""" + G, formation_id, ue1_id, ue2_id, ue3_id, module_id = setup_formation_test() + ue1 = models.UniteEns.query.get(ue1_id) + ue2 = models.UniteEns.query.get(ue2_id) + ue3 = models.UniteEns.query.get(ue3_id) + mod = models.Module.query.get(module_id) + nb_ues = 3 # 3 UEs dans ce test + nb_mods = 1 # 1 seul module + # Coef du module vers les UE + c1, c2, c3 = 1.0, 2.0, 3.0 + coefs_mod = {ue1.id: c1, ue2.id: c2, ue3.id: c3} + mod.set_ue_coef_dict(coefs_mod) + assert mod.get_ue_coef_dict() == coefs_mod + # Mise en place: + sem = G.create_formsemestre( + formation_id=formation_id, + semestre_id=2, + date_debut="01/01/2021", + date_fin="30/06/2021", + ) + mi = G.create_moduleimpl( + module_id=module_id, + formsemestre_id=sem["formsemestre_id"], + ) + moduleimpl_id = mi["id"] + modimpl = models.ModuleImpl.query.get(moduleimpl_id) + assert modimpl.formsemestre.formation.get_parcours().APC_SAE # BUT + # Check ModuleImpl + ues = modimpl.formsemestre.query_ues().all() + assert len(ues) == 3 + # + _e1 = G.create_evaluation( + moduleimpl_id=moduleimpl_id, + jour="01/01/2021", + description="evaluation 1", + coefficient=0, + ) + evaluation_id = _e1["evaluation_id"] + nb_evals = 1 # 1 seule evaluation pour l'instant + p1, p2, p3 = 1.0, 2.0, 0.0 # poids de l'éval vers les UE 1, 2 et 3 + evaluation = models.Evaluation.query.get(evaluation_id) + evaluation.set_ue_poids_dict({ue1.id: p1, ue2.id: p2}) + assert evaluation.get_ue_poids_dict() == {ue1.id: p1, ue2.id: p2} + # On n'est pas conforme car p3 est nul alors que c3 est non nul + modules_coefficients = moy_ue.df_load_ue_coefs(formation_id) + assert isinstance(modules_coefficients, pd.DataFrame) + assert modules_coefficients.shape == (nb_ues, nb_mods) + evals_poids = moy_mod.df_load_evaluations_poids(moduleimpl_id) + assert isinstance(evals_poids, pd.DataFrame) + assert all(evals_poids.dtypes == np.float64) + assert evals_poids.shape == (nb_evals, nb_ues) + assert not moy_mod.check_moduleimpl_conformity( + modimpl, evals_poids, modules_coefficients + ) -- GitLab