WIP: PN BUT

This commit is contained in:
Emmanuel Viennet 2021-11-17 10:28:51 +01:00
parent 14ab816bee
commit 58a7508043
20 changed files with 841 additions and 235 deletions

View File

@ -89,6 +89,17 @@ exemple pour travailler en mode "développement" avec `FLASK_ENV=development`.
### Tests unitaires ### 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 Certains tests ont besoin d'un département déjà créé, qui n'est pas créé par les
scripts de tests: scripts de tests:
Lancer au préalable: 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 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. 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 export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV

75
app/comp/moy_mod.py Normal file
View File

@ -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

View File

@ -34,21 +34,22 @@ from app import db
from app import models 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 """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. Unspecified coefs (not defined in db) are set to zero.
""" """
ues = models.UniteEns.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).all() modules = models.Module.query.filter_by(formation_id=formation_id)
ue_ids = [ue.id for ue in ues] ue_ids = [ue.id for ue in ues]
module_ids = [module.id for module in modules] 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 ( for mod_coef in (
db.session.query(models.ModuleUECoef) db.session.query(models.ModuleUECoef)
.filter(models.UniteEns.formation_id == formation_id) .filter(models.UniteEns.formation_id == formation_id)
.filter(models.ModuleUECoef.ue_id == models.UniteEns.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) df.fillna(value=0, inplace=True)
return df return df

View File

@ -37,7 +37,6 @@ from app.models.formations import (
Module, Module,
ModuleUECoef, ModuleUECoef,
NotesTag, NotesTag,
notes_modules_tags,
) )
from app.models.formsemestre import ( from app.models.formsemestre import (
FormSemestre, FormSemestre,

View File

@ -36,6 +36,7 @@ class Formation(db.Model):
ues = db.relationship("UniteEns", backref="formation", lazy="dynamic") ues = db.relationship("UniteEns", backref="formation", lazy="dynamic")
formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation")
ues = db.relationship("UniteEns", lazy="dynamic", backref="formation") ues = db.relationship("UniteEns", lazy="dynamic", backref="formation")
modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>" 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: # Relations:
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic") modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True) 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): def __init__(self, **kwargs):
self.ue_coefs = [] self.ue_coefs = []

View File

@ -4,6 +4,8 @@
""" """
from typing import Any from typing import Any
import flask_sqlalchemy
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
@ -12,6 +14,8 @@ from app.models import UniteEns
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.models.formations import UniteEns, Module
from app.models.moduleimpls import ModuleImpl
class FormSemestre(db.Model): class FormSemestre(db.Model):
@ -87,8 +91,24 @@ class FormSemestre(db.Model):
if self.modalite is None: if self.modalite is None:
self.modalite = FormationModalite.DEFAULT_MODALITE self.modalite = FormationModalite.DEFAULT_MODALITE
def get_ues(self): def query_ues(self) -> flask_sqlalchemy.BaseQuery:
"UE des modules de ce semestre" """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 # Association id des utilisateurs responsables (aka directeurs des etudes) du semestre

122
app/scodoc/sco_edit_apc.py Normal file
View File

@ -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)

View File

