From 58a75080433630dd93ce72a9a3a9c8bac29c26c4 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet 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 | 258 ++++++++++++-------- 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, 841 insertions(+), 235 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 b80ecd5b..4b3eda5f 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 00000000..d819beda --- /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 b2eb29e0..6a0ca770 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 6f844602..b07f12da 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 db154e50..38309a5b 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 1207d6bf..eef37102 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 00000000..2652c4f9 --- /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 979ed3af..b61854d2 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"), - """

Création d'un module dans la matière %(titre)s""" % matiere, - """ (UE %(acronyme)s)

""" % UE, - render_template("scodoc/help/modules.html", is_apc=is_apc), + html_sco_header.sco_header(page_title=f"Création {object_name}"), + f"""

Création {object_name} dans la matière {matiere.titre}, + (UE {ue.acronyme})

+ """, + 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", + "sep_ue_coefs", { - "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", - { - "size": 4, - "type": "float", - "explanation": "nombre d'heures de Travaux Pratiques", + "input_type": "separator", + "title": """ +
(les coefficients vers les UE se fixent sur la page dédiée) +
""", }, ), + ] + 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 1a46be44..77b89b1d 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}", ), - """

Formation %(titre)s (%(acronyme)s) [version %(version)s] code %(formation_code)s""" - % F, - lockicon, - "

", + f"""

Formation {formation.titre} ({formation.acronyme}) + [version {formation.version}] code {formation.formation_code} + {lockicon} +

+ """, ] 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('
') H.append( - '
Titre:%(titre)s
' - % F - ) - H.append( - '
Titre officiel:%(titre_officiel)s
' - % F - ) - H.append( - '
Acronyme:%(acronyme)s
' - % F - ) - H.append( - '
Code:%(formation_code)s
' - % F - ) - H.append( - '
Version:%(version)s
' - % F - ) - H.append( - '
Type parcours:%s
' - % parcours.__doc__ + f"""
Titre: + {formation.titre} +
+
Titre officiel: + {formation.titre_officiel} +
+
Acronyme: + {formation.acronyme} +
+
Code: + {formation.formation_code} +
+
Version: + {formation.version} +
+
Type parcours: + {parcours.__doc__} +
+ """ ) if parcours.UE_IS_MODULE: H.append( - '
(Chaque module est une UE)
' + """
+ (Chaque module est une UE)
""" ) if editable: H.append( - '
modifier ces informations
' - % F + f"""
modifier ces informations
""" ) - H.append("
") + # Formation APC (BUT) ? if is_apc: H.append( @@ -575,52 +582,32 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
Formation par compétences (BUT)
""" ) # Description des UE/matières/modules - H.append('
') - H.append('
Programme pédagogique:
') - H.append( - '
montrer les tags
' + """ +
+
Programme pédagogique:
+
+ montrer les tags +
+ """ ) - H.append( - _ue_table_ues( - parcours, - ues, - editable, - tag_editable, - has_perm_change, - arrow_up, - arrow_down, - arrow_none, - delete_icon, - delete_disabled_icon, - ) - ) - if editable: + if is_apc: H.append( - '' - % F - ) - H.append("
") # formation_ue_list - - if ues_externes: - H.append('
') - H.append( - '
UE externes déclarées (pour information):
' + sco_edit_apc.html_edit_formation_apc( + formation, editable=editable, tag_editable=tag_editable + ) ) + else: H.append( _ue_table_ues( parcours, - ues_externes, + ues, editable, tag_editable, has_perm_change, @@ -631,28 +618,84 @@ du programme" (menu "Semestre") si vous avez un semestre en cours); delete_disabled_icon, ) ) - H.append("
") # formation_ue_list + if editable: + H.append( + f""" + """ + ) + H.append("
") # formation_ue_list + + if ues_externes: + H.append( + f""" +
+
UE externes déclarées (pour information): +
+ {_ue_table_ues( + parcours, + ues_externes, + editable, + tag_editable, + has_perm_change, + arrow_up, + arrow_down, + arrow_none, + delete_icon, + delete_disabled_icon, + )} +
+ """ + ) H.append("

-

""" - % F +
  • Liste détaillée des modules de la formation (debug) +
  • + +

    """ ) 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( - """""" - % F + f"""""" ) #
  • (debug) Vérifier cohérence
  • @@ -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 a310ecdd..36b12ca2 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 e091b90a..e9ae3f07 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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 X^lgX1gQF~400000NkvXXu0mjf=F@*t delta 29210 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 B5wZXP diff --git a/app/templates/pn/form_mods.html b/app/templates/pn/form_mods.html new file mode 100644 index 00000000..830c3f67 --- /dev/null +++ b/app/templates/pn/form_mods.html @@ -0,0 +1,82 @@ +{# Édition liste modules APC (SAÉ ou ressources) #} + +
    +
    {{titre}}
    + +
      + +{% if not formation.ues.count() %} +
    • aucune UE
    • +{% else %} + {% for mod in modules %} +
    • + + {% if editable and not loop.first %} + {{arrow_up|safe}} + {% else %} + {{arrow_none|safe}} + {% endif %} + {% if editable and not loop.last %} + {{arrow_down|safe}} + {% else %} + {{arrow_none|safe}} + {% endif %} + + {% if editable and not mod.modimpls.count() %} + {{delete_icon|safe}} + {% else %} + {{delete_disabled_icon|safe}} + {% endif %} + + {% if editable %} + + {% endif %} + {{mod.code}} {{mod.titre|default("", true)}} + {% if editable %} + + {% endif %} + + {{formation.get_parcours().SESSION_NAME}} {{mod.semestre_id}} + + ({{mod.heures_cours}}/{{mod.heures_td}}/{{mod.heures_tp}}, + + Apo: + {{mod.code_apogee|default("", true)}}) + + + {% for coef in mod.ue_coefs %} + {{coef.ue.acronyme}}:{{coef.coef}} + {% endfor %} + + +
      + +
    • + {% endfor %} + + {% if editable %} +
    • {{create_element_msg}} +
    • + {% endif %} +{% endif %} +
    +
    \ 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 b0116ba7..a93db230 100644 --- a/app/templates/pn/form_modules_ue_coefs.html +++ b/app/templates/pn/form_modules_ue_coefs.html @@ -15,13 +15,35 @@

    Formation {{formation.titre}} ({{formation.acronyme}}) [version {{formation.version}}] code {{formation.code}}

    +
    Semestre: + +
    +