diff --git a/README.md b/README.md
index b80ecd5b58..4b3eda5fd2 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 0000000000..d819beda5f
--- /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 b2eb29e062..6a0ca770a8 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 6f844602c2..b07f12da68 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 db154e500c..38309a5b89 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 1207d6bfa3..eef37102cd 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 0000000000..2652c4f913
--- /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 979ed3af7b..b61854d28a 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)
+ """,
]
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('
+
+ """
)
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(
- """
\ 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 b0116ba791..a93db230ba 100644
--- a/app/templates/pn/form_modules_ue_coefs.html
+++ b/app/templates/pn/form_modules_ue_coefs.html
@@ -15,13 +15,35 @@