@ -32,6 +32,7 @@ import flask
from flask import url_for, render_template from flask import url_for, render_template
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
from app.models.formations import Matiere, UniteEns
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -106,80 +107,99 @@ def do_module_create(args) -> int:
return r 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""" """Création d'un module"""
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue 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 !") raise ScoValueError("invalid matiere !")
matiere = sco_edit_matiere.matiere_list(args={"matiere_id": matiere_id})[0] ue = matiere.ue
UE = sco_edit_ue.ue_list(args={"ue_id": matiere["ue_id"]})[0] parcours = ue.formation.get_parcours()
formation = sco_formations.formation_list(
args={"formation_id": UE["formation_id"]}
)[0]
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
is_apc = parcours.APC_SAE 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 = [ H = [
html_sco_header.sco_header(page_title="Création d'un module"), html_sco_header.sco_header(page_title=f"Création {object_name}"),
"""<h2>Création d'un module dans la matière %(titre)s""" % matiere, f"""<h2>Création {object_name} dans la matière {matiere.titre},
""" (UE %(acronyme)s)</h2>""" % UE, (UE {ue.acronyme})</h2>
render_template("scodoc/help/modules.html", is_apc=is_apc), """,
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) # 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: if modules:
default_num = max([m["numero"] for m in modules]) + 10 default_num = max([m.numero for m in modules]) + 10
else: else:
default_num = 10 default_num = 10
tf = TrivialFormulator( descr = [
request.base_url,
scu.get_request_args(),
( (
"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, "input_type": "separator",
"explanation": "code du module (doit être unique dans la formation)", "title": """
"allow_null": False, <div>(<em>les coefficients vers les UE se fixent sur la page dédiée</em>)
"validator": lambda val, field, formation_id=formation[ </div>""",
"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",
}, },
), ),
]
else:
semestres_indices = list(range(1, parcours.NB_SEM + 1))
descr += [
( (
"coefficient", "coefficient",
{ {
@ -189,10 +209,6 @@ def module_create(matiere_id=None):
"allow_null": False, "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", "semestre_id",
{ {
@ -205,24 +221,35 @@ def module_create(matiere_id=None):
"allowed_values": semestres_indices, "allowed_values": semestres_indices,
}, },
), ),
( ]
"code_apogee", descr += [
{ # ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
"title": "Code Apogée", ("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
"size": 25, ("ue_id", {"default": ue.id, "input_type": "hidden"}),
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", ("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
}, (
), "code_apogee",
( {
"numero", "title": "Code Apogée",
{ "size": 25,
"size": 2, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
"explanation": "numéro (1,2,3,4...) pour ordre d'affichage", },
"type": "int",
"default": default_num,
},
),
), ),
(
"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", submitlabel="Créer ce module",
) )
if tf[0] == 0: if tf[0] == 0:
@ -233,7 +260,7 @@ def module_create(matiere_id=None):
url_for( url_for(
"notes.ue_table", "notes.ue_table",
scodoc_dept=g.scodoc_dept, 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: if not modules:
raise ScoValueError("invalid module !") raise ScoValueError("invalid module !")
module = modules[0] module = modules[0]
a_module = models.Module.query.get(module_id)
unlocked = not module_is_locked(module_id) unlocked = not module_is_locked(module_id)
formation_id = module["formation_id"] formation_id = module["formation_id"]
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
@ -434,7 +462,6 @@ def module_edit(module_id=None):
), ),
] ]
if is_apc: if is_apc:
a_module = models.Module.query.get(module_id)
coefs_descr = a_module.ue_coefs_descr() coefs_descr = a_module.ue_coefs_descr()
if coefs_descr: if coefs_descr:
coefs_descr_txt = ", ".join(["%s: %s" % x for x in 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, "enabled": unlocked,
}, },
), ),
( ]
"semestre_id", if is_apc:
{ # le semestre du module est toujours celui de son UE
"input_type": "menu", descr += [
"type": "int", (
"title": parcours.SESSION_NAME.capitalize(), "semestre_id",
"explanation": "%s de début du module dans la formation standard" {
% parcours.SESSION_NAME, "input_type": "hidden",
"labels": [str(x) for x in semestres_indices], "type": "int",
"allowed_values": semestres_indices, "readonly": True,
"enabled": unlocked, },
}, )
), ]
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", "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( tf = TrivialFormulator(
request.base_url, request.base_url,
scu.get_request_args(), scu.get_request_args(),
@ -538,7 +583,7 @@ def module_edit(module_id=None):
def edit_module_set_code_apogee(id=None, value=None): def edit_module_set_code_apogee(id=None, value=None):
"Set UE code apogee" "Set UE code apogee"
module_id = id 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)) log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
modules = module_list(args={"module_id": module_id}) modules = module_list(args={"module_id": module_id})

View File

