diff --git a/README.md b/README.md index b80ecd5b588d9166aa4209e1a7f829077f0cab61..4b3eda5fd2543e8a2d73d8e98cf9c2b19e2a2d3b 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 0000000000000000000000000000000000000000..d819beda5fdefdd9b58c9f37491fb313433b3b13 --- /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 b2eb29e062be41726cf721d1180c83b65467e5f4..6a0ca770a8172495ad8cc4959c937aafd017d086 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 6f844602c25b13f8b9ba01e750d6f1b9aaa72bac..b07f12da686b0810111b8c5463767a5c4427d885 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 db154e500c2e4f6acf88b79576ce2292445b0a0e..38309a5b892e48a935488459d6921d0c59579ae2 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 1207d6bfa3bdc24636962d338d22b74eff321e92..eef37102cd2a5d4c90c0134a90876ea4ecd2a846 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 0000000000000000000000000000000000000000..2652c4f913785d8b0338be2a89c9d5cc66819165 --- /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 979ed3af7bbe0610b646af3d58b424a3b46fbbe9..b61854d28a252d039625199df39ab9b3f5c7be0b 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 1a46be44a669457aa3f37feb850565b0006e8158..77b89b1d00bf514e0879c29be03230526be9b0ab 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 a310ecdd5f818ee8e4ddc49812a40177eeef48c5..36b12ca2525f8ddc9b30730b40ad1ab342c93679 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 e091b90a849d0554e08fd3736f7d40bb454e2490..e9ae3f0737c308e4da5dd84150484359a4f48e39 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 Binary files a/app/static/icons/scologo_img.png and b/app/static/icons/scologo_img.png differ diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html new file mode 100644 index 0000000000000000000000000000000000000000..830c3f67295074371cb8c2b52f80131790c6557d --- /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 b0116ba791c12fa45bbeedc7d45fe9fedc45130a..a93db230ba447bd88609ef70d77e5e40c1169d1e 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 0000000000000000000000000000000000000000..ac0486446ae2ad76d0016734fa6253829801d8ec --- /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 ac67f0e11af585ae3aa7cab7c68550d309bb2f2e..1b1c97dd93c343ccbbdabebe184e6f8d276b393f 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 b9385209633fa3ade5a9cfef25868db167a91d98..0eb0d46e45ebff3d58e8537e3ac7830b3306f9bf 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 d15c3f7dba05542bed3b715e176e0c7547d2eeb0..2a48a794364ad3e6f778d4b9ddba84f7b821d0e9 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 19d6f0ecdb0f91f54fad425db993522f7a8d5dc8..371cd21dbbf3a73e16937583a2be0e3414c062f1 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 74899a0aeb5e704093d330f80d35b660272111ac..0edd198d0aec31cecb538744f6e920cc88e7018d 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 + )