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 b0871c44..38e0da1a 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 00000000..830c3f67 --- /dev/null +++ b/app/templates/pn/form_mods.html @@ -0,0 +1,82 @@ +{# Édition liste modules APC (SAÉ ou ressources) #} + +
    +
    {{titre}}
    + + +
    \ 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: + +
    +