@ -45,6 +45,7 @@ from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours 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_formation
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module 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 """ajoute semestre_id dans les ue, en regardant
semestre_idx ou à défaut le premier module de chacune. semestre_idx ou à défaut, pour les formations non APC, le premier module
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000), de chacune.
Les UE sans modules se voient attribuer le numero UE_SEM_DEFAULT (1000000),
qui les place à la fin de la liste. qui les place à la fin de la liste.
""" """
for ue in ues: for ue in ues:
if ue["semestre_idx"] is not None: if ue["semestre_idx"] is not None:
ue["semestre_id"] = ue["semestre_idx"] ue["semestre_id"] = ue["semestre_idx"]
elif is_apc:
ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
else: else:
# était le comportement ScoDoc7
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
if modules: if modules:
ue["semestre_id"] = modules[0]["semestre_id"] ue["semestre_id"] = modules[0]["semestre_id"]
else: else:
ue["semestre_id"] = 1000000 ue["semestre_id"] = sco_codes_parcours.UE_SEM_DEFAULT
def next_ue_numero(formation_id, semestre_id=None): def next_ue_numero(formation_id, semestre_id=None):
"""Numero d'une nouvelle UE dans cette formation. """Numero d'une nouvelle UE dans cette formation.
Si le semestre est specifie, cherche les UE ayant des modules de ce semestre 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}) ues = ue_list(args={"formation_id": formation_id})
if not ues: if not ues:
return 0 return 0
@ -410,7 +416,7 @@ def next_ue_numero(formation_id, semestre_id=None):
return ues[-1]["numero"] + 1000 return ues[-1]["numero"] + 1000
else: else:
# Avec semestre: (prend le semestre du 1er module de l'UE) # 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] ue_list_semestre = [ue for ue in ues if ue["semestre_id"] == semestre_id]
if ue_list_semestre: if ue_list_semestre:
return ue_list_semestre[-1]["numero"] + 10 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_formations
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
F = sco_formations.formation_list(args={"formation_id": formation_id}) formation = Formation.query.get(formation_id)
if not F: if not formation:
raise ScoValueError("invalid formation_id") raise ScoValueError("invalid formation_id")
F = F[0] parcours = formation.get_parcours()
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
is_apc = parcours.APC_SAE is_apc = parcours.APC_SAE
locked = sco_formations.formation_has_locked_sems(formation_id) locked = sco_formations.formation_has_locked_sems(formation_id)
ues = ue_list(args={"formation_id": formation_id, "is_external": False}) ues = ue_list(args={"formation_id": formation_id, "is_external": False})
ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True}) ues_externes = ue_list(args={"formation_id": formation_id, "is_external": True})
# tri par semestre et numero: # tri par semestre et numero:
_add_ue_semestre_id(ues) _add_ue_semestre_id(ues, is_apc)
_add_ue_semestre_id(ues_externes) _add_ue_semestre_id(ues_externes, is_apc)
ues.sort(key=lambda u: (u["semestre_id"], u["numero"])) ues.sort(key=lambda u: (u["semestre_id"], u["numero"]))
ues_externes.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_duplicate_ue_codes = len(set([ue["ue_code"] for ue in ues])) != len(ues)
has_perm_change = current_user.has_permission(Permission.ScoChangeFormation) has_perm_change = current_user.has_permission(Permission.ScoChangeFormation)
# editable = (not locked) and has_perm_change # editable = (not locked) and has_perm_change
# On autorise maintanant la modification des formations qui ont des semestres verrouillés, # On autorise maintenant la modification des formations qui ont
# sauf si cela affect les notes passées (verrouillées): # 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 modif des modules utilisés dans des semestres verrouillés
# - pas de changement des codes d'UE 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 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", "libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.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"""<h2>Formation {formation.titre} ({formation.acronyme})
% F, [version {formation.version}] code {formation.formation_code}
lockicon, {lockicon}
"</h2>", </h2>
""",
] ]
if locked: if locked:
H.append( H.append(
@ -533,41 +540,41 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
# Description de la formation # Description de la formation
H.append('<div class="formation_descr">') H.append('<div class="formation_descr">')
H.append( H.append(
'<div class="fd_d"><span class="fd_t">Titre:</span><span class="fd_v">%(titre)s</span></div>' f"""<div class="fd_d"><span class="fd_t">Titre:
% F </span><span class="fd_v">{formation.titre}</span>
) </div>
H.append( <div class="fd_d"><span class="fd_t">Titre officiel:</span>
'<div class="fd_d"><span class="fd_t">Titre officiel:</span><span class="fd_v">%(titre_officiel)s</span></div>' <span class="fd_v">{formation.titre_officiel}</span>
% F </div>
) <div class="fd_d"><span class="fd_t">Acronyme:</span>
H.append( <span class="fd_v">{formation.acronyme}</span>
'<div class="fd_d"><span class="fd_t">Acronyme:</span><span class="fd_v">%(acronyme)s</span></div>' </div>
% F <div class="fd_d"><span class="fd_t">Code:</span>
) <span class="fd_v">{formation.formation_code}</span>
H.append( </div>
'<div class="fd_d"><span class="fd_t">Code:</span><span class="fd_v">%(formation_code)s</span></div>' <div class="fd_d"><span class="fd_t">Version:</span>
% F <span class="fd_v">{formation.version}</span>
) </div>
H.append( <div class="fd_d"><span class="fd_t">Type parcours:</span>
'<div class="fd_d"><span class="fd_t">Version:</span><span class="fd_v">%(version)s</span></div>' <span class="fd_v">{parcours.__doc__}</span>
% F </div>
) """
H.append(
'<div class="fd_d"><span class="fd_t">Type parcours:</span><span class="fd_v">%s</span></div>'
% parcours.__doc__
) )
if parcours.UE_IS_MODULE: if parcours.UE_IS_MODULE:
H.append( 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: if editable:
H.append( H.append(
'<div><a href="formation_edit?formation_id=%(formation_id)s" class="stdlink">modifier ces informations</a></div>' f"""<div><a href="{
% F url_for('notes.formation_edit', scodoc_dept=g.scodoc_dept,
formation_id=formation_id)
}" class="stdlink">modifier ces informations</a></div>"""
) )
H.append("</div>") H.append("</div>")
# Formation APC (BUT) ? # Formation APC (BUT) ?
if is_apc: if is_apc:
H.append( H.append(
@ -575,52 +582,32 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<div class="ue_list_tit">Formation par compétences (BUT)</div> <div class="ue_list_tit">Formation par compétences (BUT)</div>
<ul> <ul>
<li><a class="stdlink" href="{ <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> }">éditer les coefficients des ressources et SAÉs</a></li>
</ul> </ul>
</div>""" </div>"""
) )
# Description des UE/matières/modules # 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( H.append(
'<form><input type="checkbox" class="sco_tag_checkbox">montrer les tags</input></form>' """
<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>
"""
) )
H.append( if is_apc:
_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( H.append(
'<ul><li><a class="stdlink" href="ue_create?formation_id=%s">Ajouter une UE</a></li>' sco_edit_apc.html_edit_formation_apc(
% formation_id formation, editable=editable, tag_editable=tag_editable
) )
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
)
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>'
) )
else:
H.append( H.append(
_ue_table_ues( _ue_table_ues(
parcours, parcours,
ues_externes, ues,
editable, editable,
tag_editable, tag_editable,
has_perm_change, has_perm_change,
@ -631,28 +618,84 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
delete_disabled_icon, delete_disabled_icon,
) )
) )
H.append("</div>") # formation_ue_list 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(
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,
tag_editable,
has_perm_change,
arrow_up,
arrow_down,
arrow_none,
delete_icon,
delete_disabled_icon,
)}
</div>
"""
)
H.append("<p><ul>") H.append("<p><ul>")
if editable: if editable:
H.append( H.append(
""" f"""
<li><a class="stdlink" href="formation_create_new_version?formation_id=%(formation_id)s">Créer une nouvelle version (non verrouillée)</a></li> <li><a class="stdlink" href="{
""" url_for('notes.formation_create_new_version',
% F scodoc_dept=g.scodoc_dept, formation_id=formation_id
)
}">Créer une nouvelle version (non verrouillée)</a>
</li>
"""
) )
H.append( H.append(
""" f"""
<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="{
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=xml">Export XML de la formation</a> (permet de la sauvegarder pour l'échanger avec un autre site)</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="formation_export?formation_id=%(formation_id)s&format=json">Export JSON de la formation</a></li> <li><a class="stdlink" href="{
url_for('notes.module_table', scodoc_dept=g.scodoc_dept,
<li><a class="stdlink" href="module_list?formation_id=%(formation_id)s">Liste détaillée des modules de la formation</a> (debug) </li> formation_id=formation_id)
</ul> }">Liste détaillée des modules de la formation</a> (debug)
</p>""" </li>
% F </ul>
</p>"""
) )
if has_perm_change: if has_perm_change:
H.append( H.append(
@ -679,12 +722,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
if current_user.has_permission(Permission.ScoImplement): if current_user.has_permission(Permission.ScoImplement):
H.append( H.append(
"""<ul> f"""<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><a class="stdlink" href="{
</li> url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, semestre_id=1)
</ul>""" }">Mettre en place un nouveau semestre de formation %(acronyme)s</a>
% F </li>
</ul>"""
) )
# <li>(debug) <a class="stdlink" href="check_form_integrity?formation_id=%(formation_id)s">Vérifier cohérence</a></li> # <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_icon,
delete_disabled_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 = [] H = []
cur_ue_semestre_id = None cur_ue_semestre_id = None
iue = 0 iue = 0
@ -802,6 +848,7 @@ def _ue_table_ues(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
module_type=module_type,
) )
) )
return "\n".join(H) return "\n".join(H)
@ -817,6 +864,7 @@ def _ue_table_matieres(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
module_type=None,
): ):
"""Édition de programme: liste des matières (et leurs modules) d'une UE.""" """Édition de programme: liste des matières (et leurs modules) d'une UE."""
H = [] H = []
@ -883,6 +931,7 @@ def _ue_table_ressources_saes(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
module_type=None,
): ):
"""Édition de programme: liste des ressources et SAÉs d'une UE. """Édition de programme: liste des ressources et SAÉs d'une UE.
(pour les parcours APC_SAE) (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"]}) 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), ("Ressources", "ressource", scu.ModuleType.RESSOURCE),
("SAÉs", "SAÉ", scu.ModuleType.SAE), ("SAÉs", "SAÉ", scu.ModuleType.SAE),
("Autres modules", "xxx", None), ("Autres modules", "xxx", None),
@ -914,9 +963,9 @@ def _ue_table_ressources_saes(
elements = [ elements = [
m m
for m in modules for m in modules
if module_type == m["module_type"] if element_type == m["module_type"]
or ( or (
(module_type is None) (element_type is None)
and m["module_type"] and m["module_type"]
not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE) not in (scu.ModuleType.RESSOURCE, scu.ModuleType.SAE)
) )
@ -933,6 +982,7 @@ def _ue_table_ressources_saes(
arrow_none, arrow_none,
delete_icon, delete_icon,
delete_disabled_icon, delete_disabled_icon,
module_type=module_type,
empty_list_msg="Aucune " + element_name, empty_list_msg="Aucune " + element_name,
create_element_msg="créer une " + element_name, create_element_msg="créer une " + element_name,
add_suppress_link=False, 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, Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
aussi ScoDoc laisse le choix. aussi ScoDoc laisse le choix.
""" """
Modlist = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]}) modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in Modlist]))) return sorted(list(set([mod["semestre_id"] for mod in modules])))

