Compare commits

..

20 Commits

Author SHA1 Message Date
leonard_montalbano
3a3c3793ed tests unitaires formsemestre + ajout des fields à tests dans tools_test_api.py 2022-06-15 16:03:31 +02:00
leonard_montalbano
281539dd3b ajout tests unitaires absences 2022-06-15 16:01:48 +02:00
leonard_montalbano
5834412dfc ajout tests unitaires formation 2022-06-15 16:01:00 +02:00
leonard_montalbano
fa17ed6736 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into new_api 2022-06-14 16:24:22 +02:00
21a2c1b7e7 removed useless function 2022-06-08 22:41:53 +02:00
b140e891f1 Bonus La Rochelle (nouvelle version) 2022-06-08 22:18:39 +02:00
3f15ff099d Ré-écriture fonction ajout modules de malus à toutes les UE 2022-06-08 21:16:09 +02:00
1676fba5ab moduleimpl_is_conforme: bug cache (?) => refresh et exception 2022-06-08 17:56:27 +02:00
bde51dc639 Fix: check suppression formation/ue 2022-06-08 17:42:52 +02:00
c890e21ed0 Modif bonus Tarbes, qui a la même logique que celui de Rennes 1 2022-06-08 14:49:31 +02:00
1dc6dd3d6d Regroupe bonus IUT de Renens 1 2022-06-02 11:53:53 +02:00
be92c86baf cosmetic 2022-06-02 11:37:13 +02:00
3240d625b0 Bonus St Nazaire 2022-06-02 11:36:47 +02:00
10811173e6 Suppression source bonus ScoDoc7 2022-06-02 00:02:32 +02:00
f626fa3eff Modification bonus IUT de Rennes (n'affecte plus les moy. d'UE hors BUT). 2022-06-01 12:41:04 +02:00
ff0437d844 lint 2022-05-31 14:15:21 +02:00
596a45a90e Fix: ajout de validations d'UE externes 2022-05-30 12:13:31 +02:00
11b1b50a42 Fix: recapcomplet / xlsall 2022-05-29 17:38:52 +02:00
994959960c Recap: rangs dans les groupes 2022-05-23 10:59:43 +02:00
8a1569ac54 API: nom des permissions 2022-05-19 10:51:43 +02:00
22 changed files with 722 additions and 777 deletions

View File

@ -266,6 +266,8 @@ class BonusSportMultiplicatif(BonusSport):
amplitude = 0.005 # multiplie les points au dessus du seuil amplitude = 0.005 # multiplie les points au dessus du seuil
# En classique, les bonus multiplicatifs agissent par défaut sur les UE: # En classique, les bonus multiplicatifs agissent par défaut sur les UE:
classic_use_bonus_ues = True classic_use_bonus_ues = True
# Facteur multiplicatif max: (bonus = moy_ue*factor)
factor_max = 1000.0 # infini
# C'est un bonus "multiplicatif": on l'exprime en additif, # C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0 # sur chaque moyenne d'UE m_0
@ -285,6 +287,8 @@ class BonusSportMultiplicatif(BonusSport):
notes = np.nan_to_num(notes, copy=False) notes = np.nan_to_num(notes, copy=False)
factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20 factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20
factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus
# note < seuil_moy_gen, pas de bonus: pas de facteur négatif, ni
factor.clip(0.0, self.factor_max, out=factor)
# Ne s'applique qu'aux moyennes d'UE # Ne s'applique qu'aux moyennes d'UE
if len(factor.shape) == 1: # classic if len(factor.shape) == 1: # classic
@ -705,13 +709,15 @@ class BonusGrenobleIUT1(BonusSportMultiplicatif):
class BonusIUTRennes1(BonusSportAdditif): class BonusIUTRennes1(BonusSportAdditif):
"""Calcul bonus optionnels (sport, langue vivante, engagement étudiant), """Calcul bonus optionnels (sport, langue vivante, engagement étudiant),
règle IUT de l'Université de Rennes 1 (Lannion, St Malo). règle IUT de l'Université de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo).
<ul> <ul>
<li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées. <li>Les étudiants peuvent suivre un ou plusieurs activités optionnelles notées
dans les semestres pairs.<br>
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20. La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li> </li>
<li>Le vingtième des points au dessus de 10 est ajouté à la moyenne des UE. <li>Le vingtième des points au dessus de 10 est ajouté à la moyenne de chaque UE
en BUT, ou à la moyenne générale pour les autres formations.
</li> </li>
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/20 = 0,3 points <li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/20 = 0,3 points
sur chaque UE. sur chaque UE.
@ -720,11 +726,11 @@ class BonusIUTRennes1(BonusSportAdditif):
""" """
name = "bonus_iut_rennes1" name = "bonus_iut_rennes1"
displayed_name = "IUTs de Rennes 1 (Lannion, St Malo)" displayed_name = "IUTs de Rennes 1 (Lannion, Rennes, St Brieuc, St Malo)"
seuil_moy_gen = 10.0 seuil_moy_gen = 10.0
proportion_point = 1 / 20.0 proportion_point = 1 / 20.0
classic_use_bonus_ues = True classic_use_bonus_ues = False
# Adapté de BonusTarbes, mais s'applique aussi en classic # S'applique aussi en classic, sur la moy. gen.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus""" """calcul du bonus"""
# Prend la note de chaque modimpl, sans considération d'UE # Prend la note de chaque modimpl, sans considération d'UE
@ -748,22 +754,76 @@ class BonusIUTRennes1(BonusSportAdditif):
self.bonus_additif(bonus_moy_arr) self.bonus_additif(bonus_moy_arr)
# juste pour compatibilité (nom bonus en base):
class BonusStBrieuc(BonusIUTRennes1):
name = "bonus_iut_stbrieuc"
displayed_name = "IUTs de Rennes 1/St-Brieuc"
__doc__ = BonusIUTRennes1.__doc__
class BonusStMalo(BonusIUTRennes1):
name = "bonus_iut_stmalo"
displayed_name = "IUTs de Rennes 1/St-Malo"
__doc__ = BonusIUTRennes1.__doc__
class BonusLaRochelle(BonusSportAdditif): class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle. """Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
<ul> <ul>
<li>Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point.</li> <li>Si la note de sport est comprise entre 0 et 10 : pas dajout de point.</li>
<li>Si la note de sport est comprise entre 10 et 20 : ajout de 1% de cette <li>Si la note de sport est comprise entre 10 et 20 :
note sur la moyenne générale du semestre (ou sur les UE en BUT).</li> <ul>
<li>Pour le BUT, application pour chaque UE du semestre :
<ul>
<li>pour une note entre 18 et 20 => + 0,10 points</li>
<li>pour une note entre 16 et 17,99 => + 0,08 points</li>
<li>pour une note entre 14 et 15,99 => + 0,06 points</li>
<li>pour une note entre 12 et 13,99 => + 0,04 points</li>
<li>pour une note entre 10 et 11,99 => + 0,02 points</li>
</ul>
</li>
<li>Pour les DUT/LP :
ajout de 1% de la note sur la moyenne générale du semestre
</li>
</ul>
</li>
</ul> </ul>
""" """
name = "bonus_iutlr" name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle" displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # si bonus > 10, seuil_moy_gen = 10.0 # si bonus > 10,
seuil_comptage = 0.0 # tous les points sont comptés seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1% proportion_point = 0.01 # 1%
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# La date du semestre ?
if self.formsemestre.formation.is_apc():
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.0] = 0.10
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.08
bonus_moy_arr[bonus_moy_arr >= 14.0] = 0.06
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.04
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.02
self.bonus_additif(bonus_moy_arr)
else:
# DUT et LP:
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusLeHavre(BonusSportAdditif): class BonusLeHavre(BonusSportAdditif):
"""Bonus sport IUT du Havre sur les moyennes d'UE """Bonus sport IUT du Havre sur les moyennes d'UE
@ -966,7 +1026,7 @@ class BonusNantes(BonusSportAdditif):
class BonusPoitiers(BonusSportAdditif): class BonusPoitiers(BonusSportAdditif):
"""Calcul bonus optionnels (sport, culture), règle IUT de Poitiers. """Calcul bonus optionnels (sport, culture), règle IUT de Poitiers.
Les deux notes d'option supérieure à 10, bonifies les moyennes de chaque UE. Les deux notes d'option supérieure à 10, bonifient les moyennes de chaque UE.
bonus = (option1 - 10)*5% + (option2 - 10)*5% bonus = (option1 - 10)*5% + (option2 - 10)*5%
""" """
@ -991,27 +1051,6 @@ class BonusRoanne(BonusSportAdditif):
proportion_point = 1 proportion_point = 1
class BonusStBrieuc(BonusSportAdditif):
"""IUT de Saint Brieuc
Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE:
<ul>
<li>Bonus = (S - 10)/20</li>
</ul>
"""
# Utilisé aussi par St Malo, voir plus bas
name = "bonus_iut_stbrieuc"
displayed_name = "IUT de Saint-Brieuc"
proportion_point = 1 / 20.0
classic_use_bonus_ues = False
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if self.formsemestre.semestre_id % 2 == 0:
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
class BonusStEtienne(BonusSportAdditif): class BonusStEtienne(BonusSportAdditif):
"""IUT de Saint-Etienne. """IUT de Saint-Etienne.
@ -1042,27 +1081,42 @@ class BonusStDenis(BonusSportAdditif):
bonus_max = 0.5 bonus_max = 0.5
class BonusStMalo(BonusStBrieuc): class BonusStNazaire(BonusSportMultiplicatif):
# identique à St Brieux, sauf la doc """IUT de Saint-Nazaire
"""IUT de Saint Malo
Ne s'applique qu'aux semestres pairs (S2, S4, S6), et bonifie les moyennes d'UE: Trois bonifications sont possibles : sport, culture et engagement citoyen
(qui seront déclarées comme des modules séparés de l'UE bonus).
<ul> <ul>
<li>Bonus = (S - 10)/20</li> <li>Chaque bonus est compris entre 0 et 20 points -> 4pt = 1%<br>
(note 4/20: 1%, 8/20: 2%, 12/20: 3%, 16/20: 4%, 20/20: 5%)
</li>
<li>Le total des 3 bonus ne peut excéder 10%</li>
<li>La somme des bonus s'applique à la moyenne de chaque UE</li>
</ul> </ul>
<p>Exemple: une moyenne d'UE de 10/20 avec un total des bonus de 6% donne
une moyenne de 10,6.</p>
<p>Les bonifications s'appliquent aussi au classement général du semestre
et de l'année.
</p>
""" """
name = "bonus_iut_stmalo"
displayed_name = "IUT de Saint-Malo" name = "bonus_iutSN"
displayed_name = "IUT de Saint-Nazaire"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 0.0 # tous les points comptent
amplitude = 0.01 / 4 # 4pt => 1%
factor_max = 0.1 # 10% max
class BonusTarbes(BonusSportAdditif): class BonusTarbes(BonusIUTRennes1):
"""Calcul bonus optionnels (sport, culture), règle IUT de Tarbes. """Calcul bonus optionnels (sport, culture), règle IUT de Tarbes.
<ul> <ul>
<li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées. <li>Les étudiants opeuvent suivre un ou plusieurs activités optionnelles notées.
La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20. La meilleure des notes obtenue est prise en compte, si elle est supérieure à 10/20.
</li> </li>
<li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE. <li>Le trentième des points au dessus de 10 est ajouté à la moyenne des UE en BUT,
ou à la moyenne générale en DUT et LP.
</li> </li>
<li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points <li> Exemple: un étudiant ayant 16/20 bénéficiera d'un bonus de (16-10)/30 = 0,2 points
sur chaque UE. sur chaque UE.
@ -1076,29 +1130,6 @@ class BonusTarbes(BonusSportAdditif):
proportion_point = 1 / 30.0 proportion_point = 1 / 30.0
classic_use_bonus_ues = True classic_use_bonus_ues = True
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
ues = self.formsemestre.query_ues(with_sport=False).all()
ues_idx = [ue.id for ue in ues]
if self.formsemestre.formation.is_apc(): # --- BUT
bonus_moy_arr = np.where(
note_bonus_max > self.seuil_moy_gen,
(note_bonus_max - self.seuil_moy_gen) * self.proportion_point,
0.0,
)
self.bonus_ues = pd.DataFrame(
np.stack([bonus_moy_arr] * len(ues)).T,
index=self.etuds_idx,
columns=ues_idx,
dtype=float,
)
class BonusTours(BonusDirect): class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours. """Calcul bonus sport & culture IUT Tours.