View File

@ -39,6 +39,7 @@ from flask import request
from app import db from app import db
from app import log from app import log
from app import models from app import models
from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError 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] modimpl = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
mod = modimpl["module"] mod = modimpl["module"]
formsemestre_id = modimpl["formsemestre_id"] formsemestre_id = modimpl["formsemestre_id"]
sem_ues = db.session.query(models.UniteEns).filter( sem = FormSemestre.query.get(formsemestre_id)
models.ModuleImpl.formsemestre_id == formsemestre_id, sem_ues = sem.query_ues().all()
models.Module.id == models.ModuleImpl.module_id,
models.UniteEns.id == models.Module.ue_id,
)
is_malus = mod["module_type"] == ModuleType.MALUS is_malus = mod["module_type"] == ModuleType.MALUS
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE) is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)

View File

@ -263,7 +263,8 @@ div.logo-logo {
} }
div.logo-logo img { div.logo-logo img {
margin-top: 20px; margin-top: 20px;
width: 100px; width: 55px; /* 100px */
padding-right: 50px;
} }
div.sidebar-bottom { div.sidebar-bottom {
margin-top: 10px; margin-top: 10px;
@ -1480,7 +1481,40 @@ div.formation_ue_list {
margin-right: 12px; margin-right: 12px;
padding-left: 5px; 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 { li.module_malus span.formation_module_tit {
color: red; color: red;
font-weight: bold; font-weight: bold;
@ -1498,7 +1532,6 @@ div.ue_list_tit {
ul.notes_ue_list { ul.notes_ue_list {
background-color: rgb(240,240,240); background-color: rgb(240,240,240);
font-weight: bold;
margin-top: 4px; margin-top: 4px;
margin-right: 1em; margin-right: 1em;
} }
@ -1519,7 +1552,10 @@ span.ue_type {
margin-left: 1.5em; margin-left: 1.5em;
margin-right: 1.5em; margin-right: 1.5em;
} }
ul.notes_module_list span.ue_coefs_list {
color: blue;
font-size: 70%;
}
div.formation_ue_list_externes { div.formation_ue_list_externes {
background-color: #98cc98; background-color: #98cc98;
} }
@ -1572,7 +1608,7 @@ div#ue_list_code {
} }
ul.notes_module_list { ul.notes_module_list {
list-style-type: none; list-style-type: none;
} }
div#ue_list_etud_validations { div#ue_list_etud_validations {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -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>

View File

@ -15,13 +15,35 @@
<h2>Formation {{formation.titre}} ({{formation.acronyme}}) <h2>Formation {{formation.titre}} ({{formation.acronyme}})
[version {{formation.version}}] code {{formation.code}}</h2> [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> <div class="tableau"></div>
<script> <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 () { $(function () {
$.getJSON("{{data_source}}", function (data) { let semestre_idx = $("#semestre_idx")[0].value;
build_table(data); 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) { function save(obj) {
var value = obj.innerText.trim(); var value = obj.innerText.trim();

View File

@ -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>

View File

@ -429,6 +429,7 @@ sco_publish(
"/edit_module_set_code_apogee", "/edit_module_set_code_apogee",
sco_edit_module.edit_module_set_code_apogee, sco_edit_module.edit_module_set_code_apogee,
Permission.ScoChangeFormation, Permission.ScoChangeFormation,
methods=["GET", "POST"],
) )
sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)

View File

@ -66,16 +66,20 @@ from app.scodoc import html_sco_header
from app.scodoc.sco_permissions import Permission 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 @scodoc
@permission_required(Permission.ScoView) @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""" """Description JSON de la table des coefs modules/UE dans une formation"""
_ = models.Formation.query.get_or_404(formation_id) # check _ = models.Formation.query.get_or_404(formation_id) # check
df = moy_ue.df_load_ue_coefs(formation_id) df = moy_ue.df_load_ue_coefs(formation_id, semestre_idx)
ues = models.UniteEns.query.filter_by(formation_id=formation_id).all() ues = models.UniteEns.query.filter_by(
modules = models.Module.query.filter_by(formation_id=formation_id).all() 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 # Titre des modules, en ligne
col_titres_mods = [ col_titres_mods = [
{ {
@ -108,7 +112,7 @@ def table_modules_ue_coefs(formation_id):
"x": col, "x": col,
"y": row, "y": row,
"style": "champs", "style": "champs",
"data": df[ue.id][mod.id] or "", "data": df[mod.id][ue.id] or "",
"editable": True, "editable": True,
"module_id": mod.id, "module_id": mod.id,
"ue_id": ue.id, "ue_id": ue.id,
@ -146,10 +150,11 @@ def set_module_ue_coef():
return scu.json_error("ok", success=True, status=201) 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 @scodoc
@permission_required(Permission.ScoChangeFormation) @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""" """Formulaire édition grille coefs EU/modules"""
formation = models.Formation.query.filter_by( formation = models.Formation.query.filter_by(
formation_id=formation_id formation_id=formation_id
@ -161,9 +166,12 @@ def edit_modules_ue_coefs(formation_id):
"notes.table_modules_ue_coefs", "notes.table_modules_ue_coefs",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formation_id=formation_id, formation_id=formation_id,
semestre_idx=semestre_idx or "",
), ),
data_save=url_for( data_save=url_for(
"notes.set_module_ue_coef", "notes.set_module_ue_coef",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
), ),
semestre_idx=int(semestre_idx),
semestre_ids=range(1, formation.get_parcours().NB_SEM + 1),
) )

View File

@ -281,7 +281,7 @@ def list_depts(depts=""): # list-dept
"--name", "--name",
is_flag=True, is_flag=True,
help="show database name instead of connexion string (required for " help="show database name instead of connexion string (required for "
"dropdb/createddb commands)", "dropdb/createdb commands)",
) )
def scodoc_database(name): # list-dept def scodoc_database(name): # list-dept
"""print the database connexion string""" """print the database connexion string"""

View File

@ -166,6 +166,7 @@ class ScoFake(object):
is_external=None, is_external=None,
code_apogee=None, code_apogee=None,
coefficient=None, coefficient=None,
semestre_idx=None,
): ):
"""Crée une UE""" """Crée une UE"""
if numero is None: if numero is None:

View File

@ -1,10 +1,15 @@
""" """
Test modèles évaluations avec poids BUT Test modèles évaluations avec poids BUT
""" """
import numpy as np
import pandas as pd
from tests.unit import sco_fake_gen from tests.unit import sco_fake_gen
from app import db from app import db
from app import models 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") mapp.set_sco_dept("RT")
@ -18,11 +23,24 @@ login_user(admin_user)
def setup_formation_test(): def setup_formation_test():
G = sco_fake_gen.ScoFake(verbose=False) G = sco_fake_gen.ScoFake(verbose=False)
_f = G.create_formation( _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") _mat = G.create_matiere(ue_id=_ue1["ue_id"], titre="matière test")
_mod = G.create_module( _mod = G.create_module(
matiere_id=_mat["matiere_id"], matiere_id=_mat["matiere_id"],
@ -31,6 +49,7 @@ def setup_formation_test():
titre="module test", titre="module test",
ue_id=_ue1["ue_id"], ue_id=_ue1["ue_id"],
formation_id=_f["formation_id"], formation_id=_f["formation_id"],
semestre_id=2,
) )
return G, _f["id"], _ue1["id"], _ue2["id"], _ue3["id"], _mod["id"] 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) e1.set_ue_poids(ue1, p1)
db.session.commit() db.session.commit()
assert e1.get_ue_poids_dict()[ue1_id] == p1 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] poids = [1.0, 2.0, 3.0]
for (ue, p) in zip(ues, poids): for (ue, p) in zip(ues, poids):
e1.set_ue_poids(ue, p) e1.set_ue_poids(ue, p)
@ -109,3 +130,60 @@ def test_modules_coefs(test_client):
mod.set_ue_coef(ue2, 0.0) mod.set_ue_coef(ue2, 0.0)
db.session.commit() db.session.commit()
assert len(mod.ue_coefs) == 0 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
)