View File

@ -41,7 +41,8 @@ from app import db
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoBugCatcher
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -423,7 +424,9 @@ def moduleimpl_is_conforme(
if nb_ues == 0: if nb_ues == 0:
return False # situation absurde (pas d'UE) return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues: if len(modules_coefficients) != nb_ues:
raise ValueError("moduleimpl_is_conforme: nb ue incoherent") # il arrive (#bug) que le cache ne soit pas à jour...
sco_cache.invalidate_formsemestre()
raise ScoBugCatcher("moduleimpl_is_conforme: nb ue incoherent")
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0 module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all( check = all(
(modules_coefficients[moduleimpl.module_id].to_numpy() != 0) (modules_coefficients[moduleimpl.module_id].to_numpy() != 0)

View File

@ -70,6 +70,7 @@ class ResultatsSemestre(ResultatsCache):
self.etud_moy_gen: pd.Series = None self.etud_moy_gen: pd.Series = None
self.etud_moy_gen_ranks = {} self.etud_moy_gen_ranks = {}
self.etud_moy_gen_ranks_int = {} self.etud_moy_gen_ranks_int = {}
self.moy_gen_rangs_by_group = None # virtual
self.modimpl_inscr_df: pd.DataFrame = None self.modimpl_inscr_df: pd.DataFrame = None
"Inscriptions: row etudid, col modimlpl_id" "Inscriptions: row etudid, col modimlpl_id"
self.modimpls_results: ModuleImplResults = None self.modimpls_results: ModuleImplResults = None
@ -397,7 +398,7 @@ class ResultatsSemestre(ResultatsCache):
- titles: { column_id : title } - titles: { column_id : title }
- columns_ids: (liste des id de colonnes) - columns_ids: (liste des id de colonnes)
. Si convert_values, transforme les notes en chaines ("12.34"). Si convert_values, transforme les notes en chaines ("12.34").
Les colonnes générées sont: Les colonnes générées sont:
etudid etudid
rang : rang indicatif (basé sur moy gen) rang : rang indicatif (basé sur moy gen)
@ -589,7 +590,9 @@ class ResultatsSemestre(ResultatsCache):
f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}" f"moy_{modimpl.module.type_abbrv()}_{modimpl.id}_{ue.id}"
) )
val_fmt = val_fmt_html = fmt_note(val) val_fmt = val_fmt_html = fmt_note(val)
if modimpl.module.module_type == scu.ModuleType.MALUS: if convert_values and (
modimpl.module.module_type == scu.ModuleType.MALUS
):
val_fmt_html = ( val_fmt_html = (
(scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else "" (scu.EMO_RED_TRIANGLE_DOWN + val_fmt) if val else ""
) )
@ -824,17 +827,25 @@ class ResultatsSemestre(ResultatsCache):
self.formsemestre.id self.formsemestre.id
) )
first_partition = True first_partition = True
col_order = 10
for partition in partitions: for partition in partitions:
cid = f"part_{partition['partition_id']}" cid = f"part_{partition['partition_id']}"
rg_cid = cid + "_rg" # rang dans la partition
titles[cid] = partition["partition_name"] titles[cid] = partition["partition_name"]
if first_partition: if first_partition:
klass = "partition" klass = "partition"
else: else:
klass = "partition partition_aux" klass = "partition partition_aux"
titles[f"_{cid}_class"] = klass titles[f"_{cid}_class"] = klass
titles[f"_{cid}_col_order"] = 10 titles[f"_{cid}_col_order"] = col_order
titles[f"_{rg_cid}_col_order"] = col_order + 1
col_order += 2
if partition["bul_show_rank"]:
titles[rg_cid] = f"Rg {partition['partition_name']}"
titles[f"_{rg_cid}_class"] = "partition_rangs"
partition_etud_groups = partitions_etud_groups[partition["partition_id"]] partition_etud_groups = partitions_etud_groups[partition["partition_id"]]
for row in rows: for row in rows:
group = None # group (dict) de l'étudiant dans cette partition
# dans NotesTableCompat, à revoir # dans NotesTableCompat, à revoir
etud_etat = self.get_etud_etat(row["etudid"]) etud_etat = self.get_etud_etat(row["etudid"])
if etud_etat == "D": if etud_etat == "D":
@ -847,8 +858,17 @@ class ResultatsSemestre(ResultatsCache):
group = partition_etud_groups.get(row["etudid"]) group = partition_etud_groups.get(row["etudid"])
gr_name = group["group_name"] if group else "" gr_name = group["group_name"] if group else ""
if gr_name: if gr_name:
row[f"{cid}"] = gr_name row[cid] = gr_name
row[f"_{cid}_class"] = klass row[f"_{cid}_class"] = klass
# Rangs dans groupe
if (
partition["bul_show_rank"]
and (group is not None)
and (group["id"] in self.moy_gen_rangs_by_group)
):
rang = self.moy_gen_rangs_by_group[group["id"]][0]
row[rg_cid] = rang.get(row["etudid"], "")
first_partition = False first_partition = False
def _recap_add_evaluations( def _recap_add_evaluations(

View File

@ -75,6 +75,15 @@ class UniteEns(db.Model):
return sco_edit_ue.ue_is_locked(self.id) return sco_edit_ue.ue_is_locked(self.id)
def can_be_deleted(self) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (self.modules.count() == 0) or not any(
m.modimpls.all() for m in self.modules
)
def guess_semestre_idx(self) -> None: def guess_semestre_idx(self) -> None:
"""Lorsqu'on prend une ancienne formation non APC, """Lorsqu'on prend une ancienne formation non APC,
les UE n'ont pas d'indication de semestre. les UE n'ont pas d'indication de semestre.

View File

@ -1,492 +0,0 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 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
#
##############################################################################
from operator import mul
import pprint
""" ANCIENS BONUS SPORT pour ScoDoc < 9.2 NON UTILISES A PARTIR DE 9.2 (voir comp/bonus_spo.py)
La fonction bonus_sport reçoit:
- notes_sport: la liste des notes des modules de sport et culture (une note par module
de l'UE de type sport/culture, toujours dans remise sur 20);
- coefs: un coef (float) pondérant chaque note (la plupart des bonus les ignorent);
- infos: dictionnaire avec des données pouvant être utilisées pour les calculs.
Ces données dépendent du type de formation.
infos = {
"moy" : la moyenne générale (float). 0. en BUT.
"sem" : {
"date_debut_iso" : "2010-08-01", # date de début de semestre
}
"moy_ues": {
ue_id : { # ue_status
"is_capitalized" : True|False,
"moy" : float, # moyenne d'UE prise en compte (peut-être capitalisée)
"sum_coefs": float, # > 0 si UE avec la moyenne calculée
"cur_moy_ue": float, # moyenne de l'UE (sans capitalisation))
}
}
}
Les notes passées sont:
- pour les formations classiques, la moyenne dans le module, calculée comme d'habitude
(moyenne pondérée des notes d'évaluations);
- pour le BUT: pareil, *en ignorant* les éventuels poids des évaluations. Le coefficient
de l'évaluation est pris en compte, mais pas les poids vers les UE.
Pour modifier les moyennes d'UE:
- modifier infos["moy_ues"][ue_id][["cur_moy_ue"]
et, seulement si l'UE n'est pas capitalisée, infos["moy_ues"][ue_id][["moy"]/
La valeur retournée est:
- formations classiques: ajoutée à la moyenne générale
- BUT: valeur multipliée par la somme des coefs modules sport ajoutée à chaque UE.
"""
def bonus_iutv(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 13 (sports, musique, deuxième langue,
culture, etc) non rattachés à une unité d'enseignement. Les points
au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 5% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10])
return bonus
def bonus_direct(notes_sport, coefs, infos=None):
"""Un bonus direct et sans chichis: les points sont directement ajoutés à la moyenne générale.
Les coefficients sont ignorés: tous les points de bonus sont sommés.
(rappel: la note est ramenée sur 20 avant application).
"""
return sum(notes_sport)
def bonus_iut_stdenis(notes_sport, coefs, infos=None):
"""Semblable à bonus_iutv mais total limité à 0.5 points."""
points = sum([x - 10 for x in notes_sport if x > 10]) # points au dessus de 10
bonus = points * 0.05 # ou / 20
return min(bonus, 0.5) # bonus limité à 1/2 point
def bonus_colmar(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT Colmar.
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'U.H.A. (sports, musique, deuxième langue, culture, etc) non
rattachés à une unité d'enseignement. Les points au-dessus de 10
sur 20 obtenus dans chacune des matières optionnelles sont cumulés
dans la limite de 10 points. 5% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
# les coefs sont ignorés
points = sum([x - 10 for x in notes_sport if x > 10])
points = min(10, points) # limite total à 10
bonus = points / 20.0 # 5%
return bonus
def bonus_iutva(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT Ville d'Avray
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Paris 10 (C2I) non rattachés à une unité d'enseignement.
Si la note est >= 10 et < 12, bonus de 0.1 point
Si la note est >= 12 et < 16, bonus de 0.2 point
Si la note est >= 16, bonus de 0.3 point
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant.
"""
sumc = sum(coefs) # assumes sum. coefs > 0
note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée
if note_sport >= 16.0:
return 0.3
if note_sport >= 12.0:
return 0.2
if note_sport >= 10.0:
return 0.1
return 0
def bonus_iut1grenoble_2017(notes_sport, coefs, infos=None):
"""Calcul bonus sport IUT Grenoble sur la moyenne générale (version 2017)
La note de sport de nos étudiants va de 0 à 5 points.
Chaque point correspond à un % qui augmente la moyenne de chaque UE et la moyenne générale.
Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%.
Calcul ici du bonus sur moyenne générale
"""
# les coefs sont ignorés
# notes de 0 à 5
points = sum([x for x in notes_sport])
factor = (points / 4.0) / 100.0
bonus = infos["moy"] * factor
return bonus
def bonus_lille(notes_sport, coefs, infos=None):
"""calcul bonus modules optionels (sport, culture), règle IUT Villeneuve d'Ascq
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Lille 1 (sports,etc) non rattachés à une unité d'enseignement. Les points
au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 4% (2% avant aout 2010) de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
if (
infos["sem"]["date_debut_iso"] > "2010-08-01"
): # changement de regle en aout 2010.
return sum([(x - 10) / 25.0 for x in notes_sport if x > 10])
return sum([(x - 10) / 50.0 for x in notes_sport if x > 10])
# Fonction Le Havre, par Dom. Soud.
def bonus_iutlh(notes_sport, coefs, infos=None):
"""Calcul bonus sport IUT du Havre sur moyenne générale et UE
La note de sport de nos étudiants va de 0 à 20 points.
m2=m1*(1+0.005*((10-N1)+(10-N2))
m2 : Nouvelle moyenne de l'unité d'enseignement si note de sport et/ou de langue supérieure à 10
m1 : moyenne de l'unité d'enseignement avant bonification
N1 : note de sport si supérieure à 10
N2 : note de seconde langue si supérieure à 10
Par exemple : sport 15/20 et langue 12/20 : chaque UE sera multipliée par 1+0.005*7, ainsi que la moyenne générale.
Calcul ici de la moyenne générale et moyennes d'UE non capitalisées.
"""
# les coefs sont ignorés
points = sum([x - 10 for x in notes_sport if x > 10])
points = min(10, points) # limite total à 10
factor = 1.0 + (0.005 * points)
# bonus nul puisque les moyennes sont directement modifiées par factor
bonus = 0
# Modifie la moyenne générale
infos["moy"] = infos["moy"] * factor
# Modifie les moyennes de toutes les UE:
for ue_id in infos["moy_ues"]:
ue_status = infos["moy_ues"][ue_id]
if ue_status["sum_coefs"] > 0:
# modifie moyenne UE ds semestre courant
ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * factor
if not ue_status["is_capitalized"]:
# si non capitalisee, modifie moyenne prise en compte
ue_status["moy"] = ue_status["cur_moy_ue"]
# open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
return bonus
def bonus_nantes(notes_sport, coefs, infos=None):
"""IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification
bonfication Sport / Culture / engagement citoyen
Nous ajoutons sur le bulletin une bonification de 0,2 pour chaque item
la bonification totale ne doit pas excéder les 0,5 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
Dans ScoDoc: on a déclaré une UE "sport&culture" dans laquelle on aura des modules
pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
"""
bonus = min(0.5, sum([x for x in notes_sport])) # plafonnement à 0.5 points
return bonus
# Bonus sport IUT Tours
def bonus_tours(notes_sport, coefs, infos=None):
"""Calcul bonus sport & culture IUT Tours sur moyenne generale
La note de sport & culture de nos etudiants est applique sur la moyenne generale.
"""
return min(1.0, sum(notes_sport)) # bonus maximum de 1 point
def bonus_iutr(notes_sport, coefs, infos=None):
"""Calcul du bonus , règle de l'IUT de Roanne
(contribuée par Raphael C., nov 2012)
Le bonus est compris entre 0 et 0.35 point.
cette procédure modifie la moyenne de chaque UE capitalisable.
"""
# modifie les moyennes de toutes les UE:
# le bonus est le minimum entre 0.35 et la somme de toutes les bonifs
bonus = min(0.35, sum([x for x in notes_sport]))
for ue_id in infos["moy_ues"]:
# open('/tmp/log','a').write( str(ue_id) + infos['moy_ues'] + '\n\n' )
ue_status = infos["moy_ues"][ue_id]
if ue_status["sum_coefs"] > 0:
# modifie moyenne UE dans semestre courant
ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] + bonus
if not ue_status["is_capitalized"]:
ue_status["moy"] = ue_status["cur_moy_ue"]
return bonus
def bonus_iutam(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport), regle IUT d'Amiens.
Les etudiants de l'IUT peuvent suivre des enseignements optionnels.
Si la note est de 10.00 a 10.49 -> 0.50% de la moyenne
Si la note est de 10.50 a 10.99 -> 0.75%
Si la note est de 11.00 a 11.49 -> 1.00%
Si la note est de 11.50 a 11.99 -> 1.25%
Si la note est de 12.00 a 12.49 -> 1.50%
Si la note est de 12.50 a 12.99 -> 1.75%
Si la note est de 13.00 a 13.49 -> 2.00%
Si la note est de 13.50 a 13.99 -> 2.25%
Si la note est de 14.00 a 14.49 -> 2.50%
Si la note est de 14.50 a 14.99 -> 2.75%
Si la note est de 15.00 a 15.49 -> 3.00%
Si la note est de 15.50 a 15.99 -> 3.25%
Si la note est de 16.00 a 16.49 -> 3.50%
Si la note est de 16.50 a 16.99 -> 3.75%
Si la note est de 17.00 a 17.49 -> 4.00%
Si la note est de 17.50 a 17.99 -> 4.25%
Si la note est de 18.00 a 18.49 -> 4.50%
Si la note est de 18.50 a 18.99 -> 4.75%
Si la note est de 19.00 a 20.00 -> 5.00%
Ce bonus s'ajoute a la moyenne generale du semestre de l'etudiant.
"""
# une seule note
note_sport = notes_sport[0]
if note_sport < 10.0:
return 0.0
prc = min((int(2 * note_sport - 20.0) + 2) * 0.25, 5)
bonus = infos["moy"] * prc / 100.0
return bonus
def bonus_saint_etienne(notes_sport, coefs, infos=None):
"""IUT de Saint-Etienne (jan 2014)
Nous avons différents types de bonification
bonfication Sport / Associations
coopératives de département / Bureau Des Étudiants
/ engagement citoyen / Langues optionnelles
Nous ajoutons sur le bulletin une bonification qui varie entre 0,1 et 0,3 ou 0,35 pour chaque item
la bonification totale ne doit pas excéder les 0,6 point.
Sur le bulletin nous ne mettons pas une note sur 20 mais directement les bonifications.
Dans ScoDoc: on a déclarer une UE "sport&culture" dans laquelle on aura des modules
pour chaque activité (Sport, Associations, ...)
avec à chaque fois une note (ScoDoc l'affichera comme une note sur 20, mais en fait ce sera la
valeur de la bonification: entrer 0,1/20 signifiera un bonus de 0,1 point la moyenne générale)
"""
bonus = min(0.6, sum([x for x in notes_sport])) # plafonnement à 0.6 points
return bonus
def bonus_iutTarbes(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionnels
(sport, Langues, action sociale, Théâtre), règle IUT Tarbes
Les coefficients ne sont pas pris en compte,
seule la meilleure note est prise en compte
le 1/30ème des points au-dessus de 10 sur 20 est retenu et s'ajoute à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
bonus = max([(x - 10) / 30.0 for x in notes_sport if x > 10] or [0.0])
return bonus
def bonus_iutSN(notes_sport, coefs, infos=None):
"""Calcul bonus sport IUT Saint-Nazaire sur moyenne générale
La note de sport de nos étudiants va de 0 à 5 points.
La note de culture idem,
Elles sont cumulables,
Chaque point correspond à un % qui augmente la moyenne générale.
Par exemple : note de sport 2/5 : la moyenne générale sera augmentée de 2%.
Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées.
"""
# les coefs sont ignorés
# notes de 0 à 5
points = sum([x for x in notes_sport])
factor = points / 100.0
bonus = infos["moy"] * factor
return bonus
def bonus_iutBordeaux1(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT Bordeaux 1, sur moyenne générale et UE
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université Bordeaux 1 (sport, théâtre) non rattachés à une unité d'enseignement.
En cas de double activité, c'est la meilleure des 2 notes qui compte.
Chaque point au-dessus de 10 sur 20 obtenus dans cet enseignement correspond à un %
qui augmente la moyenne de chaque UE et la moyenne générale.
Formule : le % = points>moyenne / 2
Par exemple : sport 13/20 : chaque UE sera multipliée par 1+0,015, ainsi que la moyenne générale.
Calcul ici du bonus sur moyenne générale et moyennes d'UE non capitalisées.
"""
# open('/tmp/log','a').write( '\n---------------\n' + pprint.pformat(infos) + '\n' )
# les coefs sont ignorés
# on récupère la note maximum et les points au-dessus de la moyenne
sport = max(notes_sport)
points = max(0, sport - 10)
# on calcule le bonus
factor = (points / 2.0) / 100.0
bonus = infos["moy"] * factor
# Modifie les moyennes de toutes les UE:
for ue_id in infos["moy_ues"]:
ue_status = infos["moy_ues"][ue_id]
if ue_status["sum_coefs"] > 0:
# modifie moyenne UE ds semestre courant
ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] * (1.0 + factor)
if not ue_status["is_capitalized"]:
# si non capitalisee, modifie moyenne prise en compte
ue_status["moy"] = ue_status["cur_moy_ue"]
# open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
return bonus
def bonus_iuto(notes_sport, coefs, infos=None): # OBSOLETE => EN ATTENTE (27/01/2022)
"""Calcul bonus modules optionels (sport, culture), règle IUT Orleans
* Avant aout 2013
Un bonus de 2,5% de la note de sport est accordé à chaque UE sauf
les UE de Projet et Stages
* Après aout 2013
Un bonus de 2,5% de la note de sport est accordé à la moyenne générale
"""
sumc = sum(coefs) # assumes sum. coefs > 0
note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée
bonus = note_sport * 2.5 / 100
if (
infos["sem"]["date_debut_iso"] > "2013-08-01"
): # changement de regle en aout 2013.
return bonus
coefs = 0.0
coefs_total = 0.0
for ue_id in infos["moy_ues"]:
ue_status = infos["moy_ues"][ue_id]
coefs_total = coefs_total + ue_status["sum_coefs"]
# Extremement spécifique (et n'est plus utilisé)
if ue_status["ue"]["ue_code"] not in {
"ORA14",
"ORA24",
"ORA34",
"ORA44",
"ORB34",
"ORB44",
"ORD42",
"ORE14",
"ORE25",
"ORN44",
"ORO44",
"ORP44",
"ORV34",
"ORV42",
"ORV43",
}:
if ue_status["sum_coefs"] > 0:
coefs = coefs + ue_status["sum_coefs"]
# modifie moyenne UE ds semestre courant
ue_status["cur_moy_ue"] = ue_status["cur_moy_ue"] + bonus
if not ue_status["is_capitalized"]:
# si non capitalisee, modifie moyenne prise en compte
ue_status["moy"] = ue_status["cur_moy_ue"]
return bonus * coefs / coefs_total
def bonus_iutbethune(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport), règle IUT Bethune
Les points au dessus de la moyenne de 10 apportent un bonus pour le semestre.
Ce bonus est égal au nombre de points divisé par 200 et multiplié par la
moyenne générale du semestre de l'étudiant.
"""
# les coefs sont ignorés
points = sum([x - 10 for x in notes_sport if x > 10])
points = min(10, points) # limite total à 10
bonus = int(infos["moy"] * points / 2) / 100.0 # moyenne-semestre x points x 0,5%
return bonus
def bonus_iutbeziers(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), regle IUT BEZIERS
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
sport , etc) non rattaches à une unité d'enseignement. Les points
au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 3% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
sumc = sum(coefs) # assumes sum. coefs > 0
# note_sport = sum(map(mul, notes_sport, coefs)) / sumc # moyenne pondérée
bonus = sum([(x - 10) * 0.03 for x in notes_sport if x > 10])
# le total du bonus ne doit pas dépasser 0.3 - Fred, 28/01/2020
if bonus > 0.3:
bonus = 0.3
return bonus
def bonus_iutlemans(notes_sport, coefs, infos=None):
"fake: formule inutilisée en ScoDoc 9.2 mais doiut être présente"
return 0.0
def bonus_iutlr(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point
Si la note de sport est comprise entre 10.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre
"""
# les coefs sont ignorés
# une seule note
note_sport = notes_sport[0]
if note_sport <= 10:
return 0
bonus = note_sport * 0.01 # 1%
return bonus
def bonus_demo(notes_sport, coefs, infos=None):
"""Fausse fonction "bonus" pour afficher les informations disponibles
et aider les développeurs.
Les informations sont placées dans le fichier /tmp/scodoc_bonus.log
qui est ECRASE à chaque appel.
*** Ne pas utiliser en production !!! ***
"""
with open("/tmp/scodoc_bonus.log", "w") as f: # mettre 'a' pour ajouter en fin
f.write("\n---------------\n" + pprint.pformat(infos) + "\n")
# Statut de chaque UE
# for ue_id in infos['moy_ues']:
# ue_status = infos['moy_ues'][ue_id]
# #open('/tmp/log','a').write( pprint.pformat(ue_status) + '\n\n' )
return 0.0

View File

@ -1,13 +1,11 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Configuration de ScoDoc (version ScoDOc 9) """Configuration de ScoDoc (version ScoDoc 9)
NE PAS MODIFIER localement ce fichier ! NE PAS MODIFIER localement ce fichier !
mais éditer /opt/scodoc-data/config/scodoc_local.py mais éditer /opt/scodoc-data/config/scodoc_local.py
""" """
from app.scodoc import bonus_sport
class AttrDict(dict): class AttrDict(dict):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -66,7 +66,8 @@ def formation_delete(formation_id=None, dialog_confirmed=False):
sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id}) sems = sco_formsemestre.do_formsemestre_list({"formation_id": formation_id})
if sems: if sems:
H.append( H.append(
"""<p class="warning">Impossible de supprimer cette formation, car les sessions suivantes l'utilisent:</p> """<p class="warning">Impossible de supprimer cette formation,
car les sessions suivantes l'utilisent:</p>
<ul>""" <ul>"""
) )
for sem in sems: for sem in sems:

View File

@ -33,7 +33,7 @@ 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 import log from app import db, log
from app import models from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
@ -421,7 +421,7 @@ def module_delete(module_id=None):
H = [ H = [
html_sco_header.sco_header(page_title="Suppression d'un module"), html_sco_header.sco_header(page_title="Suppression d'un module"),
f"""<h2>Suppression du module {module.titre} ({module.code})</h2>""", f"""<h2>Suppression du module {module.titre or "<em>sans titre</em>"} ({module.code})</h2>""",
] ]
dest_url = url_for( dest_url = url_for(
@ -848,21 +848,13 @@ def module_count_moduleimpls(module_id):
def formation_add_malus_modules(formation_id, titre=None, redirect=True): def formation_add_malus_modules(formation_id, titre=None, redirect=True):
"""Création d'un module de "malus" dans chaque UE d'une formation""" """Création d'un module de "malus" dans chaque UE d'une formation"""
from app.scodoc import sco_edit_ue
ues = sco_edit_ue.ue_list(args={"formation_id": formation_id}) formation = Formation.query.get_or_404(formation_id)
for ue in ues: for ue in formation.ues:
# Un seul module de malus par UE: ue_add_malus_module(ue, titre=titre)
nb_mod_malus = len(
[ formation.invalidate_cached_sems()
mod
for mod in module_list(args={"ue_id": ue["ue_id"]})
if mod["module_type"] == scu.ModuleType.MALUS
]
)
if nb_mod_malus == 0:
ue_add_malus_module(ue["ue_id"], titre=titre)
if redirect: if redirect:
return flask.redirect( return flask.redirect(
@ -872,20 +864,22 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
) )
def ue_add_malus_module(ue_id, titre=None, code=None): def ue_add_malus_module(ue: UniteEns, titre=None, code=None) -> int:
"""Add a malus module in this ue""" """Add a malus module in this ue.
from app.scodoc import sco_edit_ue If already exists, do nothing.
Returns id of malus module.
"""
modules_malus = [m for m in ue.modules if m.module_type == scu.ModuleType.MALUS]
if len(modules_malus) > 0:
return modules_malus[0].id # déjà existant
ue = sco_edit_ue.ue_list(args={"ue_id": ue_id})[0] titre = titre or ""
code = code or f"MALUS{ue.numero}"
if titre is None:
titre = ""
if code is None:
code = "MALUS%d" % ue["numero"]
# Tout module doit avoir un semestre_id (indice 1, 2, ...) # Tout module doit avoir un semestre_id (indice 1, 2, ...)
semestre_ids = sco_edit_ue.ue_list_semestre_ids(ue) if ue.semestre_idx is None:
if semestre_ids: semestre_ids = sorted(list(set([m.semestre_id for m in ue.modules])))
if len(semestre_ids) > 0:
semestre_id = semestre_ids[0] semestre_id = semestre_ids[0]
else: else:
# c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement # c'est ennuyeux: dans ce cas, on pourrait demander à indiquer explicitement
@ -893,25 +887,35 @@ def ue_add_malus_module(ue_id, titre=None, code=None):
raise ScoValueError( raise ScoValueError(
"Impossible d'ajouter un malus s'il n'y a pas d'autres modules" "Impossible d'ajouter un malus s'il n'y a pas d'autres modules"
) )
else:
semestre_id = ue.semestre_idx
# Matiere pour placer le module malus # Matiere pour placer le module malus
Matlist = sco_edit_matiere.matiere_list(args={"ue_id": ue_id}) titre_matiere_malus = "Malus"
numero = max([mat["numero"] for mat in Matlist]) + 10
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue_id, "titre": "Malus", "numero": numero}
)
module_id = do_module_create( matieres_malus = [mat for mat in ue.matieres if mat.titre == titre_matiere_malus]
{ if len(matieres_malus) > 0:
"titre": titre, # matière Malus déjà existante, l'utilise
"code": code, matiere = matieres_malus[0]
"coefficient": 0.0, # unused else:
"ue_id": ue_id, if ue.matieres.count() > 0:
"matiere_id": matiere_id, numero = max([mat.numero for mat in ue.matieres]) + 10
"formation_id": ue["formation_id"], else:
"semestre_id": semestre_id, numero = 0
"module_type": scu.ModuleType.MALUS, matiere = Matiere(ue_id=ue.id, titre=titre_matiere_malus, numero=numero)
}, db.session.add(matiere)
)
return module_id module = Module(
titre=titre,
code=code,
coefficient=0.0,
ue=ue,
matiere=matiere,
formation=ue.formation,
semestre_id=semestre_id,
module_type=scu.ModuleType.MALUS,
)
db.session.add(module)
db.session.commit()
return module.id

View File

@ -142,14 +142,6 @@ def do_ue_create(args):
return ue_id return ue_id
def can_delete_ue(ue: UniteEns) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (not len(ue.modules.all())) and not any(m.modimpls.all() for m in ue.modules)
def do_ue_delete(ue_id, delete_validations=False, force=False): def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)" "delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -158,9 +150,9 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
formation_id = ue.formation_id formation_id = ue.formation_id
semestre_idx = ue.semestre_idx semestre_idx = ue.semestre_idx
if not can_delete_ue(ue): if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
"UE", f"UE (id={ue.id}, dud)",
msg=ue.titre, msg=ue.titre,
dest_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
@ -540,9 +532,9 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
semestre_idx=ue.semestre_idx, semestre_idx=ue.semestre_idx,
), ),
) )
if not can_delete_ue(ue): if not ue.can_be_deleted():
raise ScoNonEmptyFormationObject( raise ScoNonEmptyFormationObject(
"UE", f"UE",
msg=ue.titre, msg=ue.titre,
dest_url=url_for( dest_url=url_for(
"notes.ue_table", "notes.ue_table",
@ -1354,16 +1346,6 @@ def ue_is_locked(ue_id):
return len(r) > 0 return len(r) > 0
def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
aussi ScoDoc laisse le choix.
"""
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in modules])))
UE_PALETTE = [ UE_PALETTE = [
"#B80004", # rouge "#B80004", # rouge
"#F97B3D", # Orange Crayola "#F97B3D", # Orange Crayola

View File

@ -606,12 +606,10 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
# -------------- VIEWS # -------------- VIEWS
def evaluation_describe(evaluation_id="", edit_in_place=True): def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True):
"""HTML description of evaluation, for page headers """HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented) edit_in_place: allow in-place editing when permitted (not implemented)
""" """
from app.scodoc import sco_saisie_notes
E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0] E = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})[0]
moduleimpl_id = E["moduleimpl_id"] moduleimpl_id = E["moduleimpl_id"]
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
@ -646,7 +644,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
if Mod["module_type"] == ModuleType.MALUS: if Mod["module_type"] == ModuleType.MALUS:
etit += ' <span class="eval_malus">(points de malus)</span>' etit += ' <span class="eval_malus">(points de malus)</span>'
H = [ H = [
'<span class="eval_title">Evaluation%s</span><p><b>Module : %s</b></p>' '<span class="eval_title">Évaluation%s</span><p><b>Module : %s</b></p>'
% (etit, mod_descr) % (etit, mod_descr)
] ]
if Mod["module_type"] == ModuleType.MALUS: if Mod["module_type"] == ModuleType.MALUS:
@ -689,7 +687,11 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
<a class="stdlink" href="{url_for( <a class="stdlink" href="{url_for(
"notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id) "notes.evaluation_edit", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
}">modifier l'évaluation</a> }">modifier l'évaluation</a>
"""
)
if link_saisie:
H.append(
f"""
<a class="stdlink" href="{url_for( <a class="stdlink" href="{url_for(
"notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id) "notes.saisie_notes", scodoc_dept=g.scodoc_dept, evaluation_id=evaluation_id)
}">saisie des notes</a> }">saisie des notes</a>

View File

@ -1046,9 +1046,7 @@ def partition_set_attr(partition_id, attr, value):
partition[attr] = value partition[attr] = value
partitionEditor.edit(cnx, partition) partitionEditor.edit(cnx, partition)
# invalid bulletin cache # invalid bulletin cache
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(formsemestre_id=partition["formsemestre_id"])
pdfonly=True, formsemestre_id=partition["formsemestre_id"]
)
return "enregistré" return "enregistré"

View File

@ -277,7 +277,11 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if modimpl.module.is_apc(): if modimpl.module.is_apc():
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list())) H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
else: else:
H.append(f"Coef. dans le semestre: {modimpl.module.coefficient}") H.append(
f"""Coef. dans le semestre: {
"non défini" if modimpl.module.coefficient is None else modimpl.module.coefficient
}"""
)
H.append("""</td><td></td></tr>""") H.append("""</td><td></td></tr>""")
# 3ieme ligne: Formation # 3ieme ligne: Formation
H.append( H.append(

View File

@ -50,10 +50,10 @@ _SCO_PERMISSIONS = (
(1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"), (1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"),
# 27 à 39 ... réservé pour "entreprises" # 27 à 39 ... réservé pour "entreprises"
# Api scodoc9 # Api scodoc9
(1 << 40, "APIView", "Voir"), (1 << 40, "APIView", "API: Lecture"),
(1 << 41, "APIEtudChangeGroups", "Modifier les groupes"), (1 << 41, "APIEtudChangeGroups", "API: Modifier les groupes"),
(1 << 42, "APIEditAllNotes", "Modifier toutes les notes"), (1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"),
(1 << 43, "APIAbsChange", "Saisir des absences"), (1 << 43, "APIAbsChange", "API: Saisir des absences"),
) )

View File

@ -943,7 +943,9 @@ def saisie_notes(evaluation_id, group_ids=[]):
cssstyles=sco_groups_view.CSSSTYLES, cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True, init_qtip=True,
), ),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id), sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, link_saisie=False
),
'<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>', '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
] ]
H.append("""<div id="group-tabs"><table><tr><td>""") H.append("""<div id="group-tabs"><table><tr><td>""")

View File

@ -56,6 +56,7 @@ Solution proposée (nov 2014):
import flask import flask
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from app.models.formsemestre import FormSemestre
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
@ -65,7 +66,6 @@ from app.scodoc import sco_codes_parcours
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
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
@ -85,17 +85,21 @@ def external_ue_create(
ects=0.0, ects=0.0,
): ):
"""Crée UE/matiere/module/evaluation puis saisie les notes""" """Crée UE/matiere/module/evaluation puis saisie les notes"""
log("external_ue_create( formsemestre_id=%s, titre=%s )" % (formsemestre_id, titre)) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) log(f"creating external UE in {formsemestre}: {acronyme}")
# Contrôle d'accès: # Contrôle d'accès:
if not current_user.has_permission(Permission.ScoImplement): if not current_user.has_permission(Permission.ScoImplement):
if not sem["resp_can_edit"] or (current_user.id not in sem["responsables"]): if (not formsemestre.resp_can_edit) or (
current_user.id not in [u.id for u in formsemestre.responsables]
):
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
# #
formation_id = sem["formation_id"] formation_id = formsemestre.formation.id
log("creating external UE in %s: %s" % (formsemestre_id, acronyme))
numero = sco_edit_ue.next_ue_numero(formation_id, semestre_id=sem["semestre_id"]) numero = sco_edit_ue.next_ue_numero(
formation_id, semestre_id=formsemestre.semestre_id
)
ue_id = sco_edit_ue.do_ue_create( ue_id = sco_edit_ue.do_ue_create(
{ {
"formation_id": formation_id, "formation_id": formation_id,
@ -120,7 +124,8 @@ def external_ue_create(
"ue_id": ue_id, "ue_id": ue_id,
"matiere_id": matiere_id, "matiere_id": matiere_id,
"formation_id": formation_id, "formation_id": formation_id,
"semestre_id": sem["semestre_id"], "semestre_id": formsemestre.semestre_id,
"module_type": scu.ModuleType.STANDARD,
}, },
) )
@ -129,17 +134,23 @@ def external_ue_create(
"module_id": module_id, "module_id": module_id,
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
# affecte le 1er responsable du semestre comme resp. du module # affecte le 1er responsable du semestre comme resp. du module
"responsable_id": sem["responsables"][0], "responsable_id": formsemestre.responsables[0].id
if len(formsemestre.responsables)
else None,
}, },
) )
return moduleimpl_id return moduleimpl_id
def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds): def external_ue_inscrit_et_note(
moduleimpl_id: int, formsemestre_id: int, notes_etuds: dict
):
"""Inscrit les étudiants au moduleimpl, crée au besoin une évaluation
et enregistre les notes.
"""
log( log(
"external_ue_inscrit_et_note(moduleimpl_id=%s, notes_etuds=%s)" f"external_ue_inscrit_et_note(moduleimpl_id={moduleimpl_id}, notes_etuds={notes_etuds})"
% (moduleimpl_id, notes_etuds)
) )
# Inscription des étudiants # Inscription des étudiants
sco_moduleimpl.do_moduleimpl_inscrit_etuds( sco_moduleimpl.do_moduleimpl_inscrit_etuds(
@ -175,17 +186,17 @@ def external_ue_inscrit_et_note(moduleimpl_id, formsemestre_id, notes_etuds):
) )
def get_existing_external_ue(formation_id): def get_existing_external_ue(formation_id: int) -> list[dict]:
"la liste de toutes les UE externes définies dans cette formation" "Liste de toutes les UE externes définies dans cette formation"
return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True}) return sco_edit_ue.ue_list(args={"formation_id": formation_id, "is_external": True})
def get_external_moduleimpl_id(formsemestre_id, ue_id): def get_external_moduleimpl_id(formsemestre_id: int, ue_id: int) -> int:
"moduleimpl correspondant à l'UE externe indiquée de ce formsemestre" "moduleimpl correspondant à l'UE externe indiquée de ce formsemestre"
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
""" """
SELECT mi.id AS moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo SELECT mi.id AS moduleimpl_id FROM notes_moduleimpl mi, notes_modules mo
WHERE mi.id = %(formsemestre_id)s WHERE mi.formsemestre_id = %(formsemestre_id)s
AND mi.module_id = mo.id AND mi.module_id = mo.id
AND mo.ue_id = %(ue_id)s AND mo.ue_id = %(ue_id)s
""", """,
@ -194,11 +205,14 @@ def get_external_moduleimpl_id(formsemestre_id, ue_id):
if r: if r:
return r[0]["moduleimpl_id"] return r[0]["moduleimpl_id"]
else: else:
raise ScoValueError("aucun module externe ne correspond") raise ScoValueError(
f"""Aucun module externe ne correspond
(formsemestre_id={formsemestre_id}, ue_id={ue_id})"""
)
# Web function # Web function
def external_ue_create_form(formsemestre_id, etudid): def external_ue_create_form(formsemestre_id: int, etudid: int):
"""Formulaire création UE externe + inscription étudiant et saisie note """Formulaire création UE externe + inscription étudiant et saisie note
- Demande UE: peut-être existante (liste les UE externes de cette formation), - Demande UE: peut-être existante (liste les UE externes de cette formation),
ou sinon spécifier titre, acronyme, type, ECTS ou sinon spécifier titre, acronyme, type, ECTS
@ -233,7 +247,9 @@ def external_ue_create_form(formsemestre_id, etudid):
html_footer = html_sco_header.sco_footer() html_footer = html_sco_header.sco_footer()
Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0] Fo = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(Fo["type_parcours"])
ue_types = parcours.ALLOWED_UE_TYPES ue_types = [
typ for typ in parcours.ALLOWED_UE_TYPES if typ != sco_codes_parcours.UE_SPORT
]
ue_types.sort() ue_types.sort()
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types] ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x in ue_types] ue_types = [str(x) for x in ue_types]
@ -255,7 +271,7 @@ def external_ue_create_form(formsemestre_id, etudid):
"input_type": "menu", "input_type": "menu",
"title": "UE externe existante:", "title": "UE externe existante:",
"allowed_values": [""] "allowed_values": [""]
+ [ue["ue_id"] for ue in existing_external_ue], + [str(ue["ue_id"]) for ue in existing_external_ue],
"labels": [default_label] "labels": [default_label]
+ [ + [
"%s (%s)" % (ue["titre"], ue["acronyme"]) "%s (%s)" % (ue["titre"], ue["acronyme"])
@ -337,7 +353,7 @@ def external_ue_create_form(formsemestre_id, etudid):
+ html_footer + html_footer
) )
if tf[2]["existing_ue"]: if tf[2]["existing_ue"]:
ue_id = tf[2]["existing_ue"] ue_id = int(tf[2]["existing_ue"])
moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id) moduleimpl_id = get_external_moduleimpl_id(formsemestre_id, ue_id)
else: else:
acronyme = tf[2]["acronyme"].strip() acronyme = tf[2]["acronyme"].strip()

View File

@ -10,18 +10,21 @@ function toggle_new_ue_form(state) {
} }
$("#tf_extue_titre td:eq(1) input").prop("disabled", state); $("#tf_extue_titre td:eq(1) input").prop("disabled", state);
$("#tf_extue_titre td:eq(1) input").css('color', text_color) $("#tf_extue_titre").css('color', text_color)
$("#tf_extue_acronyme td:eq(1) input").prop("disabled", state); $("#tf_extue_acronyme td:eq(1) input").prop("disabled", state);
$("#tf_extue_acronyme td:eq(1) input").css('color', text_color) $("#tf_extue_acronyme").css('color', text_color)
$("#tf_extue_type td:eq(1) select").prop("disabled", state);
$("#tf_extue_type").css('color', text_color)
$("#tf_extue_ects td:eq(1) input").prop("disabled", state); $("#tf_extue_ects td:eq(1) input").prop("disabled", state);
$("#tf_extue_ects td:eq(1) input").css('color', text_color) $("#tf_extue_ects").css('color', text_color)
} }
function update_external_ue_form() { function update_external_ue_form() {
var state = (tf.existing_ue.value != "") var state = (tf.existing_ue.value != "");
toggle_new_ue_form(state); toggle_new_ue_form(state);
} }

View File

@ -15,13 +15,23 @@ $(function () {
}, },
{ {
name: "toggle_partitions", name: "toggle_partitions",
text: "Toutes les partitions", text: "Montrer groupes",
action: function (e, dt, node, config) { action: function (e, dt, node, config) {
let visible = dt.columns(".partition_aux").visible()[0]; let visible = dt.columns(".partition_aux").visible()[0];
dt.columns(".partition_aux").visible(!visible); dt.columns(".partition_aux").visible(!visible);
dt.buttons('toggle_partitions:name').text(visible ? "Toutes les partitions" : "Cacher les partitions"); dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes");
} }
}]; },
{
name: "toggle_partitions_rangs",
text: "Rangs groupes",
action: function (e, dt, node, config) {
let rangs_visible = dt.columns(".partition_rangs").visible()[0];
dt.columns(".partition_rangs").visible(!rangs_visible);
dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes");
}
},
];
if (!$('table.table_recap').hasClass("jury")) { if (!$('table.table_recap').hasClass("jury")) {
buttons.push( buttons.push(
$('table.table_recap').hasClass("apc") ? $('table.table_recap').hasClass("apc") ?
@ -95,7 +105,7 @@ $(function () {
"columnDefs": [ "columnDefs": [
{ {
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides // cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
targets: ["codes", "identite_detail", "partition_aux", "admission", "col_empty"], targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"],
visible: false, visible: false,
}, },
{ {

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.2.22" SCOVERSION = "9.2.24"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -21,6 +21,12 @@ import requests
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
# Etudiant pour les tests # Etudiant pour les tests
from tests.api.tools_test_api import (
verify_fields,
ABSENCES_FIELDS,
ABSENCES_GROUP_ETAT_FIELDS,
)
ETUDID = 1 ETUDID = 1
@ -29,10 +35,8 @@ def test_absences(api_headers):
""" """
Test 'absences' Test 'absences'
Routes : Route :
- /absences/etudid/<int:etudid> - /absences/etudid/<int:etudid>
- /absences/nip/<int:nip>
- /absences/ine/<int:ine>
""" """
r = requests.get( r = requests.get(
f"{API_URL}/absences/etudid/{ETUDID}", f"{API_URL}/absences/etudid/{ETUDID}",
@ -40,6 +44,20 @@ def test_absences(api_headers):
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
absences = r.json()
assert isinstance(absences, list)
for abs in absences:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
# absences_justify # absences_justify
@ -47,32 +65,144 @@ def test_absences_justify(api_headers):
""" """
Test 'absences_just' Test 'absences_just'
Routes : Route :
- /absences/etudid/<int:etudid>/just - /absences/etudid/<int:etudid>/just
- /absences/nip/<int:nip>/just
- /absences/ine/<int:ine>/just
""" """
r = requests.get( r = requests.get(
API_URL + f"/absences/etudid/{ETUDID}/just", f"{API_URL}/absences/etudid/{ETUDID}/just",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# TODO vérifier résultat absences = r.json()
assert isinstance(absences, list)
for abs in absences:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
def test_abs_groupe_etat(api_headers):
"""
Test 'abs_groupe_etat'
Routes :
- /absences/abs_group_etat/<int:group_id>
- /absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>
"""
group_id = 1
r = requests.get(
f"{API_URL}/absences/abs_group_etat/{group_id}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
list_absences = r.json()
assert isinstance(list_absences, list)
list_id_etu = []
for etu in list_absences:
list_id_etu.append(etu["etudid"])
assert verify_fields(etu, ABSENCES_GROUP_ETAT_FIELDS) is True
assert isinstance(etu["etudid"], int)
assert isinstance(etu["list_abs"], list)
list_abs = etu["list_abs"]
for abs in list_abs:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
all_unique = True
for id in list_id_etu:
if list_id_etu.count(id) > 1:
all_unique = False
assert all_unique is True
date_debut = "Fri, 15 Apr 2021 00:00:00 GMT"
date_fin = "Fri, 18 Apr 2022 00:00:00 GMT"
r1 = requests.get(
f"{API_URL}/absences/abs_group_etat/group_id/{group_id}/date_debut/{date_debut}/date_fin/{date_fin}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r1.status_code == 200
list_absences1 = r.json()
assert isinstance(list_absences1, list)
list_id_etu1 = []
for etu in list_absences1:
list_id_etu1.append(etu["etudid"])
assert verify_fields(etu, ABSENCES_GROUP_ETAT_FIELDS) is True
assert isinstance(etu["etudid"], int)
assert isinstance(etu["list_abs"], list)
list_abs1 = etu["list_abs"]
for abs in list_abs1:
assert verify_fields(abs, ABSENCES_FIELDS) is True
assert isinstance(abs["jour"], str)
assert isinstance(abs["matin"], bool)
assert isinstance(abs["estabs"], bool)
assert isinstance(abs["estjust"], bool)
assert isinstance(abs["description"], str)
assert isinstance(abs["begin"], str)
assert isinstance(abs["end"], str)
assert abs["begin"] < abs["end"]
all_unique1 = True
for id in list_id_etu1:
if list_id_etu1.count(id) > 1:
all_unique1 = False
assert all_unique1 is True
# XXX TODO # XXX TODO
# def test_abs_groupe_etat(api_headers): # def reset_etud_abs(api_headers):
# """ # """
# Test 'abs_groupe_etat' # Test 'abs_groupe_etat'
# #
# Routes : # Routes :
# - /absences/abs_group_etat/<int:group_id> # - /absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs
# - /absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin> # - /absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs/only_not_just
# - /absences/etudid/<int:etudid>/list_abs/<string:list_abs>/reset_etud_abs/only_just
# """ # """
# list_abs = []
# r = requests.get( # r = requests.get(
# API_URL + "/absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/" # f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs",
# "date_fin/<string:date_fin>", # headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
#
# r_only_not_just = requests.get(
# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs/only_not_just",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
#
#
# r_only_just = requests.get(
# f"{API_URL}/absences/etudid/{ETUDID}/list_abs/{list_abs}/reset_etud_abs/only_just",
# headers=api_headers, # headers=api_headers,
# verify=CHECK_CERTIFICATE, # verify=CHECK_CERTIFICATE,
# ) # )

View File

@ -20,7 +20,15 @@ Utilisation :
import requests import requests
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers
from tests.api.tools_test_api import verify_fields from tests.api.tools_test_api import (
verify_fields,
FORMATION_EXPORT_FIELDS,
FORMATION_EXPORT_UE_FIELDS,
FORMATION_EXPORT_UE_MATIERE_FIELDS,
FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS,
FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS,
MODULE_FIELDS,
)
from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS from tests.api.tools_test_api import FORMATION_FIELDS, MODIMPL_FIELDS
@ -45,15 +53,52 @@ def test_formations_by_id(api_headers):
""" """
Route: /formation/<int:formation_id> Route: /formation/<int:formation_id>
""" """
id_formation = 1
r = requests.get( r = requests.get(
API_URL + "/formation/1", f"{API_URL}/formation/{id_formation}",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
formation = r.json() formation = r.json()
assert verify_fields(formation, FORMATION_FIELDS) is True assert verify_fields(formation, FORMATION_FIELDS) is True
# TODO tester le contenu de certains champs assert isinstance(formation["dept_id"], int)
assert isinstance(formation["acronyme"], str)
assert isinstance(formation["titre_officiel"], str)
assert isinstance(formation["formation_code"], str)
assert formation["code_specialite"] is None or isinstance(
formation["code_specialite"], str
)
assert isinstance(formation["id"], int)
assert isinstance(formation["titre"], str)
assert isinstance(formation["version"], int)
assert isinstance(formation["type_parcours"], int)
assert formation["referentiel_competence_id"] is None or isinstance(
formation["referentiel_competence_id"], int
)
assert isinstance(formation["formation_id"], int)
assert id_formation == formation["formation_id"]
assert id_formation == formation["id"]
r1 = requests.get(
f"{API_URL}/formation/{formation['formation_id']}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r1.status_code == 200
formation1 = r1.json()
assert formation == formation1
# ERROR
id_formation_inexistant = 1516476846861656351
r_error = requests.get(
f"{API_URL}/formation/{id_formation_inexistant}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r_error.status_code == 404
def test_formation_export(api_headers): def test_formation_export(api_headers):
@ -67,33 +112,150 @@ def test_formation_export(api_headers):
) )
assert r.status_code == 200 assert r.status_code == 200
export_formation = r.json() export_formation = r.json()
assert verify_fields(export_formation, FORMATION_FIELDS) is True assert verify_fields(export_formation, FORMATION_EXPORT_FIELDS) is True
# TODO tester le contenu de certains champs assert isinstance(export_formation["dept_id"], int)
assert isinstance(export_formation["acronyme"], str)
assert isinstance(export_formation["titre_officiel"], str)
assert isinstance(export_formation["formation_code"], str)
assert export_formation["code_specialite"] is None or isinstance(
export_formation["code_specialite"], str
)
assert isinstance(export_formation["id"], int)
assert isinstance(export_formation["titre"], str)
assert isinstance(export_formation["version"], int)
assert isinstance(export_formation["type_parcours"], int)
assert export_formation["referentiel_competence_id"] is None or isinstance(
export_formation["referentiel_competence_id"], int
)
assert isinstance(export_formation["formation_id"], int)
assert isinstance(export_formation["ue"], list)
ues = export_formation["ue"]
# TODO for ue in ues:
# def test_formsemestre_apo(api_headers): assert verify_fields(ue, FORMATION_EXPORT_UE_FIELDS) is True
# r = requests.get( assert isinstance(ue["acronyme"], str)
# API_URL + "/formation/apo/<string:etape_apo>", assert isinstance(ue["numero"], int)
# headers=api_headers, assert isinstance(ue["titre"], str)
# verify=CHECK_CERTIFICATE, assert isinstance(ue["type"], int)
# ) assert isinstance(ue["ue_code"], str)
# assert r.status_code == 200 assert isinstance(ue["ects"], float)
assert isinstance(ue["is_external"], bool)
assert isinstance(ue["code_apogee"], str)
assert isinstance(ue["coefficient"], float)
assert isinstance(ue["semestre_idx"], int)
assert isinstance(ue["color"], str)
assert isinstance(ue["reference"], int)
assert isinstance(ue["matiere"], list)
matieres = ue["matiere"]
for matiere in matieres:
assert verify_fields(matiere, FORMATION_EXPORT_UE_MATIERE_FIELDS)
assert isinstance(matiere["titre"], str)
assert isinstance(matiere["numero"], int)
assert isinstance(matiere["module"], list)
modules = matiere["module"]
for module in modules:
assert verify_fields(module, FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS)
assert isinstance(module["titre"], str)
assert isinstance(module["abbrev"], str)
assert isinstance(module["code"], str)
assert isinstance(module["heures_cours"], float)
assert isinstance(module["heures_td"], float)
assert isinstance(module["heures_tp"], float)
assert isinstance(module["coefficient"], float)
assert isinstance(module["ects"], str)
assert isinstance(module["semestre_id"], int)
assert isinstance(module["numero"], int)
assert isinstance(module["code_apogee"], str)
assert isinstance(module["module_type"], int)
assert isinstance(module["coefficients"], list)
coefficients = module["coefficients"]
for coef in coefficients:
assert verify_fields(
coef, FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS
)
assert isinstance(coef["ue_reference"], str)
assert isinstance(coef["coef"], str)
# ERROR
id_formation_inexistant = 1516476846861656351
r_error = requests.get(
f"{API_URL}/formation/formation_export/{id_formation_inexistant}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r_error.status_code == 404
def test_moduleimpl(api_headers): def test_moduleimpl(api_headers):
""" """
Route: /formation/moduleimpl/<int:moduleimpl_id> Route: /formation/moduleimpl/<int:moduleimpl_id>
""" """
moduleimpl_id = 1
r = requests.get( r = requests.get(
API_URL + "/formation/moduleimpl/1", f"{API_URL}/formation/moduleimpl/{moduleimpl_id}",
headers=api_headers, headers=api_headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
moduleimpl = r.json() moduleimpl = r.json()
assert verify_fields(moduleimpl, MODIMPL_FIELDS) is True assert verify_fields(moduleimpl, MODIMPL_FIELDS) is True
# TODO tester le contenu de certains champs assert isinstance(moduleimpl["id"], int)
assert isinstance(moduleimpl["responsable_id"], int)
assert isinstance(moduleimpl["module_id"], int)
assert isinstance(moduleimpl["formsemestre_id"], int)
assert moduleimpl["computation_expr"] is None or isinstance(
moduleimpl["computation_expr"], str
)
assert isinstance(moduleimpl["moduleimpl_id"], int)
assert isinstance(moduleimpl["ens"], list)
assert isinstance(moduleimpl["module"], dict)
module = moduleimpl["module"]
assert verify_fields(module, MODULE_FIELDS)
assert isinstance(module["heures_cours"], float)
assert isinstance(module["semestre_id"], int)
assert isinstance(module["heures_td"], float)
assert isinstance(module["numero"], int)
assert isinstance(module["heures_tp"], float)
assert isinstance(module["code_apogee"], str)
assert isinstance(module["titre"], str)
assert isinstance(module["coefficient"], float)
assert isinstance(module["module_type"], int)
assert isinstance(module["id"], int)
assert module["ects"] is None or isinstance(module["ects"], str)
assert isinstance(module["abbrev"], str)
assert isinstance(module["ue_id"], int)
assert isinstance(module["code"], str)
assert isinstance(module["formation_id"], int)
assert isinstance(module["matiere_id"], int)
assert isinstance(module["module_id"], int)
assert moduleimpl_id == moduleimpl["id"]
assert moduleimpl_id == moduleimpl["moduleimpl_id"]
r1 = requests.get(
f"{API_URL}/formation/moduleimpl/{moduleimpl['moduleimpl_id']}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r1.status_code == 200
moduleimpl1 = r1.json()
assert moduleimpl == moduleimpl1
# ERROR
id_formation_inexistant = 1516476846861656351
r_error = requests.get(
f"{API_URL}/formation/moduleimpl/{id_formation_inexistant}",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r_error.status_code == 404
def test_referentiel_competences(api_headers): def test_referentiel_competences(api_headers):
@ -106,4 +268,4 @@ def test_referentiel_competences(api_headers):
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
) )
assert r.status_code == 200 assert r.status_code == 200
# XXX A compléter # XXX TODO ajouter un referentiel competence dans la base de test

View File

@ -49,44 +49,19 @@ def test_formsemestre(api_headers):
assert verify_fields(formsemestre, FSEM_FIELDS) assert verify_fields(formsemestre, FSEM_FIELDS)
def test_etudiant_bulletin(api_headers): # XXX TODO ajouter une etape_apo dans la base de test
""" # def test_formsemestre_apo(api_headers):
Route: # r = requests.get(
""" # API_URL + "/formation/apo/<string:etape_apo>",
formsemestre_id = 1 # headers=api_headers,
r = requests.get( # verify=CHECK_CERTIFICATE,
f"{API_URL}/etudiant/etudid/1/formsemestre/{formsemestre_id}/bulletin", # )
headers=api_headers, # assert r.status_code == 200
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
bull_a = r.json()
r = requests.get(
f"{API_URL}/etudiant/nip/{NIP}/formsemestre/{formsemestre_id}/bulletin",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
bull_b = r.json()
r = requests.get(
f"{API_URL}/etudiant/ine/{INE}/formsemestre/{formsemestre_id}/bulletin",
headers=api_headers,
verify=CHECK_CERTIFICATE,
)
assert r.status_code == 200
bull_c = r.json()
# elimine les dates de publication pour comparer les autres champs
del bull_a["date"]
del bull_b["date"]
del bull_c["date"]
assert bull_a == bull_b == bull_c
def test_bulletins(api_headers): def test_bulletins(api_headers):
""" """
Route: Route: /formsemestre/<int:formsemestre_id>/bulletins
""" """
r = requests.get( r = requests.get(
API_URL + "/formsemestre/1/bulletins", API_URL + "/formsemestre/1/bulletins",
@ -106,6 +81,32 @@ def test_bulletins(api_headers):
# assert r.status_code == 200 # assert r.status_code == 200
# def test_formsemestre_etudiants(api_headers):
# """
# Route: /formsemestre/<int:formsemestre_id>/etudiants, /formsemestre/<int:formsemestre_id>/etudiants/demissionnaires, /formsemestre/<int:formsemestre_id>/etudiants/defaillants
# """
# r = requests.get(
# API_URL + "/formsemestre/<int:formsemestre_id>/etudiants",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
#
# r = requests.get(
# API_URL + "/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
#
# r = requests.get(
# API_URL + "/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
def test_formsemestre_programme(api_headers): def test_formsemestre_programme(api_headers):
""" """
Route: /formsemestre/1/programme Route: /formsemestre/1/programme
@ -138,32 +139,6 @@ def test_formsemestre_programme(api_headers):
assert verify_fields(sae, MODIMPL_FIELDS) assert verify_fields(sae, MODIMPL_FIELDS)
# def test_formsemestre_etudiants(api_headers):
# """
# Route: /formsemestre/<int:formsemestre_id>/etudiants, /formsemestre/<int:formsemestre_id>/etudiants/demissionnaires, /formsemestre/<int:formsemestre_id>/etudiants/defaillants
# """
# r = requests.get(
# API_URL + "/formsemestre/<int:formsemestre_id>/etudiants",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
#
# r = requests.get(
# API_URL + "/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
#
# r = requests.get(
# API_URL + "/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
# headers=api_headers,
# verify=CHECK_CERTIFICATE,
# )
# assert r.status_code == 200
def test_etat_evals( def test_etat_evals(
api_headers, api_headers,
): ):

View File

@ -78,12 +78,12 @@ ETUD_FIELDS = {
} }
FORMATION_FIELDS = { FORMATION_FIELDS = {
"id", "dept_id",
"acronyme", "acronyme",
"titre_officiel", "titre_officiel",
"formation_code", "formation_code",
"code_specialite", "code_specialite",
"dept_id", "id",
"titre", "titre",
"version", "version",
"type_parcours", "type_parcours",
@ -91,6 +91,63 @@ FORMATION_FIELDS = {
"formation_id", "formation_id",
} }
FORMATION_EXPORT_FIELDS = {
"dept_id",
"acronyme",
"titre_officiel",
"formation_code",
"code_specialite",
"id",
"titre",
"version",
"type_parcours",
"referentiel_competence_id",
"formation_id",
"ue",
}
FORMATION_EXPORT_UE_FIELDS = {
"acronyme",
"numero",
"titre",
"type",
"ue_code",
"ects",
"is_external",
"code_apogee",
"coefficient",
"semestre_idx",
"color",
"reference",
"matiere",
}
FORMATION_EXPORT_UE_MATIERE_FIELDS = {
"titre",
"numero",
"module",
}
FORMATION_EXPORT_UE_MATIERE_MODULE_FIELDS = {
"titre",
"abbrev",
"code",
"heures_cours",
"heures_td",
"coefficient",
"ects",
"semestre_id",
"numero",
"code_apogee",
"module_type",
"coefficients",
}
FORMATION_EXPORT_UE_MATIERE_MODULE_COEF_FIELDS = {
"ue_reference",
"coef",
}
FORMSEMESTRE_FIELDS = [ FORMSEMESTRE_FIELDS = [
"titre", "titre",
"gestion_semestrielle", "gestion_semestrielle",
@ -432,3 +489,33 @@ BULLETIN_SEMESTRE_ECTS_FIELDS = {"acquis", "total"}
BULLETIN_SEMESTRE_NOTES_FIELDS = {"value", "min", "moy", "max"} BULLETIN_SEMESTRE_NOTES_FIELDS = {"value", "min", "moy", "max"}
BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"} BULLETIN_SEMESTRE_RANG_FIELDS = {"value", "total"}
EVAL_FIELDS = {
"id",
"description",
"datetime_epreuve",
"heure_fin",
"coefficient",
"comptee",
"inscrits",
"manquantes",
"ABS",
"ATT",
"EXC",
"saisie_notes",
}
SAISIE_NOTES_FIELDS = {"datetime_debut", "datetime_fin", "datetime_mediane"}
ABSENCES_FIELDS = {
"jour",
"matin",
"estabs",
"estjust",
"description",
"begin",
"end",
}
ABSENCES_GROUP_ETAT_FIELDS = {"etudid", "list_abs"}