Merge branch 'refactor_nt' of https://scodoc.org/git/ScoDoc/ScoDoc into entreprises

This commit is contained in:
Arthur ZHU 2022-01-31 14:22:04 +01:00
commit 2207d25e35
81 changed files with 2926 additions and 1094 deletions

View File

@ -18,12 +18,12 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (4 dec 21)
### État actuel (26 jan 22)
- 9.0 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- ancien module "Entreprises" (obsolète)
- 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
- 9.1 (branche "PNBUT") est la version de développement.
- 9.2 (branche refactor_nt) est la version de développement.
### Lignes de commandes

View File

@ -253,7 +253,7 @@ def create_app(config_class=DevConfig):
host_name = socket.gethostname()
mail_handler = ScoSMTPHandler(
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
fromaddr="no-reply@" + app.config["MAIL_SERVER"],
fromaddr=app.config["SCODOC_MAIL_FROM"],
toaddrs=["exception@scodoc.org"],
subject="ScoDoc Exception", # unused see ScoSMTPHandler
credentials=auth,

View File

@ -8,7 +8,7 @@ def send_password_reset_email(user):
token = user.get_reset_password_token()
send_email(
"[ScoDoc] Réinitialisation de votre mot de passe",
sender=current_app.config["ADMINS"][0],
sender=current_app.config["SCODOC_MAIL_FROM"],
recipients=[user.email],
text_body=render_template("email/reset_password.txt", user=user, token=token),
html_body=render_template("email/reset_password.html", user=user, token=token),

View File

@ -112,6 +112,7 @@ class User(UserMixin, db.Model):
self.password_hash = generate_password_hash(password)
else:
self.password_hash = None
self.passwd_temp = False
def check_password(self, password):
"""Check given password vs current one.

View File

@ -13,6 +13,7 @@ from flask import url_for, g
from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import fmt_note
from app.comp.res_but import ResultatsSemestreBUT
@ -27,45 +28,71 @@ class BulletinBUT(ResultatsSemestreBUT):
"dict synthèse résultats dans l'UE pour les modules indiqués"
d = {}
etud_idx = self.etud_index[etud.id]
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
if ue.type != UE_SPORT:
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
etud_moy_module = self.sem_cube[etud_idx] # module x UE
for modimpl in modimpls:
if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
if coef > 0:
d[modimpl.module.code] = {
"id": modimpl.id,
"coef": coef,
"moyenne": fmt_note(
etud_moy_module[
self.modimpl_coefs_df.columns.get_loc(modimpl.id)
][ue_idx]
),
}
if self.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
if ue.type != UE_SPORT:
coef = self.modimpl_coefs_df[modimpl.id][ue.id]
if coef > 0:
d[modimpl.module.code] = {
"id": modimpl.id,
"coef": coef,
"moyenne": fmt_note(
etud_moy_module[
self.modimpl_coefs_df.columns.get_loc(modimpl.id)
][ue_idx]
),
}
# else: # modules dans UE bonus sport
# d[modimpl.module.code] = {
# "id": modimpl.id,
# "coef": "",
# "moyenne": "?x?",
# }
return d
def etud_ue_results(self, etud, ue):
"dict synthèse résultats UE"
d = {
"id": ue.id,
"titre": ue.titre,
"numero": ue.numero,
"type": ue.type,
"ECTS": {
"acquis": 0, # XXX TODO voir jury
"total": ue.ects,
},
"color": ue.color,
"competence": None, # XXX TODO lien avec référentiel
"moyenne": {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
},
"bonus": None, # XXX TODO
"moyenne": None,
# Le bonus sport appliqué sur cette UE
"bonus": fmt_note(self.bonus_ues[ue.id][etud.id])
if self.bonus_ues is not None and ue.id in self.bonus_ues
else fmt_note(0.0),
"malus": None, # XXX TODO voir ce qui est ici
"capitalise": None, # "AAAA-MM-JJ" TODO
"ressources": self.etud_ue_mod_results(etud, ue, self.ressources),
"saes": self.etud_ue_mod_results(etud, ue, self.saes),
}
if ue.type != UE_SPORT:
d["moyenne"] = {
"value": fmt_note(self.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(self.etud_moy_ue[ue.id].min()),
"max": fmt_note(self.etud_moy_ue[ue.id].max()),
"moy": fmt_note(self.etud_moy_ue[ue.id].mean()),
}
else:
# ceci suppose que l'on a une seule UE bonus,
# en tous cas elles auront la même description
d["bonus_description"] = self.etud_bonus_description(etud.id)
modimpls_spo = [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.type == UE_SPORT
]
d["modules"] = self.etud_mods_results(etud, modimpls_spo)
return d
def etud_mods_results(self, etud, modimpls) -> dict:
@ -88,7 +115,7 @@ class BulletinBUT(ResultatsSemestreBUT):
# except RuntimeWarning: # all nans in np.nanmean
# pass
modimpl_results = self.modimpls_results[modimpl.id]
if self.modimpl_inscr_df[str(modimpl.id)][etud.id]: # si inscrit
if self.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
d[modimpl.module.code] = {
"id": modimpl.id,
"titre": modimpl.module.titre,
@ -144,14 +171,42 @@ class BulletinBUT(ResultatsSemestreBUT):
}
return d
def bulletin_etud(self, etud, formsemestre) -> dict:
"""Le bulletin de l'étudiant dans ce semestre"""
def etud_bonus_description(self, etudid):
"""description du bonus affichée dans la section "UE bonus"."""
if self.bonus_ues is None or self.bonus_ues.shape[1] == 0:
return ""
import random
bonus_vect = self.bonus_ues.loc[etudid]
if bonus_vect.nunique() > 1:
# détail UE par UE
details = [
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
for ue in self.ues
if self.modimpls_in_ue(ue.id, etudid)
and ue.id in self.bonus_ues
and bonus_vect[ue.id] > 0.0
]
if details:
return "Bonus de " + ", ".join(details)
else:
return "" # aucun bonus
else:
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict:
"""Le bulletin de l'étudiant dans ce semestre.
Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
(bulletins non publiés).
"""
etat_inscription = etud.etat_inscription(formsemestre.id)
nb_inscrits = self.get_inscriptions_counts()[scu.INSCRIT]
published = (not formsemestre.bul_hide_xml) or force_publishing
d = {
"version": "0",
"type": "BUT",
"date": datetime.datetime.utcnow().isoformat() + "Z",
"publie": not formsemestre.bul_hide_xml,
"etudiant": etud.to_dict_bul(),
"formation": {
"id": formsemestre.formation.id,
@ -163,6 +218,10 @@ class BulletinBUT(ResultatsSemestreBUT):
"etat_inscription": etat_inscription,
"options": sco_preferences.bulletin_option_affichage(formsemestre.id),
}
if not published:
return d
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
semestre_infos = {
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
"date_debut": formsemestre.date_debut.isoformat(),
@ -171,9 +230,9 @@ class BulletinBUT(ResultatsSemestreBUT):
"inscription": "TODO-MM-JJ", # XXX TODO
"numero": formsemestre.semestre_id,
"groupes": [], # XXX TODO
"absences": { # XXX TODO
"injustifie": -1,
"total": -1,
"absences": {
"injustifie": nbabsjust,
"total": nbabs,
},
}
semestre_infos.update(
@ -199,7 +258,11 @@ class BulletinBUT(ResultatsSemestreBUT):
"ressources": self.etud_mods_results(etud, self.ressources),
"saes": self.etud_mods_results(etud, self.saes),
"ues": {
ue.acronyme: self.etud_ue_results(etud, ue) for ue in self.ues
ue.acronyme: self.etud_ue_results(etud, ue)
for ue in self.ues
if self.modimpls_in_ue(
ue.id, etud.id
) # si l'UE comporte des modules auxquels on est inscrit
},
"semestre": semestre_infos,
},

View File

@ -134,8 +134,12 @@ def bulletin_but_xml_compat(
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
)
)
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
bonus = 0 # XXX TODO valeur du bonus sport
rang = 0 # XXX TODO rang de l'étudiant selon la moy gen indicative
# valeur du bonus sport
if results.bonus is not None:
bonus = results.bonus[etud.id]
else:
bonus = 0
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
doc.append(Element("note_max", value="20")) # notes toujours sur 20

View File

@ -19,12 +19,16 @@ class StatsMoyenne:
def __init__(self, vals):
"""Calcul les statistiques.
Les valeurs NAN ou non numériques sont toujours enlevées.
Si vals is None, renvoie des zéros (utilisé pour UE bonus)
"""
self.moy = np.nanmean(vals)
self.min = np.nanmin(vals)
self.max = np.nanmax(vals)
self.size = len(vals)
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
if vals is None:
self.moy = self.min = self.max = self.size = self.nb_vals = 0
else:
self.moy = np.nanmean(vals)
self.min = np.nanmin(vals)
self.max = np.nanmax(vals)
self.size = len(vals)
self.nb_vals = self.size - np.count_nonzero(np.isnan(vals))
def to_dict(self):
"Tous les attributs dans un dict"

664
app/comp/bonus_spo.py Normal file
View File

@ -0,0 +1,664 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Classes spécifiques de calcul du bonus sport, culture ou autres activités
Les classes de Bonus fournissent deux méthodes:
- get_bonus_ues()
- get_bonus_moy_gen()
"""
import datetime
import numpy as np
import pandas as pd
from flask import g
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
def get_bonus_sport_class_from_name(dept_id):
"""La classe de bonus sport pour le département indiqué.
Note: en ScoDoc 9, le bonus sport est défini gloabelement et
ne dépend donc pas du département.
Résultat: une sous-classe de BonusSport
"""
raise NotImplementedError()
class BonusSport:
"""Calcul du bonus sport.
Arguments:
- sem_modimpl_moys :
notes moyennes aux modules (tous les étuds x tous les modimpls)
floats avec des NaN.
En classique: sem_matrix, ndarray (etuds x modimpls)
En APC: sem_cube, ndarray (etuds x modimpls x UEs non bonus)
- ues: les ues du semestre (incluant le bonus sport)
- modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
- modimpl_coefs: les coefs des modules
En classique: 1d ndarray de float (modimpl)
En APC: 2d ndarray de float, (modimpl x UE) <= attention à transposer
- etud_moy_gen: Series, index etudid, valeur float (moyenne générale avant bonus)
- etud_moy_ue: DataFrame columns UE (sans sport), rows etudid (moyennes avant bonus)
etud_moy_gen et etud_moy_ue ne sont PAS modifiés (mais utilisés par certains bonus non additifs).
"""
# En classique, active un bonus sur les UEs: (dans ce cas bonus_moy_gen reste None)
classic_use_bonus_ues = False
# Attributs virtuels:
seuil_moy_gen = None
proportion_point = None
bonus_max = None
name = "virtual"
def __init__(
self,
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
etud_moy_gen,
etud_moy_ue,
):
self.formsemestre = formsemestre
self.ues = ues
self.etud_moy_gen = etud_moy_gen
self.etud_moy_ue = etud_moy_ue
self.etuds_idx = modimpl_inscr_df.index # les étudiants inscrits au semestre
self.bonus_ues: pd.DataFrame = None # virtual
self.bonus_moy_gen: pd.Series = None # virtual (pour formations non apc slt)
# Restreint aux modules standards des UE de type "sport":
modimpl_mask = np.array(
[
(m.module.module_type == ModuleType.STANDARD)
and (m.module.ue.type == UE_SPORT)
for m in formsemestre.modimpls_sorted
]
)
self.modimpls_spo = [
modimpl
for i, modimpl in enumerate(formsemestre.modimpls_sorted)
if modimpl_mask[i]
]
"liste des modimpls sport"
# Les moyennes des modules "sport": (une par UE en APC)
# donc (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
sem_modimpl_moys_spo = sem_modimpl_moys[:, modimpl_mask]
# Les inscriptions aux modules sport:
modimpl_inscr_spo = modimpl_inscr_df.values[:, modimpl_mask]
# Les coefficients des modules sport (en apc: nb_mod_sport x nb_ue) (toutes ues)
modimpl_coefs_spo = modimpl_coefs[modimpl_mask]
# sem_modimpl_moys_spo est (nb_etuds, nb_mod_sport)
# ou (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
nb_etuds, nb_mod_sport = sem_modimpl_moys_spo.shape[:2]
nb_ues = len(ues)
# Enlève les NaN du numérateur:
sem_modimpl_moys_no_nan = np.nan_to_num(sem_modimpl_moys_spo, nan=0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
if formsemestre.formation.is_apc():
# BUT
nb_ues_no_bonus = sem_modimpl_moys.shape[2]
# Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_spo_stacked = np.stack(
[modimpl_inscr_spo] * nb_ues_no_bonus, axis=2
)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes: (nb_etuds, nb_mod_bonus, nb_ues_non_bonus)
sem_modimpl_moys_inscrits = np.where(
modimpl_inscr_spo_stacked, sem_modimpl_moys_no_nan, 0.0
)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr_spo_stacked,
np.stack([modimpl_coefs_spo.T] * nb_etuds),
0.0,
)
else:
# Formations classiques
# Ne prend pas en compte les notes des étudiants non inscrits au module:
# Annule les notes:
sem_modimpl_moys_inscrits = np.where(
modimpl_inscr_spo, sem_modimpl_moys_no_nan, 0.0
)
modimpl_coefs_spo = modimpl_coefs_spo.T
modimpl_coefs_etuds = np.where(
modimpl_inscr_spo, np.stack([modimpl_coefs_spo] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN (nb_etuds x nb_mod_sport)
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_modimpl_moys_spo), 0.0, modimpl_coefs_etuds
)
#
self.compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
def compute_bonus(
self,
sem_modimpl_moys_inscrits: np.ndarray,
modimpl_coefs_etuds_no_nan: np.ndarray,
):
"""Calcul des bonus: méthode virtuelle à écraser.
Arguments:
- sem_modimpl_moys_inscrits:
ndarray (nb_etuds, mod_sport) ou en APC (nb_etuds, mods_sport, nb_ue_non_bonus)
les notes aux modules sports auxquel l'étudiant est inscrit, 0 sinon. Pas de nans.
- modimpl_coefs_etuds_no_nan:
les coefficients: float ndarray
Résultat: None
"""
raise NotImplementedError("méthode virtuelle")
def get_bonus_ues(self) -> pd.Series:
"""Les bonus à appliquer aux UE
Résultat: DataFrame de float, index etudid, columns: ue.id
"""
if self.classic_use_bonus_ues or self.formsemestre.formation.is_apc():
return self.bonus_ues
return None
def get_bonus_moy_gen(self):
"""Le bonus à appliquer à la moyenne générale.
Résultat: Series de float, index etudid
"""
if self.formsemestre.formation.is_apc():
return None # garde-fou
return self.bonus_moy_gen
class BonusSportAdditif(BonusSport):
"""Bonus sport simples calcule un bonus à partir des notes moyennes de modules
de l'UE sport, et ce bonus est soit ajouté à la moyenne générale (formations classiques),
soit ajouté à chaque UE (formations APC).
Le bonus est par défaut calculé comme:
Les points au-dessus du seuil (par défaut) 10 sur 20 obtenus dans chacun des
modules optionnels sont cumulés et une fraction de ces points cumulés s'ajoute
à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus
sem_modimpl_moys_inscrits: les notes de sport
En APC: ndarray (nb_etuds, nb_mod_sport, nb_ues_non_bonus)
modimpl_coefs_etuds_no_nan:
"""
bonus_moy_arr = np.sum(
np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
* self.proportion_point,
0.0,
),
axis=1,
)
if self.bonus_max is not None:
# Seuil: bonus limité à bonus_max points (et >= 0)
bonus_moy_arr = np.clip(
bonus_moy_arr, 0.0, self.bonus_max, out=bonus_moy_arr
)
else: # necessaire pour éviter bonus négatifs !
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc() or self.classic_use_bonus_ues:
# Bonus sur les UE et None sur moyenne générale
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
self.bonus_ues = pd.DataFrame(
bonus_moy_arr, index=self.etuds_idx, columns=ues_idx, dtype=float
)
else:
# Bonus sur la moyenne générale seulement
self.bonus_moy_gen = pd.Series(
bonus_moy_arr, index=self.etuds_idx, dtype=float
)
# if len(bonus_moy_arr.shape) > 1:
# bonus_moy_arr = bonus_moy_arr.sum(axis=1)
# Laisse bonus_moy_gen à None, en APC le bonus moy. gen. sera réparti sur les UEs.
class BonusSportMultiplicatif(BonusSport):
"""Bonus sport qui multiplie les moyennes d'UE par un facteur"""
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
amplitude = 0.005 # multiplie les points au dessus du seuil
# En classique, les bonus multiplicatifs agissent par défaut sur les UE:
classic_use_bonus_ues = True
# C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0
# Augmenter de 5% correspond à multiplier par a=1.05
# m_1 = a . m_0
# m_1 = m_0 + bonus
# bonus = m_0 (a - 1)
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Calcule moyenne pondérée des notes de sport:
notes = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
notes = np.nan_to_num(notes, copy=False)
factor = (notes - self.seuil_moy_gen) * self.amplitude # 5% si note=20
factor[factor <= 0] = 0.0 # note < seuil_moy_gen, pas de bonus
# Ne s'applique qu'aux moyennes d'UE
if len(factor.shape) == 1: # classic
factor = factor[:, np.newaxis]
bonus = self.etud_moy_ue * factor
if self.bonus_max is not None:
# Seuil: bonus limité à bonus_max points
bonus.clip(upper=self.bonus_max, inplace=True)
self.bonus_ues = bonus # DataFrame
# Les bonus multiplicatifs ne s'appliquent pas à la moyenne générale
self.bonus_moy_gen = None
class BonusDirect(BonusSportAdditif):
"""Bonus direct: 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).
"""
name = "bonus_direct"
displayed_name = 'Bonus "direct"'
seuil_moy_gen = 0.0 # tous les points sont comptés
proportion_point = 1.0
class BonusBethune(BonusSportMultiplicatif):
"""Calcul bonus modules optionels (sport), règle IUT de Béthune.
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.
"""
name = "bonus_iutbethune"
displayed_name = "IUT de Béthune"
seuil_moy_gen = 10.0
amplitude = 0.005
class BonusBezier(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT de Bézier.
Les étudiants de l'IUT peuvent suivre des enseignements optionnels
sport , 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 3% de ces points cumulés s'ajoutent à
la moyenne générale du semestre déjà obtenue par l'étudiant, dans
la limite de 0,3 points.
"""
# note: cela revient à dire que l'on ajoute 5% des points au dessus de 10,
# et qu'on limite à 5% de 10, soit 0.5 points
# ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis)
name = "bonus_iutbeziers"
displayed_name = "IUT de Bézier"
bonus_max = 0.3
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.03
class BonusBordeaux1(BonusSportMultiplicatif):
"""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.
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.
"""
name = "bonus_iutBordeaux1"
displayed_name = "IUT de Bordeaux 1"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 10.0
amplitude = 0.005
class BonusColmar(BonusSportAdditif):
"""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.
"""
# note: cela revient à dire que l'on ajoute 5% des points au dessus de 10,
# et qu'on limite à 5% de 10, soit 0.5 points
# ce bonus est donc strictement identique à celui de St Denis (BonusIUTStDenis)
name = "bonus_colmar"
displayed_name = "IUT de Colmar"
bonus_max = 0.5
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.05
class BonusGrenobleIUT1(BonusSportMultiplicatif):
"""Bonus IUT1 de Grenoble
À compter de sept. 2021:
La note de sport est sur 20, et on calcule une bonification (en %)
qui va s'appliquer à la moyenne de chaque UE du semestre en appliquant
la formule : bonification (en %) = (note-10)*0,5.
Bonification qui ne s'applique que si la note est >10.
(Une note de 10 donne donc 0% de bonif ; note de 20 : 5% de bonif)
Avant sept. 2021, la note de sport allait de 0 à 5 points (sur 20).
Chaque point correspondait à 0.25% d'augmentation de la moyenne
générale.
Par exemple : note de sport 2/5 : la moyenne générale était augmentée de 0.5%.
"""
name = "bonus_iut1grenoble_2017"
displayed_name = "IUT de Grenoble 1"
# C'est un bonus "multiplicatif": on l'exprime en additif,
# sur chaque moyenne d'UE m_0
# Augmenter de 5% correspond à multiplier par a=1.05
# m_1 = a . m_0
# m_1 = m_0 + bonus
# bonus = m_0 (a - 1)
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant la date"""
if self.formsemestre.date_debut > datetime.date(2021, 7, 15):
self.seuil_moy_gen = 10.0
self.amplitude = 0.005
else: # anciens semestres
self.seuil_moy_gen = 0.0
self.amplitude = 1 / 400.0
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
class BonusLaRochelle(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT de 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 et 20 : ajout de 1% de cette
note sur la moyenne générale du semestre (ou sur les UE en BUT).
"""
name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.01
class BonusLeHavre(BonusSportMultiplicatif):
"""Bonus sport IUT du Havre sur moyenne générale et UE
Les points des modules bonus au dessus de 10/20 sont ajoutés,
et les moyennes d'UE augmentées de 5% de ces points.
"""
name = "bonus_iutlh"
displayed_name = "IUT du Havre"
seuil_moy_gen = 10.0 # seuls les points au dessus du seuil sont comptés
amplitude = 0.005 # multiplie les points au dessus du seuil
class BonusLeMans(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT Le Mans.
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés.
En BUT: la moyenne de chacune des UE du semestre est augmentée de
2% du cumul des points de bonus,
En DUT/LP: la moyenne générale est augmentée de 5% du cumul des points bonus.
Dans tous les cas, le bonus est dans la limite de 0,5 point.
"""
name = "bonus_iutlemans"
displayed_name = "IUT du Mans"
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
bonus_max = 0.5 #
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():
self.proportion_point = 0.02
else:
self.proportion_point = 0.05
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
# Bonus simple, mais avec changement de paramètres en 2010 !
class BonusLille(BonusSportAdditif):
"""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 (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 août 2010) de ces points cumulés
s'ajoutent à la moyenne générale du semestre déjà obtenue par l'étudiant.
"""
name = "bonus_lille"
displayed_name = "IUT de Lille"
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# La date du semestre ?
if self.formsemestre.date_debut > datetime.date(2010, 8, 1):
self.proportion_point = 0.04
else:
self.proportion_point = 0.02
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusLyonProvisoire(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Lyon (provisoire)
Les points au-dessus de 10 sur 20 obtenus dans chacune des matières
optionnelles sont cumulés et 1,8% de ces points cumulés
s'ajoutent aux moyennes, dans la limite d'1/2 point.
"""
name = "bonus_lyon_provisoire"
displayed_name = "IUT de Lyon (provisoire)"
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
proportion_point = 0.018
bonus_max = 0.5
class BonusMulhouse(BonusSportAdditif):
"""Calcul bonus modules optionnels (sport, culture), règle IUT de Mulhouse
La moyenne de chacune des UE du semestre sera majorée à hauteur de
5% du cumul des points supérieurs à 10 obtenus en matières optionnelles,
dans la limite de 0,5 point.
"""
name = "bonus_iutmulhouse"
displayed_name = "IUT de Mulhouse"
seuil_moy_gen = 10.0 # points comptés au dessus de 10.
proportion_point = 0.05
bonus_max = 0.5 #
class BonusNantes(BonusSportAdditif):
"""IUT de Nantes (Septembre 2018)
Nous avons différents types de bonification
(sport, culture, engagement citoyen).
Nous ajoutons aux moyennes 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éclarera 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)
"""
name = "bonus_nantes"
displayed_name = "IUT de Nantes"
seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 1 # multiplie les points au dessus du seuil
bonus_max = 0.5 # plafonnement à 0.5 points
class BonusRoanne(BonusSportAdditif):
"""IUT de Roanne.
Le bonus est compris entre 0 et 0.6 points
et est toujours appliqué aux UEs.
"""
name = "bonus_iutr"
displayed_name = "IUT de Roanne"
seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points
apply_bonus_mg_to_ues = True # sur les UE, même en DUT et LP
class BonusStDenis(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Saint-Denis
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, dans la limite
d'1/2 point.
"""
name = "bonus_iut_stdenis"
displayed_name = "IUT de Saint-Denis"
bonus_max = 0.5
class BonusTours(BonusDirect):
"""Calcul bonus sport & culture IUT Tours.
Les notes des UE bonus (ramenées sur 20) sont sommées
et 1/40 (2,5%) est ajouté aux moyennes: soit à la moyenne générale,
soit pour le BUT à chaque moyenne d'UE.
Attention: en GEII, facteur 1/40, ailleurs facteur 1.
Le bonus total est limité à 1 point.
"""
name = "bonus_tours"
displayed_name = "IUT de Tours"
bonus_max = 1.0 #
seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés
proportion_point = 1.0 / 40.0
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul différencié selon le département !"""
if g.scodoc_dept == "GEII":
self.proportion_point = 1.0 / 40.0
else:
self.proportion_point = 1.0
return super().compute_bonus(
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
)
class BonusVilleAvray(BonusSport):
"""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.
"""
name = "bonus_iutva"
displayed_name = "IUT de Ville d'Avray"
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# Calcule moyenne pondérée des notes de sport:
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)
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
# Bonus moyenne générale, et 0 sur les UE
self.bonus_moy_gen = pd.Series(bonus_moy_arr, index=self.etuds_idx, dtype=float)
if self.bonus_max is not None:
# Seuil: bonus (sur moy. gen.) limité à bonus_max points
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_max)
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
class BonusIUTV(BonusSportAdditif):
"""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.
"""
name = "bonus_iutv"
displayed_name = "IUT de Villetaneuse"
pass # oui, c'ets le bonus par défaut
def get_bonus_class_dict(start=BonusSport, d=None):
"""Dictionnaire des classes de bonus
(liste les sous-classes de BonusSport ayant un nom)
Resultat: { name : class }
"""
if d is None:
d = {}
if start.name != "virtual":
d[start.name] = start
for subclass in start.__subclasses__():
get_bonus_class_dict(subclass, d=d)
return d

View File

@ -21,7 +21,7 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
value: bool (0/1 inscrit ou pas)
"""
# méthode la moins lente: une requete par module, merge les dataframes
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
etudids = [inscr.etudid for inscr in formsemestre.inscriptions]
df = pd.DataFrame(index=etudids, dtype=int)
for moduleimpl_id in moduleimpl_ids:
@ -35,6 +35,8 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
dtype=int,
)
df = df.merge(ins_df, how="left", left_index=True, right_index=True)
# Force columns names to integers (moduleimpl ids)
df.columns = pd.Int64Index([int(x) for x in df.columns], dtype="int")
# les colonnes de df sont en float (Nan) quand il n'y a
# aucun inscrit au module.
df.fillna(0, inplace=True) # les non-inscrits
@ -47,10 +49,10 @@ def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
def df_load_modimpl_inscr_v0(formsemestre):
# methode 0, pur SQL Alchemy, 1.5 à 2 fois plus lente
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
etudids = [i.etudid for i in formsemestre.inscriptions]
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
for modimpl in formsemestre.modimpls:
for modimpl in formsemestre.modimpls_sorted:
ins_mod = df[modimpl.id]
for inscr in modimpl.inscriptions:
ins_mod[inscr.etudid] = True
@ -58,7 +60,7 @@ def df_load_modimpl_inscr_v0(formsemestre):
def df_load_modimpl_inscr_v2(formsemestre):
moduleimpl_ids = [m.id for m in formsemestre.modimpls]
moduleimpl_ids = [m.id for m in formsemestre.modimpls_sorted]
etudids = [i.etudid for i in formsemestre.inscriptions]
df = pd.DataFrame(False, columns=moduleimpl_ids, index=etudids, dtype=bool)
cursor = db.engine.execute(

View File

@ -40,6 +40,8 @@ import pandas as pd
from app import db
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
@dataclass
@ -64,9 +66,10 @@ class ModuleImplResults:
self.moduleimpl_id = moduleimpl.id
self.module_id = moduleimpl.module.id
self.etudids = None
"liste des étudiants inscrits au SEMESTRE"
"liste des étudiants inscrits au SEMESTRE (incluant dem et def)"
self.nb_inscrits_module = None
"nombre d'inscrits (non DEM) au module"
"nombre d'inscrits (non DEM) à ce module"
self.evaluations_completes = []
"séquence de booléens, indiquant les évals à prendre en compte."
self.evaluations_completes_dict = {}
@ -117,7 +120,7 @@ class ModuleImplResults:
# --- Calcul nombre d'inscrits pour déterminer les évaluations "completes":
# on prend les inscrits au module ET au semestre (donc sans démissionnaires)
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
self.etudids
moduleimpl.formsemestre.etudids_actifs
)
self.nb_inscrits_module = len(inscrits_module)
@ -125,14 +128,14 @@ class ModuleImplResults:
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
self.evaluations_completes_dict = {}
for evaluation in moduleimpl.evaluations:
eval_df = self._load_evaluation_notes(evaluation)
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
# ou évaluaton déclarée "à prise en compte immédiate"
is_complete = (
len(set(eval_df.index).intersection(self.etudids))
== self.nb_inscrits_module
) or evaluation.publish_incomplete # immédiate
# ou évaluation déclarée "à prise en compte immédiate"
is_complete = evaluation.publish_incomplete or (
not (inscrits_module - set(eval_df.index))
)
self.evaluations_completes.append(is_complete)
self.evaluations_completes_dict[evaluation.id] = is_complete
@ -263,14 +266,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
return self.etuds_moy_module
def load_evaluations_poids(
moduleimpl_id: int, default_poids=1.0
) -> tuple[pd.DataFrame, list]:
def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
"""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.
Résultat: (evals_poids, liste de UE du semestre)
remplies: 1 si le coef de ce module dans l'UE est non nul, zéro sinon
(sauf pour module bonus, defaut à 1)
Résultat: (evals_poids, liste de UEs du semestre sauf le sport)
"""
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
@ -281,9 +283,21 @@ def load_evaluations_poids(
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
if default_poids is not None:
evals_poids.fillna(value=default_poids, inplace=True)
try:
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
except KeyError as exc:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés:
default_poids = 1.0 if modimpl.module.ue.type == UE_SPORT else 0.0
if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict()
for ue in ues:
evals_poids[ue.id][evals_poids[ue.id].isna()] = (
1 if ue_coefs.get(ue.id, default_poids) > 0 else 0
)
return evals_poids, ues
@ -296,6 +310,7 @@ def moduleimpl_is_conforme(
évaluations vers une UE de coefficient non nul est non nulle.
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
NB: les UEs dans evals_poids sont sans le bonus sport
"""
nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0:

View File

@ -38,7 +38,7 @@ def compute_sem_moys_apc(
= moyenne des moyennes d'UE, pondérée par la somme de leurs coefs
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE
modimpl_coefs_df: DataFrame, colonnes moduleimpl_id, lignes UE (sans ue bonus)
Result: panda Series, index etudid, valeur float (moyenne générale)
"""

View File

@ -36,6 +36,8 @@ from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef
from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
@ -44,10 +46,10 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
En APC, ces coefs lient les modules à chaque UE.
Résultat: (module_coefs_df, ues, modules)
Résultat: (module_coefs_df, ues_no_bonus, modules)
DataFrame rows = UEs, columns = modules, value = coef.
Considère toutes les UE (sauf sport) et modules du semestre.
Considère toutes les UE sauf bonus et tous les modules du semestre.
Les coefs non définis (pas en base) sont mis à zéro.
Si semestre_idx None, prend toutes les UE de la formation.
@ -62,6 +64,10 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
.filter(
(Module.module_type == ModuleType.RESSOURCE)
| (Module.module_type == ModuleType.SAE)
| (
(Module.ue_id == UniteEns.id)
& (UniteEns.type == sco_codes_parcours.UE_SPORT)
)
)
.order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
@ -88,7 +94,17 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
# silently ignore coefs associated to other modules (ie when module_type is changed)
module_coefs_df.fillna(value=0, inplace=True)
# Initialisation des poids non fixés:
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
mod.id: 1.0
if (mod.module_type == ModuleType.STANDARD) and (mod.ue.type == UE_SPORT)
else 0.0
for mod in modules
}
module_coefs_df.fillna(value=default_poids, inplace=True)
return module_coefs_df, ues, modules
@ -100,15 +116,15 @@ def df_load_modimpl_coefs(
Comme df_load_module_coefs mais prend seulement les UE
et modules du formsemestre.
Si ues et modimpls sont None, prend tous ceux du formsemestre.
Si ues et modimpls sont None, prend tous ceux du formsemestre (sauf ue bonus).
Résultat: (module_coefs_df, ues, modules)
DataFrame rows = UEs, columns = modimpl, value = coef.
DataFrame rows = UEs (sans bonus), columns = modimpl, value = coef.
"""
if ues is None:
ues = formsemestre.query_ues().all()
ue_ids = [x.id for x in ues]
if modimpls is None:
modimpls = formsemestre.modimpls.all()
modimpls = formsemestre.modimpls_sorted
modimpl_ids = [x.id for x in modimpls]
mod2impl = {m.module.id: m.id for m in modimpls}
modimpl_coefs_df = pd.DataFrame(columns=modimpl_ids, index=ue_ids, dtype=float)
@ -120,7 +136,19 @@ def df_load_modimpl_coefs(
for mod_coef in mod_coefs:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][mod_coef.ue_id] = mod_coef.coef
modimpl_coefs_df.fillna(value=0, inplace=True)
# Initialisation des poids non fixés:
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
# sur toutes les UE)
default_poids = {
modimpl.id: 1.0
if (modimpl.module.module_type == ModuleType.STANDARD)
and (modimpl.module.ue.type == UE_SPORT)
else 0.0
for modimpl in formsemestre.modimpls_sorted
}
modimpl_coefs_df.fillna(value=default_poids, inplace=True)
return modimpl_coefs_df, ues, modimpls
@ -134,7 +162,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
assert len(modimpls_notes)
modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x UE)
# passe de (mod x etud x ue) à (etud x mod x ue)
return modimpls_notes.swapaxes(0, 1)
@ -144,10 +172,14 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
et assemble le cube.
etuds: tous les inscrits au semestre (avec dem. et def.)
modimpls: _tous_ les modimpls de ce semestre
UEs: X?X voir quelles sont les UE considérées ici
modimpls: _tous_ les modimpls de ce semestre (y compris bonus sport)
UEs: toutes les UE du semestre (même si pas d'inscrits) SAUF le sport.
Resultat:
Attention: la liste des modimpls inclut les modules des UE sport, mais
elles ne sont pas dans la troisième dimension car elles n'ont pas de
"moyenne d'UE".
Résultat:
sem_cube : ndarray (etuds x modimpls x UEs)
modimpls_evals_poids dict { modimpl.id : evals_poids }
modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
@ -155,7 +187,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
modimpls_results = {}
modimpls_evals_poids = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsAPC(modimpl)
evals_poids, _ = moy_mod.load_evaluations_poids(modimpl.id)
etuds_moy_module = mod_results.compute_module_moy(evals_poids)
@ -194,26 +226,27 @@ def compute_ue_moys_apc(
modimpls : liste des modules à considérer (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
Resultat: DataFrame columns UE, rows etudid
Résultat: DataFrame columns UE (sans sport), rows etudid
"""
nb_etuds, nb_modules, nb_ues = sem_cube.shape
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
nb_ues_tot = len(ues)
assert len(modimpls) == nb_modules
if nb_modules == 0 or nb_etuds == 0:
return pd.DataFrame(
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
)
assert len(etuds) == nb_etuds
assert len(ues) == nb_ues
assert modimpl_inscr_df.shape[0] == nb_etuds
assert modimpl_inscr_df.shape[1] == nb_modules
assert modimpl_coefs_df.shape[0] == nb_ues
assert modimpl_coefs_df.shape[0] == nb_ues_no_bonus
assert modimpl_coefs_df.shape[1] == nb_modules
modimpl_inscr = modimpl_inscr_df.values
modimpl_coefs = modimpl_coefs_df.values
# Duplique les inscriptions sur les UEs:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
# Duplique les inscriptions sur les UEs non bonus:
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues_no_bonus, axis=2)
# Enlève les NaN du numérateur:
# si on veut prendre en compte les modules avec notes neutralisées ?
sem_cube_no_nan = np.nan_to_num(sem_cube, nan=0.0)
@ -234,7 +267,9 @@ def compute_ue_moys_apc(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
return pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
etud_moy_ue,
index=modimpl_inscr_df.index, # les etudids
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
)
@ -244,6 +279,7 @@ def compute_ue_moys_classic(
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique.
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
@ -251,13 +287,19 @@ def compute_ue_moys_classic(
NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
sem_matrix: notes moyennes aux modules
L'éventuel bonus sport n'est PAS appliqué ici.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (SAEs, tout sauf sport, ...).
sem_matrix: notes moyennes aux modules (tous les étuds x tous les modimpls)
ndarray (etuds x modimpls)
(floats avec des NaN)
etuds : listes des étudiants (dim. 0 de la matrice)
ues : liste des UE
ues : liste des UE du semestre
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs: vecteur des coefficients de modules
modimpl_mask: masque des modimpls à prendre en compte
Résultat:
- moyennes générales: pd.Series, index etudid
@ -266,10 +308,15 @@ def compute_ue_moys_classic(
les coefficients effectifs de chaque UE pour chaque étudiant
(sommes de coefs de modules pris en compte)
"""
# Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask]
modimpl_inscr = modimpl_inscr_df.values[:, modimpl_mask]
modimpl_coefs = modimpl_coefs[modimpl_mask]
nb_etuds, nb_modules = sem_matrix.shape
assert len(modimpl_coefs) == nb_modules
nb_ues = len(ues)
modimpl_inscr = modimpl_inscr_df.values
nb_ues = len(ues) # en comptant bonus
# Enlève les NaN du numérateur:
sem_matrix_no_nan = np.nan_to_num(sem_matrix, nan=0.0)
# Ne prend pas en compte les notes des étudiants non inscrits au module:
@ -283,16 +330,11 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
# Calcul des moyennes générales:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_gen = np.sum(
modimpl_coefs_etuds_no_nan * sem_matrix_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index)
# Calcul des moyennes d'UE
# --------------------- Calcul des moyennes d'UE
ue_modules = np.array(
[[m.module.ue == ue for m in formsemestre.modimpls] for ue in ues]
)[..., np.newaxis]
[[m.module.ue == ue for m in formsemestre.modimpls_sorted] for ue in ues]
)[..., np.newaxis][:, modimpl_mask, :]
modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues
)
@ -305,9 +347,35 @@ def compute_ue_moys_classic(
etud_moy_ue_df = pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, columns=[ue.id for ue in ues]
)
etud_coef_ue_df = pd.DataFrame(
coefs.sum(axis=2).T,
index=modimpl_inscr_df.index, # etudids
columns=[ue.id for ue in ues],
)
# --------------------- Calcul des moyennes générales
if sco_preferences.get_preference("use_ue_coefs", formsemestre.id):
# Cas avec coefficients d'UE forcés: (on met à zéro l'UE bonus)
etud_coef_ue_df = pd.DataFrame(
{ue.id: ue.coefficient if ue.type != UE_SPORT else 0.0 for ue in ues},
index=modimpl_inscr_df.index,
columns=[ue.id for ue in ues],
)
# remplace NaN par zéros dans les moyennes d'UE
etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False)
# Si on voulait annuler les coef d'UE dont la moyenne d'UE est NaN
# etud_coef_ue_df_no_nan = etud_coef_ue_df.where(etud_moy_ue_df.notna(), 0.0)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_gen_s = (etud_coef_ue_df * etud_moy_ue_df_no_nan).sum(
axis=1
) / etud_coef_ue_df.sum(axis=1)
else:
# Cas normal: pondère directement les modules
etud_coef_ue_df = pd.DataFrame(
coefs.sum(axis=2).T,
index=modimpl_inscr_df.index, # etudids
columns=[ue.id for ue in ues],
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_gen = np.sum(
modimpl_coefs_etuds_no_nan * sem_matrix_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
etud_moy_gen_s = pd.Series(etud_moy_gen, index=modimpl_inscr_df.index)
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df

View File

@ -10,6 +10,9 @@ import pandas as pd
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.scodoc.sco_codes_parcours import UE_SPORT
class ResultatsSemestreBUT(NotesTableCompat):
@ -37,15 +40,25 @@ class ResultatsSemestreBUT(NotesTableCompat):
) = moy_ue.notes_sem_load_cube(self.formsemestre)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
self.formsemestre, ues=self.ues, modimpls=self.modimpls
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
)
# l'idx de la colonne du mod modimpl.id est
# modimpl_coefs_df.columns.get_loc(modimpl.id)
# idx de l'UE: modimpl_coefs_df.index.get_loc(ue.id)
# Elimine les coefs des modimpl bonus sports:
modimpls_sport = [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.type == UE_SPORT
]
for modimpl in modimpls_sport:
self.modimpl_coefs_df[modimpl.id] = 0
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.modimpls,
self.formsemestre.modimpls_sorted,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
@ -54,6 +67,28 @@ class ResultatsSemestreBUT(NotesTableCompat):
self.etud_coef_ue_df = pd.DataFrame(
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
# --- Bonus Sport & Culture
if len(modimpls_sport) > 0:
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:
bonus: BonusSport = bonus_class(
self.formsemestre,
self.sem_cube,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df.transpose(),
self.etud_moy_gen,
self.etud_moy_ue,
)
self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None:
self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
# Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyenens d'UE, et impacte
# donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
)

View File

@ -11,7 +11,11 @@ import pandas as pd
from app.comp import moy_mod, moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.formsemestre import FormSemestre
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
class ResultatsSemestreClassic(NotesTableCompat):
@ -41,11 +45,20 @@ class ResultatsSemestreClassic(NotesTableCompat):
)
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
self.modimpl_coefs = np.array(
[m.module.coefficient for m in self.formsemestre.modimpls]
[m.module.coefficient for m in self.formsemestre.modimpls_sorted]
)
self.modimpl_idx = {m.id: i for i, m in enumerate(self.formsemestre.modimpls)}
self.modimpl_idx = {
m.id: i for i, m in enumerate(self.formsemestre.modimpls_sorted)
}
"l'idx de la colonne du mod modimpl.id est modimpl_idx[modimpl.id]"
modimpl_standards_mask = np.array(
[
(m.module.module_type == ModuleType.STANDARD)
and (m.module.ue.type != UE_SPORT)
for m in self.formsemestre.modimpls_sorted
]
)
(
self.etud_moy_gen,
self.etud_moy_ue,
@ -56,7 +69,32 @@ class ResultatsSemestreClassic(NotesTableCompat):
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
modimpl_standards_mask,
)
# --- Bonus Sport & Culture
bonus_class = ScoDocSiteConfig.get_bonus_sport_class()
if bonus_class is not None:
bonus: BonusSport = bonus_class(
self.formsemestre,
self.sem_matrix,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
self.etud_moy_gen,
self.etud_moy_ue,
)
self.bonus_ues = bonus.get_bonus_ues()
if self.bonus_ues is not None:
self.etud_moy_ue += self.bonus_ues # somme les dataframes
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
bonus_mg = bonus.get_bonus_moy_gen()
if bonus_mg is not None:
self.etud_moy_gen += bonus_mg
self.etud_moy_gen.clip(lower=0.0, upper=20.0, inplace=True)
self.bonus = (
bonus_mg # compat nt, utilisé pour l'afficher sur les bulletins
)
# --- Classements:
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
@ -85,9 +123,9 @@ class ResultatsSemestreClassic(NotesTableCompat):
}
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple:
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
"""Calcule la matrice des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
(charge toutes les notes, calcule les moyennes des modules
et assemble la matrice)
Resultat:
sem_matrix : 2d-array (etuds x modimpls)
@ -95,7 +133,7 @@ def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple:
"""
modimpls_results = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
for modimpl in formsemestre.modimpls_sorted:
mod_results = moy_mod.ModuleImplResultsClassic(modimpl)
etuds_moy_module = mod_results.compute_module_moy()
modimpls_results[modimpl.id] = mod_results

View File

@ -8,7 +8,7 @@ from collections import defaultdict, Counter
from functools import cached_property
import numpy as np
import pandas as pd
from app.comp.aux import StatsMoyenne
from app.comp.aux_stats import StatsMoyenne
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models.ues import UniteEns
@ -100,42 +100,28 @@ class ResultatsSemestre:
@cached_property
def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre
"""Liste des UEs du semestre (avec les UE bonus sport)
(indices des DataFrames)
"""
return self.formsemestre.query_ues(with_sport=True).all()
@cached_property
def modimpls(self):
"""Liste des modimpls du semestre
- triée par numéro de module en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.formsemestre.modimpls.all()
if self.is_apc:
modimpls.sort(key=lambda m: (m.module.numero, m.module.code))
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls
@cached_property
def ressources(self):
"Liste des ressources du semestre, triées par numéro de module"
return [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
m
for m in self.formsemestre.modimpls_sorted
if m.module.module_type == scu.ModuleType.RESSOURCE
]
@cached_property
def saes(self):
"Liste des SAÉs du semestre, triées par numéro de module"
return [m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE]
return [
m
for m in self.formsemestre.modimpls_sorted
if m.module.module_type == scu.ModuleType.SAE
]
@cached_property
def ue_validables(self) -> list:
@ -144,6 +130,16 @@ class ResultatsSemestre:
"""
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
def modimpls_in_ue(self, ue_id, etudid):
"""Liste des modimpl de cet ue auxquels l'étudiant est inscrit"""
# sert pour l'affichage ou non de l'UE sur le bulletin
return [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if modimpl.module.ue.id == ue_id
and self.modimpl_inscr_df[modimpl.id][etudid]
]
@cached_property
def ue_au_dessus(self, seuil=10.0) -> pd.DataFrame:
"""DataFrame columns UE, rows etudid, valeurs: bool
@ -163,16 +159,20 @@ class NotesTableCompat(ResultatsSemestre):
développements (API malcommode et peu efficace).
"""
_cached_attrs = ResultatsSemestre._cached_attrs + ()
_cached_attrs = ResultatsSemestre._cached_attrs + (
"bonus",
"bonus_ues",
)
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre)
nb_etuds = len(self.etuds)
self.bonus = defaultdict(lambda: 0.0) # XXX TODO
self.ue_rangs = {u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.ues}
self.bonus = None # virtuel
self.bonus_ues = None # virtuel
self.ue_rangs = {u.id: (None, nb_etuds) for u in self.ues}
self.mod_rangs = {
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.modimpls
m.id: (None, nb_etuds) for m in self.formsemestre.modimpls_sorted
}
self.moy_min = "NA"
self.moy_max = "NA"
@ -221,18 +221,26 @@ class NotesTableCompat(ResultatsSemestre):
ues = []
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
d = ue.to_dict()
d.update(StatsMoyenne(self.etud_moy_ue[ue.id]).to_dict())
if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id]
else:
moys = None
d.update(StatsMoyenne(moys).to_dict())
ues.append(d)
return ues
def get_modimpls_dict(self, ue_id=None):
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
"""Liste des modules pour une UE (ou toutes si ue_id==None),
triés par numéros (selon le type de formation)
"""
if ue_id is None:
return [m.to_dict() for m in self.modimpls]
else:
return [m.to_dict() for m in self.modimpls if m.module.ue.id == ue_id]
modimpls_dict = []
for modimpl in self.formsemestre.modimpls_sorted:
if ue_id == None or modimpl.module.ue.id == ue_id:
d = modimpl.to_dict()
# compat ScoDoc < 9.2: ajoute matières
d["mat"] = modimpl.module.matiere.to_dict()
modimpls_dict.append(d)
return modimpls_dict
def get_etud_decision_sem(self, etudid: int) -> dict:
"""Decision du jury prise pour cet etudiant, ou None s'il n'y en pas eu.
@ -261,13 +269,10 @@ class NotesTableCompat(ResultatsSemestre):
return ""
return ins.etat
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne générale de cet etudiant dans ce semestre.
Prend(ra) en compte les UE capitalisées. (TODO) XXX
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
def get_etud_mat_moy(self, matiere_id, etudid):
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
# non supporté en 9.2
return "na"
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
@ -276,6 +281,14 @@ class NotesTableCompat(ResultatsSemestre):
"""
raise NotImplementedError() # virtual method
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne générale de cet etudiant dans ce semestre.
Prend(ra) en compte les UE capitalisées. (TODO) XXX
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
def get_etud_ue_status(self, etudid: int, ue_id: int):
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
@ -359,12 +372,16 @@ class NotesTableCompat(ResultatsSemestre):
moy_gen = self.etud_moy_gen.get(etudid, False)
if moy_gen is False:
# pas de moyenne: démissionnaire ou def
t = ["-"] + ["0.00"] * len(self.ues) + ["NI"] * len(self.modimpls)
t = (
["-"]
+ ["0.00"] * len(self.ues)
+ ["NI"] * len(self.formsemestre.modimpls_sorted)
)
else:
moy_ues = self.etud_moy_ue.loc[etudid]
t = [moy_gen] + list(moy_ues)
# TODO UE capitalisées: ne pas afficher moyennes modules
for modimpl in self.modimpls:
for modimpl in self.formsemestre.modimpls_sorted:
val = self.get_etud_mod_moy(modimpl.id, etudid)
t.append(val)
t.append(etudid)

View File

@ -0,0 +1,77 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# 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
#
##############################################################################
"""
Formulaires configuration Exports Apogée (codes)
"""
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from wtforms import SubmitField, validators
from wtforms.fields.simple import StringField
from app import models
from app.models import ScoDocSiteConfig
from app.models import SHORT_STR_LEN
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_utils as scu
def _build_code_field(code):
return StringField(
label=code,
description=sco_codes_parcours.CODES_EXPL[code],
validators=[
validators.regexp(
r"^[A-Z0-9_]*$",
message="Ne doit comporter que majuscules et des chiffres",
),
validators.Length(
max=SHORT_STR_LEN,
message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères",
),
validators.DataRequired("code requis"),
],
)
class CodesDecisionsForm(FlaskForm):
ADC = _build_code_field("ADC")
ADJ = _build_code_field("ADJ")
ADM = _build_code_field("ADM")
AJ = _build_code_field("AJ")
ATB = _build_code_field("ATB")
ATJ = _build_code_field("ATJ")
ATT = _build_code_field("ATT")
CMP = _build_code_field("CMP")
DEF = _build_code_field("DEF")
DEM = _build_code_field("DEF")
NAR = _build_code_field("NAR")
RAT = _build_code_field("RAT")
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -47,7 +47,6 @@ from app.scodoc.sco_config_actions import (
LogoDelete,
LogoUpdate,
LogoInsert,
BonusSportUpdate,
)
from flask_login import current_user
@ -296,23 +295,15 @@ def _make_depts_data(modele):
return data
def _make_data(bonus_sport, modele):
def _make_data(modele):
data = {
"bonus_sport_func_name": bonus_sport,
"depts": _make_depts_data(modele=modele),
}
return data
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration général"
bonus_sport_func_name = SelectField(
label="Fonction de calcul des bonus sport&culture",
choices=[
(x, x if x else "Aucune")
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
],
)
class LogosConfigurationForm(FlaskForm):
"Panneau de configuration des logos"
depts = FieldList(FormField(DeptForm))
def __init__(self, *args, **kwargs):
@ -361,11 +352,6 @@ class ScoDocConfigurationForm(FlaskForm):
return dept_form.get_form(logoname)
def select_action(self):
if (
self.data["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
):
return BonusSportUpdate(self.data)
for dept_entry in self.depts:
dept_form = dept_entry.form
action = dept_form.select_action()
@ -374,14 +360,11 @@ class ScoDocConfigurationForm(FlaskForm):
return None
def configuration():
"""Panneau de configuration général"""
auth_name = str(current_user)
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
form = ScoDocConfigurationForm(
def config_logos():
"Page de configuration des logos"
# nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue
form = LogosConfigurationForm(
data=_make_data(
bonus_sport=ScoDocSiteConfig.get_bonus_sport_func_name(),
modele=sco_logos.list_logos(),
)
)
@ -392,11 +375,11 @@ def configuration():
flash(action.message)
return redirect(
url_for(
"scodoc.configuration",
"scodoc.configure_logos",
)
)
return render_template(
"configuration.html",
"config_logos.html",
scodoc_dept=None,
title="Configuration ScoDoc",
form=form,

View File

@ -0,0 +1,76 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# ScoDoc
#
# 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
#
##############################################################################
"""
Formulaires configuration Exports Apogée (codes)
"""
from flask import flash, url_for, redirect, request, render_template
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField
import app
from app.models import ScoDocSiteConfig
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration des logos"
bonus_sport_func_name = SelectField(
label="Fonction de calcul des bonus sport&culture",
choices=[
(name, displayed_name if name else "Aucune")
for (name, displayed_name) in ScoDocSiteConfig.get_bonus_sport_class_list()
],
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
def configuration():
"Page de configuration principale"
# nb: le contrôle d'accès (SuperAdmin) doit être fait dans la vue
form = ScoDocConfigurationForm(
data={
"bonus_sport_func_name": ScoDocSiteConfig.get_bonus_sport_class_name(),
}
)
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index"))
if form.validate_on_submit():
if (
form.data["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_class_name()
):
ScoDocSiteConfig.set_bonus_sport_class(form.data["bonus_sport_func_name"])
app.clear_scodoc_cache()
flash(f"Fonction bonus sport&culture configurée.")
return redirect(url_for("scodoc.index"))
return render_template(
"configuration.html",
form=form,
)

View File

@ -6,13 +6,12 @@ XXX version préliminaire ScoDoc8 #sco8 sans département
CODE_STR_LEN = 16 # chaine pour les codes
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
APO_CODE_STR_LEN = 24 # nb de car max d'un code Apogée
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
GROUPNAME_STR_LEN = 64
from app.models.raw_sql_init import create_database_functions
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
from app.models.departements import Departement
from app.models.etudiants import (
Identite,
@ -57,7 +56,7 @@ from app.models.notes import (
NotesNotes,
NotesNotesLog,
)
from app.models.preferences import ScoPreference, ScoDocSiteConfig
from app.models.preferences import ScoPreference
from app.models.but_refcomp import (
ApcReferentielCompetences,
@ -65,3 +64,4 @@ from app.models.but_refcomp import (
ApcSituationPro,
ApcAppCritique,
)
from app.models.config import ScoDocSiteConfig

209
app/models/config.py Normal file
View File

@ -0,0 +1,209 @@
# -*- coding: UTF-8 -*
"""Model : site config WORK IN PROGRESS #WIP
"""
from flask import flash
from app import db, log
from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_codes_parcours import (
ADC,
ADJ,
ADM,
AJ,
ATB,
ATJ,
ATT,
CMP,
DEF,
DEM,
NAR,
RAT,
)
CODES_SCODOC_TO_APO = {
ADC: "ADMC",
ADJ: "ADM",
ADM: "ADM",
AJ: "AJ",
ATB: "AJAC",
ATJ: "AJAC",
ATT: "AJAC",
CMP: "COMP",
DEF: "NAR",
DEM: "NAR",
NAR: "NAR",
RAT: "ATT",
}
def code_scodoc_to_apo_default(code):
"""Conversion code jury ScoDoc en code Apogée
(codes par défaut, c'est configurable via ScoDocSiteConfig.get_code_apo)
"""
return CODES_SCODOC_TO_APO.get(code, "DEF")
class ScoDocSiteConfig(db.Model):
"""Config. d'un site
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
antérieures étaient dans scodoc_config.py
"""
__tablename__ = "scodoc_site_config"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
BONUS_SPORT = "bonus_sport_func_name"
NAMES = {
BONUS_SPORT: str,
"always_require_ine": bool,
"SCOLAR_FONT": str,
"SCOLAR_FONT_SIZE": str,
"SCOLAR_FONT_SIZE_FOOT": str,
"INSTITUTION_NAME": str,
"INSTITUTION_ADDRESS": str,
"INSTITUTION_CITY": str,
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
}
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
@classmethod
def get_dict(cls) -> dict:
"Returns all data as a dict name = value"
return {
c.name: cls.NAMES.get(c.name, lambda x: x)(c.value)
for c in ScoDocSiteConfig.query.all()
}
@classmethod
def set_bonus_sport_class(cls, class_name):
"""Record bonus_sport config.
If class_name not defined, raise NameError
"""
if class_name not in cls.get_bonus_sport_class_names():
raise NameError("invalid class name for bonus_sport")
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c:
log("setting to " + class_name)
c.value = class_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_class_name(cls):
"""Get configured bonus function name, or None if None."""
klass = cls.get_bonus_sport_class_from_name()
if klass is None:
return ""
else:
return klass.name
@classmethod
def get_bonus_sport_class(cls):
"""Get configured bonus function, or None if None."""
return cls.get_bonus_sport_class_from_name()
@classmethod
def get_bonus_sport_class_from_name(cls, class_name=None):
"""returns bonus class with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
If class_name not found in module bonus_sport, returns None
and flash a warning.
"""
if not class_name: # None or ""
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
class_name = c.value
if class_name == "": # pas de bonus défini
return None
klass = bonus_spo.get_bonus_class_dict().get(class_name)
if klass is None:
flash(
f"""Fonction de calcul bonus sport inexistante: {class_name}.
Changez ou contactez votre administrateur local."""
)
return klass
@classmethod
def get_bonus_sport_class_names(cls) -> list:
"""List available bonus class names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(bonus_spo.get_bonus_class_dict().keys())
@classmethod
def get_bonus_sport_class_list(cls) -> list[tuple]:
"""List available bonus class names
(starting with empty string to represent "no bonus function").
"""
d = bonus_spo.get_bonus_class_dict()
class_list = [(name, d[name].displayed_name) for name in d.keys()]
class_list.sort(key=lambda x: x[1].replace(" du ", " de "))
return [("", "")] + class_list
@classmethod
def get_bonus_sport_func(cls):
"""Fonction bonus_sport ScoDoc 7 XXX
Transitoire pour les tests durant la transition #sco92
"""
"""returns bonus func with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises ScoValueError if func_name not found in module bonus_sport.
"""
from app.scodoc import bonus_sport
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
func_name = c.value
if func_name == "": # pas de bonus défini
return None
try:
return getattr(bonus_sport, func_name)
except AttributeError:
raise ScoValueError(
f"""Fonction de calcul maison inexistante: {func_name}.
(contacter votre administrateur local)."""
)
@classmethod
def get_code_apo(cls, code: str) -> str:
"""La représentation d'un code pour les exports Apogée.
Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL
Les codes par défaut sont donnés dans sco_apogee_csv.
"""
cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
if not cfg:
code_apo = code_scodoc_to_apo_default(code)
else:
code_apo = cfg.value
return code_apo
@classmethod
def set_code_apo(cls, code: str, code_apo: str):
"""Enregistre nouvelle représentation du code"""
if code_apo != cls.get_code_apo(code):
cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
if cfg is None:
cfg = ScoDocSiteConfig(code, code_apo)
else:
cfg.value = code_apo
db.session.add(cfg)
db.session.commit()

View File

@ -103,7 +103,16 @@ class Evaluation(db.Model):
Note: si les poids ne sont pas initialisés (poids par défaut),
ils ne sont pas affichés.
"""
return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids])
# restreint aux UE du semestre dans lequel est cette évaluation
# au cas où le module ait changé de semestre et qu'il reste des poids
evaluation_semestre_idx = self.moduleimpl.module.semestre_id
return ", ".join(
[
f"{p.ue.acronyme}: {p.poids}"
for p in self.ue_poids
if evaluation_semestre_idx == p.ue.semestre_idx
]
)
class EvaluationUEPoids(db.Model):

View File

@ -1,6 +1,7 @@
"""ScoDoc 9 models : Formations
"""
import app
from app import db
from app.comp import df_cache
from app.models import SHORT_STR_LEN
@ -141,8 +142,7 @@ class Formation(db.Model):
db.session.add(ue)
db.session.commit()
if change:
self.invalidate_module_coefs()
app.clear_scodoc_cache()
class Matiere(db.Model):
@ -161,3 +161,16 @@ class Matiere(db.Model):
numero = db.Column(db.Integer) # ordre de présentation
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
def __repr__(self):
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
self.ue_id}, titre='{self.titre}')>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["ue_id"] = self.id
e["numero"] = e["numero"] if e["numero"] else 0
return e

View File

@ -49,7 +49,7 @@ class FormSemestre(db.Model):
gestion_compensation = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# ne publie pas le bulletin XML:
# ne publie pas le bulletin XML ou JSON:
bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
@ -112,6 +112,9 @@ class FormSemestre(db.Model):
if self.modalite is None:
self.modalite = FormationModalite.DEFAULT_MODALITE
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
def to_dict(self):
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
@ -152,6 +155,28 @@ class FormSemestre(db.Model):
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
return sem_ues.order_by(UniteEns.numero)
@cached_property
def modimpls_sorted(self) -> list[ModuleImpl]:
"""Liste des modimpls du semestre
- triée par type/numéro/code en APC
- triée par numéros d'UE/matières/modules pour les formations standard.
"""
modimpls = self.modimpls.all()
if self.formation.is_apc():
modimpls.sort(
key=lambda m: (m.module.module_type, m.module.numero, m.module.code)
)
else:
modimpls.sort(
key=lambda m: (
m.module.ue.numero,
m.module.matiere.numero,
m.module.numero,
m.module.code,
)
)
return modimpls
def est_courant(self) -> bool:
"""Vrai si la date actuelle (now) est dans le semestre
(les dates de début et fin sont incluses)
@ -262,7 +287,7 @@ class FormSemestre(db.Model):
self.date_fin.year})"""
def titre_num(self) -> str:
"""Le titre est le semestre, ex ""DUT Informatique semestre 2"" """
"""Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
return self.titre
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
@ -288,6 +313,11 @@ class FormSemestre(db.Model):
else:
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
@cached_property
def etudids_actifs(self) -> set:
"Set des etudids inscrits non démissionnaires"
return {ins.etudid for ins in self.inscriptions if ins.etat == scu.INSCRIT}
@cached_property
def etuds_inscriptions(self) -> dict:
"""Map { etudid : inscription } (incluant DEM et DEF)"""

View File

@ -5,7 +5,7 @@ import pandas as pd
from app import db
from app.comp import df_cache
from app.models import UniteEns, Identite
from app.models import Identite, Module
import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu
@ -127,3 +127,16 @@ class ModuleImplInscription(db.Model):
ModuleImpl,
backref=db.backref("inscriptions", cascade="all, delete-orphan"),
)
@classmethod
def nb_inscriptions_dans_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id,
Module.ue_id == ue_id,
).count()

View File

@ -4,6 +4,7 @@
from app import db
from app.models import APO_CODE_STR_LEN
from app.scodoc import sco_utils as scu
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
@ -131,7 +132,8 @@ class Module(db.Model):
def ue_coefs_list(self, include_zeros=True):
"""Liste des coefs vers les UE (pour les modules APC).
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre.
Si include_zeros, liste aussi les UE sans coef (donc nul) de ce semestre,
sauf UE bonus sport.
Result: List of tuples [ (ue, coef) ]
"""
if not self.is_apc():
@ -140,6 +142,7 @@ class Module(db.Model):
# Toutes les UE du même semestre:
ues_semestre = (
self.formation.ues.filter_by(semestre_idx=self.ue.semestre_idx)
.filter(UniteEns.type != UE_SPORT)
.order_by(UniteEns.numero)
.all()
)

View File

@ -2,9 +2,8 @@
"""Model : preferences
"""
from app import db, log
from app.scodoc import bonus_sport
from app.scodoc.sco_exceptions import ScoValueError
from app import db
class ScoPreference(db.Model):
@ -19,108 +18,3 @@ class ScoPreference(db.Model):
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
class ScoDocSiteConfig(db.Model):
"""Config. d'un site
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
antérieures étaient dans scodoc_config.py
"""
__tablename__ = "scodoc_site_config"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
BONUS_SPORT = "bonus_sport_func_name"
NAMES = {
BONUS_SPORT: str,
"always_require_ine": bool,
"SCOLAR_FONT": str,
"SCOLAR_FONT_SIZE": str,
"SCOLAR_FONT_SIZE_FOOT": str,
"INSTITUTION_NAME": str,
"INSTITUTION_ADDRESS": str,
"INSTITUTION_CITY": str,
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
}
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
def get_dict(self) -> dict:
"Returns all data as a dict name = value"
return {
c.name: self.NAMES.get(c.name, lambda x: x)(c.value)
for c in ScoDocSiteConfig.query.all()
}
@classmethod
def set_bonus_sport_func(cls, func_name):
"""Record bonus_sport config.
If func_name not defined, raise NameError
"""
if func_name not in cls.get_bonus_sport_func_names():
raise NameError("invalid function name for bonus_sport")
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c:
log("setting to " + func_name)
c.value = func_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_func_name(cls):
"""Get configured bonus function name, or None if None."""
f = cls.get_bonus_sport_func_from_name()
if f is None:
return ""
else:
return f.__name__
@classmethod
def get_bonus_sport_func(cls):
"""Get configured bonus function, or None if None."""
return cls.get_bonus_sport_func_from_name()
@classmethod
def get_bonus_sport_func_from_name(cls, func_name=None):
"""returns bonus func with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises ScoValueError if func_name not found in module bonus_sport.
"""
if func_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
func_name = c.value
if func_name == "": # pas de bonus défini
return None
try:
return getattr(bonus_sport, func_name)
except AttributeError:
raise ScoValueError(
f"""Fonction de calcul maison inexistante: {func_name}.
(contacter votre administrateur local)."""
)
@classmethod
def get_bonus_sport_func_names(cls):
"""List available functions names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(
[
getattr(bonus_sport, name).__name__
for name in dir(bonus_sport)
if name.startswith("bonus_")
]
)

View File

@ -41,6 +41,8 @@ class UniteEns(db.Model):
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
coefficient = db.Column(db.Float)
color = db.Column(db.Text())
# relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
modules = db.relationship("Module", lazy="dynamic", backref="ue")

View File

@ -35,13 +35,15 @@ Created on Thu Sep 8 09:36:33 2016
@author: barasc
"""
from __future__ import print_function
import os
import datetime
import re
import unicodedata
from flask import g
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.sco_logos import find_logo
@ -54,7 +56,6 @@ if not PE_DEBUG:
# kw is ignored. log always add a newline
log(" ".join(a))
else:
pe_print = print # print function
@ -206,7 +207,9 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
for name in logos_names:
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
if logo is not None:
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
add_local_file_to_zip(
zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
)
# ----------------------------------------------------------------------------------------

View File

@ -97,7 +97,7 @@ def pe_view_sem_recap(
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
template_latex = avis_tmpl_file.read()
template_latex = avis_tmpl_file.read().decode('utf-8')
template_latex = template_latex
else:
# template indiqué dans préférences ScoDoc ?
@ -114,7 +114,7 @@ def pe_view_sem_recap(
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read()
footer_latex = footer_tmpl_file.read().decode('utf-8')
footer_latex = footer_latex
else:
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(

View File

@ -73,7 +73,8 @@ def TrivialFormulator(
input_type : 'text', 'textarea', 'password',
'radio', 'menu', 'checkbox',
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
'boolcheckbox', 'text_suggest'
'boolcheckbox', 'text_suggest',
'color'
(default text)
size : text field width
rows, cols: textarea geometry
@ -594,6 +595,11 @@ class TF(object):
var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
"""
)
elif input_type == "color":
lem.append(
'<input type="color" name="%s" id="%s" %s' % (field, field, attribs)
)
lem.append(('value="%(' + field + ')s" >') % values)
else:
raise ValueError("unkown input_type for form (%s)!" % input_type)
explanation = descr.get("explanation", "")
@ -712,7 +718,11 @@ var {field}_as = new bsn.AutoSuggest('{field}', {field}_opts);
R.append("%s</td>" % title)
R.append('<td class="tf-ro-field%s">' % klass)
if input_type == "text" or input_type == "text_suggest":
if (
input_type == "text"
or input_type == "text_suggest"
or input_type == "color"
):
R.append(("%(" + field + ")s") % self.values)
elif input_type in ("radio", "menu", "checkbox", "boolcheckbox"):
if input_type == "boolcheckbox":

View File

@ -28,10 +28,12 @@
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);
- 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.
@ -77,7 +79,6 @@ def bonus_iutv(notes_sport, coefs, infos=None):
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.
"""
# breakpoint()
bonus = sum([(x - 10) / 20.0 for x in notes_sport if x > 10])
return bonus
@ -91,7 +92,7 @@ def bonus_direct(notes_sport, coefs, infos=None):
def bonus_iut_stdenis(notes_sport, coefs, infos=None):
"""Semblable à bonus_iutv mais sans coefficients et total limité à 0.5 points."""
"""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
@ -374,7 +375,7 @@ def bonus_iutBordeaux1(notes_sport, coefs, infos=None):
return bonus
def bonus_iuto(notes_sport, coefs, infos=None):
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

View File

@ -29,6 +29,7 @@
"""
from html.parser import HTMLParser
from html.entities import name2codepoint
from multiprocessing.sharedctypes import Value
import re
from flask import g, url_for
@ -36,17 +37,23 @@ from flask import g, url_for
from . import listhistogram
def horizontal_bargraph(value, mark):
def horizontal_bargraph(value, mark) -> str:
"""html drawing an horizontal bar and a mark
used to vizualize the relative level of a student
"""
tmpl = """
try:
vals = {"value": int(value), "mark": int(mark)}
except ValueError:
return ""
return (
"""
<span class="graph">
<span class="bar" style="width: %(value)d%%;"></span>
<span class="mark" style="left: %(mark)d%%; "></span>
</span>
"""
return tmpl % {"value": int(value), "mark": int(mark)}
% vals
)
def histogram_notes(notes):

View File

@ -170,7 +170,7 @@ class NotesTable:
"""
def __init__(self, formsemestre_id):
log(f"NotesTable( formsemestre_id={formsemestre_id} )")
# log(f"NotesTable( formsemestre_id={formsemestre_id} )")
# raise NotImplementedError() # XXX
if not formsemestre_id:
raise ValueError("invalid formsemestre_id (%s)" % formsemestre_id)
@ -788,7 +788,12 @@ class NotesTable:
moy_ue_cap = ue_cap["moy"]
mu["was_capitalized"] = True
event_date = event_date or ue_cap["event_date"]
if (moy_ue_cap != "NA") and (moy_ue_cap > max_moy_ue):
if (
(moy_ue_cap != "NA")
and isinstance(moy_ue_cap, float)
and isinstance(max_moy_ue, float)
and (moy_ue_cap > max_moy_ue)
):
# meilleure UE capitalisée
event_date = ue_cap["event_date"]
max_moy_ue = moy_ue_cap
@ -909,6 +914,7 @@ class NotesTable:
if len(coefs_bonus_gen) == 1:
coefs_bonus_gen = [1.0] # irrelevant, may be zero
# XXX attention: utilise anciens bonus_sport, évidemment
bonus_func = ScoDocSiteConfig.get_bonus_sport_func()
if bonus_func:
bonus = bonus_func(
@ -1328,7 +1334,11 @@ class NotesTable:
t[0] = results.etud_moy_gen[etudid]
for i, ue in enumerate(ues, start=1):
if ue["type"] != UE_SPORT:
t[i] = results.etud_moy_ue[ue["id"]][etudid]
# temporaire pour 9.1.29 !
if ue["id"] in results.etud_moy_ue:
t[i] = results.etud_moy_ue[ue["id"]][etudid]
else:
t[i] = ""
# re-trie selon la nouvelle moyenne générale:
self.T.sort(key=self._row_key)
# Remplace aussi le rang:

View File

@ -95,30 +95,21 @@ from flask import send_file
# Pour la détection auto de l'encodage des fichiers Apogée:
from chardet import detect as chardet_detect
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_codes_parcours import code_semestre_validant
from app.scodoc.sco_codes_parcours import (
ADC,
ADJ,
ADM,
AJ,
ATB,
ATJ,
ATT,
CMP,
DEF,
DEM,
NAR,
RAT,
)
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud
@ -132,24 +123,6 @@ APO_SEP = "\t"
APO_NEWLINE = "\r\n"
def code_scodoc_to_apo(code):
"""Conversion code jury ScoDoc en code Apogée"""
return {
ATT: "AJAC",
ATB: "AJAC",
ATJ: "AJAC",
ADM: "ADM",
ADJ: "ADM",
ADC: "ADMC",
AJ: "AJ",
CMP: "COMP",
"DEM": "NAR",
DEF: "NAR",
NAR: "NAR",
RAT: "ATT",
}.get(code, "DEF")
def _apo_fmt_note(note):
"Formatte une note pour Apogée (séparateur décimal: ',')"
if not note and isinstance(note, float):
@ -449,7 +422,7 @@ class ApoEtud(dict):
N=_apo_fmt_note(ue_status["moy"]),
B=20,
J="",
R=code_scodoc_to_apo(code_decision_ue),
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
M="",
)
else:
@ -475,13 +448,9 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid):
"""Calcul résultat apo semestre"""
# resultat du semestre
decision_apo = code_scodoc_to_apo(decision["code"])
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid)
if (
decision_apo == "DEF"
or decision["code"] == "DEM"
or decision["code"] == DEF
):
if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
note_str = "0,01" # note non nulle pour les démissionnaires
else:
note_str = _apo_fmt_note(note)
@ -520,21 +489,21 @@ class ApoEtud(dict):
# ou jury intermediaire et etudiant non redoublant...
return self.comp_elt_semestre(cur_nt, cur_decision, etudid)
decision_apo = code_scodoc_to_apo(cur_decision["code"])
decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"])
autre_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"])
autre_decision = autre_nt.get_etud_decision_sem(etudid)
if not autre_decision:
# pas de decision dans l'autre => pas de résultat annuel
return VOID_APO_RES
autre_decision_apo = code_scodoc_to_apo(autre_decision["code"])
autre_decision_apo = ScoDocSiteConfig.get_code_apo(autre_decision["code"])
if (
autre_decision_apo == "DEF"
or autre_decision["code"] == "DEM"
or autre_decision["code"] == DEM
or autre_decision["code"] == DEF
) or (
decision_apo == "DEF"
or cur_decision["code"] == "DEM"
or cur_decision["code"] == DEM
or cur_decision["code"] == DEF
):
note_str = "0,01" # note non nulle pour les démissionnaires

View File

@ -43,6 +43,7 @@ from flask import g, request
from flask import url_for
from flask_login import current_user
from flask_mail import Message
from app.models.moduleimpls import ModuleImplInscription
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
@ -285,21 +286,34 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
else:
I["rang_nt"], I["rang_txt"] = "", ""
I["note_max"] = 20.0 # notes toujours sur 20
I["bonus_sport_culture"] = nt.bonus[etudid]
I["bonus_sport_culture"] = nt.bonus[etudid] if nt.bonus is not None else 0.0
# Liste les UE / modules /evals
I["ues"] = []
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
):
continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
else:
x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True)
if nt.bonus is not None:
x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True)
else:
x = ""
if isinstance(x, str):
u["cur_moy_ue_txt"] = "pas de bonus"
if nt.bonus_ues is None:
u["cur_moy_ue_txt"] = "pas de bonus"
else:
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
else:
u["cur_moy_ue_txt"] = "bonus de %.3g points" % x
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
@ -369,7 +383,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
)
else:
if prefs["bul_show_ue_rangs"] and ue["type"] != sco_codes_parcours.UE_SPORT:
if ue_attente: # nt.get_moduleimpls_attente():
if ue_attente or nt.ue_rangs[ue["ue_id"]][0] is None:
u["ue_descr_txt"] = "%s/%s" % (
scu.RANG_ATTENTE_STR,
nt.ue_rangs[ue["ue_id"]][1],
@ -387,8 +401,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["ues"].append(u) # ne montre pas les UE si non inscrit
# Accès par matieres
# voir si on supporte encore cela en #sco92 XXX
# I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
# En #sco92, pas d'information
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
#
C = make_context_dict(I["sem"], I["etud"])
@ -605,12 +619,15 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
# Classement
if bul_show_mod_rangs and mod["mod_moy_txt"] != "-" and not is_malus:
rg = nt.mod_rangs[modimpl["moduleimpl_id"]]
if mod_attente: # nt.get_moduleimpls_attente():
mod["mod_rang"] = scu.RANG_ATTENTE_STR
if rg[0] is None:
mod["mod_rang_txt"] = ""
else:
mod["mod_rang"] = rg[0][etudid]
mod["mod_eff"] = rg[1] # effectif dans ce module
mod["mod_rang_txt"] = "%s/%s" % (mod["mod_rang"], mod["mod_eff"])
if mod_attente: # nt.get_moduleimpls_attente():
mod["mod_rang"] = scu.RANG_ATTENTE_STR
else:
mod["mod_rang"] = rg[0][etudid]
mod["mod_eff"] = rg[1] # effectif dans ce module
mod["mod_rang_txt"] = "%s/%s" % (mod["mod_rang"], mod["mod_eff"])
else:
mod["mod_rang_txt"] = ""
if mod_attente:

View File

@ -98,9 +98,9 @@ def formsemestre_bulletinetud_published_dict(
d = {}
if (not sem["bul_hide_xml"]) or force_publishing:
published = 1
published = True
else:
published = 0
published = False
if xml_nodate:
docdate = ""
else:
@ -192,7 +192,9 @@ def formsemestre_bulletinetud_published_dict(
)
d["note_max"] = dict(value=20) # notes toujours sur 20
d["bonus_sport_culture"] = dict(value=nt.bonus[etudid])
d["bonus_sport_culture"] = dict(
value=nt.bonus[etudid] if nt.bonus is not None else 0.0
)
# Liste les UE / modules /evals
d["ue"] = []

View File

@ -195,7 +195,12 @@ def make_xml_formsemestre_bulletinetud(
)
)
doc.append(Element("note_max", value="20")) # notes toujours sur 20
doc.append(Element("bonus_sport_culture", value=str(nt.bonus[etudid])))
doc.append(
Element(
"bonus_sport_culture",
value=str(nt.bonus[etudid] if nt.bonus is not None else 0.0),
)
)
# Liste les UE / modules /evals
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
@ -211,7 +216,7 @@ def make_xml_formsemestre_bulletinetud(
if ue["type"] != sco_codes_parcours.UE_SPORT:
v = ue_status["cur_moy_ue"]
else:
v = nt.bonus[etudid]
v = nt.bonus[etudid] if nt.bonus is not None else 0.0
x_ue.append(
Element(
"note",

View File

@ -98,8 +98,9 @@ class ScoDocCache:
status = CACHE.set(key, value, timeout=cls.timeout)
if not status:
log("Error: cache set failed !")
except:
except Exception as exc:
log("XXX CACHE Warning: error in set !!!")
log(exc)
status = None
return status

View File

@ -125,6 +125,7 @@ CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
NAR = "NAR"
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
DEM = "DEM"
# codes actions
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
@ -140,22 +141,26 @@ BUG = "BUG"
ALL = "ALL"
# Explication des codes (de demestre ou d'UE)
CODES_EXPL = {
ADM: "Validé",
ADC: "Validé par compensation",
ADJ: "Validé par le Jury",
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
ADM: "Validé",
AJ: "Ajourné",
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
AJ: "Ajourné",
NAR: "Echec, non autorisé à redoubler",
RAT: "En attente d'un rattrapage",
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
CMP: "Code UE acquise car semestre acquis",
DEF: "Défaillant",
NAR: "Échec, non autorisé à redoubler",
RAT: "En attente d'un rattrapage",
}
# Nota: ces explications sont personnalisables via le fichier
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
# variable: CONFIG.CODES_EXP
# Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente

View File

@ -152,28 +152,3 @@ class LogoInsert(Action):
name=self.parameters["name"],
dept_id=dept_id,
)
class BonusSportUpdate(Action):
"""Action: Change bonus_sport_function_name.
bonus_sport_function_name: the new value"""
def __init__(self, parameters):
super().__init__(
f"Changement du calcul de bonus sport pour ({parameters['bonus_sport_func_name']}).",
parameters,
)
@staticmethod
def build_action(parameters):
if (
parameters["bonus_sport_func_name"]
!= ScoDocSiteConfig.get_bonus_sport_func_name()
):
return [BonusSportUpdate(parameters)]
return []
def execute(self):
current_app.logger.info(self.message)
ScoDocSiteConfig.set_bonus_sport_func(self.parameters["bonus_sport_func_name"])
app.clear_scodoc_cache()

View File

@ -33,6 +33,7 @@ from flask_login import current_user
from app import db
from app.models import Formation, UniteEns, Matiere, Module, FormSemestre, ModuleImpl
from app.models.notes import ScolarFormSemestreValidation
from app.scodoc.sco_codes_parcours import UE_SPORT
import app.scodoc.sco_utils as scu
from app.scodoc import sco_groups
from app.scodoc.sco_utils import ModuleType
@ -99,12 +100,19 @@ def html_edit_formation_apc(
ressources_in_sem = ressources.filter_by(semestre_id=semestre_idx)
saes_in_sem = saes.filter_by(semestre_id=semestre_idx)
other_modules_in_sem = other_modules.filter_by(semestre_id=semestre_idx)
matiere_parent = Matiere.query.filter(
Matiere.ue_id == UniteEns.id,
UniteEns.formation_id == formation.id,
UniteEns.semestre_idx == semestre_idx,
UniteEns.type != UE_SPORT,
).first()
H += [
render_template(
"pn/form_mods.html",
formation=formation,
titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource",
matiere_parent=matiere_parent,
modules=ressources_in_sem,
module_type=ModuleType.RESSOURCE,
editable=editable,
@ -117,6 +125,7 @@ def html_edit_formation_apc(
formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ",
matiere_parent=matiere_parent,
modules=saes_in_sem,
module_type=ModuleType.SAE,
editable=editable,

View File

@ -32,15 +32,16 @@ import flask
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
from app import log
from app import models
from app.models import APO_CODE_STR_LEN
from app.models import Matiere, Module, UniteEns
from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app import log
from app import models
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
@ -144,7 +145,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
]
if is_apc:
H += [
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}</h2>"""
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}, Semestre {ue.semestre_idx}, {ue.acronyme}</h2>"""
]
else:
H += [
@ -190,35 +191,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
),
]
semestres_indices = list(range(1, parcours.NB_SEM + 1))
if is_apc: # BUT: choix de l'UE de rattachement (qui donnera le semestre)
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée pour la présentation dans certains documents",
"labels": [f"{u.acronyme} {u.titre}" for u in ues],
"allowed_values": [u.id for u in ues],
},
),
]
else:
# Formations classiques: choix du semestre
descr += [
(
"semestre_id",
{
"input_type": "menu",
"type": "int",
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s du module" % parcours.SESSION_NAME,
"labels": [str(x) for x in semestres_indices],
"allowed_values": semestres_indices,
},
),
]
descr += [
(
"module_type",
@ -294,6 +267,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
},
),
(
@ -316,12 +290,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
else:
if is_apc:
# BUT: l'UE indique le semestre
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
if selected_ue is None:
raise ValueError("UE invalide")
tf[2]["semestre_id"] = selected_ue.semestre_idx
tf[2]["semestre_id"] = ue.semestre_idx
_ = do_module_create(tf[2])
@ -472,18 +441,23 @@ def module_edit(module_id=None):
formation_id = module["formation_id"]
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
is_apc = parcours.APC_SAE
ues_matieres = ndb.SimpleDictFetch(
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
FROM notes_matieres mat, notes_ue ue
WHERE mat.ue_id = ue.id
AND ue.formation_id = %(formation_id)s
ORDER BY ue.numero, mat.numero
""",
{"formation_id": formation_id},
)
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
is_apc = parcours.APC_SAE # BUT
in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls
matieres = Matiere.query.filter(
Matiere.ue_id == UniteEns.id, UniteEns.formation_id == formation_id
).order_by(UniteEns.semestre_idx, UniteEns.numero, Matiere.numero)
if in_use:
# restreint aux matières du même semestre
matieres = matieres.filter(UniteEns.semestre_idx == a_module.ue.semestre_idx)
if is_apc:
mat_names = [
"S%s / %s" % (mat.ue.semestre_idx, mat.ue.acronyme) for mat in matieres
]
else:
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
semestres_indices = list(range(1, parcours.NB_SEM + 1))
@ -500,12 +474,25 @@ def module_edit(module_id=None):
),
"""<h2>Modification du module %(titre)s""" % module,
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
render_template("scodoc/help/modules.html", is_apc=is_apc),
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
).all(),
),
]
if not unlocked:
H.append(
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
)
if in_use:
H.append(
"""<div class="ue_warning"><span>Module déjà utilisé dans des semestres,
soyez prudents !
</span></div>"""
)
descr = [
(
@ -534,11 +521,17 @@ def module_edit(module_id=None):
),
(
"heures_cours",
{"size": 4, "type": "float", "explanation": "nombre d'heures de cours"},
{
"title": "Heures CM :",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de cours",
},
),
(
"heures_td",
{
"title": "Heures TD :",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Dirigés",
@ -547,6 +540,7 @@ def module_edit(module_id=None):
(
"heures_tp",
{
"title": "Heures TP :",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques",
@ -566,9 +560,9 @@ def module_edit(module_id=None):
"ue_coefs",
{
"readonly": True,
"title": "Coefficients vers les UE",
"title": "Coefficients vers les UE ",
"default": coefs_descr_txt,
"explanation": "passer par la page d'édition de la formation pour modifier les coefficients",
"explanation": " <br>(passer par la page d'édition de la formation pour modifier les coefficients)",
},
)
]
@ -594,7 +588,14 @@ def module_edit(module_id=None):
{
"input_type": "menu",
"title": "Rattachement :" if is_apc else "Matière :",
"explanation": "UE de rattachement, utilisée pour la présentation"
"explanation": (
"UE de rattachement"
+ (
" module utilisé, ne peut pas être changé de semestre"
if in_use
else ""
)
)
if is_apc
else "un module appartient à une seule matière.",
"labels": mat_names,
@ -666,8 +667,30 @@ def module_edit(module_id=None):
initvalues=module,
submitlabel="Modifier ce module",
)
# Affiche liste des formseemstre utilisant ce module
if in_use:
formsemestre_ids = {modimpl.formsemestre_id for modimpl in a_module.modimpls}
formsemestres = [FormSemestre.query.get(fid) for fid in formsemestre_ids]
formsemestres.sort(key=lambda f: f.date_debut)
items = [
f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=f.id )
}">{f.titre}</a>"""
for f in formsemestres
]
sem_descr = f"""
<div class="ue_warning">
<div>Ce module est utilisé dans les formsemestres suivants:</div>
<ul><li>
{"</li><li>".join( items )}
</li></ul>
</div>
"""
else:
sem_descr = ""
#
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
return "\n".join(H) + tf[1] + sem_descr + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for(
@ -678,8 +701,17 @@ def module_edit(module_id=None):
)
)
else:
# l'UE peut changer
# l'UE de rattachement peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
old_ue_id = a_module.ue.id
new_ue_id = int(tf[2]["ue_id"])
if (old_ue_id != new_ue_id) and in_use:
new_ue = UniteEns.query.get_or_404(new_ue_id)
if new_ue.semestre_idx != a_module.ue.semestre_idx:
# pas changer de semestre un module utilisé !
raise ScoValueError(
"Module utilisé: il ne peut pas être changé de semestre !"
)
# En APC, force le semestre égal à celui de l'UE
if is_apc:
selected_ue = UniteEns.query.get(tf[2]["ue_id"])

View File

@ -33,13 +33,15 @@ from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
from app import db
from app import log
from app.models import APO_CODE_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
@ -81,6 +83,7 @@ _ueEditor = ndb.EditableTable(
"is_external",
"code_apogee",
"coefficient",
"color",
),
sortkey="numero",
input_formators={
@ -330,7 +333,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
la moyenne générale</em> est activée. Par défaut, le coefficient
d'une UE est simplement la somme des coefficients des modules dans
lesquels l'étudiant a des notes.
Jamais utilisé en BUT.
""",
"enabled": not is_apc,
},
),
(
@ -358,6 +363,14 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
"explanation": "réservé pour les capitalisations d'UE effectuées à l'extérieur de l'établissement",
},
),
(
"color",
{
"input_type": "color",
"title": "Couleur",
"explanation": "pour affichages",
},
),
]
if create and not parcours.UE_IS_MODULE and not is_apc:
fw.append(
@ -379,9 +392,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
submitlabel=submitlabel,
)
if tf[0] == 0:
X = """<div id="ue_list_code"></div>
"""
return "\n".join(H) + tf[1] + X + html_sco_header.sco_footer()
ue_div = """<div id="ue_list_code"></div>"""
bonus_div = """<div id="bonus_description"></div>"""
return "\n".join(H) + tf[1] + bonus_div + ue_div + html_sco_header.sco_footer()
else:
if create:
if not tf[2]["ue_code"]:
@ -533,6 +546,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
# pour faciliter la transition des anciens programmes non APC
for ue in ues_obj:
ue.guess_semestre_idx()
# vérifie qu'on a bien au moins une matière dans chaque UE
for ue in ues_obj:
if ue.matieres.count() < 1:
mat = Matiere(ue_id=ue.id)
db.session.add(mat)
db.session.commit()
ues = [ue.to_dict() for ue in ues_obj]
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
@ -1220,7 +1239,8 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
def edit_ue_set_code_apogee(id=None, value=None):
"set UE code apogee"
ue_id = id
value = value.strip("-_ \t")
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
ues = ue_list(args={"ue_id": ue_id})

View File

@ -185,7 +185,8 @@ def _check_evaluation_args(args):
if (jour > date_fin) or (jour < date_debut):
raise ScoValueError(
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
% (d, m, y)
% (d, m, y),
dest_url="javascript:history.back();",
)
heure_debut = args.get("heure_debut", None)
args["heure_debut"] = heure_debut

View File

@ -143,6 +143,7 @@ def evaluation_create_form(
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = []
#
ue_coef_dict = {}
if is_apc: # BUT: poids vers les UE
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
for ue in sem_ues:
@ -290,7 +291,10 @@ def evaluation_create_form(
"title": f"Poids {ue.acronyme}",
"size": 2,
"type": "float",
"explanation": f"{ue.titre}",
"explanation": f"""
<span class="eval_coef_ue" title="coef. du module dans cette UE">{ue_coef_dict.get(ue.id, 0.)}</span>
<span class="eval_coef_ue_titre">{ue.titre}</span>
""",
"allow_null": False,
},
),

View File

@ -36,8 +36,19 @@ class ScoException(Exception):
pass
class NoteProcessError(ScoException):
"misc errors in process"
class InvalidNoteValue(ScoException):
pass
# Exception qui stoque dest_url
class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None):
super().__init__(msg)
self.dest_url = dest_url
class NoteProcessError(ScoValueError):
"Valeurs notes invalides"
pass
@ -45,17 +56,6 @@ class InvalidEtudId(NoteProcessError):
pass
class InvalidNoteValue(ScoException):
pass
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message
class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None):
super().__init__(msg)
self.dest_url = dest_url
class ScoFormatError(ScoValueError):
pass

View File

@ -28,13 +28,16 @@
"""Form choix modules / responsables et creation formsemestre
"""
import flask
from flask import url_for, g, request
from flask import url_for, flash
from flask import g, request
from flask_login import current_user
from app import db
from app.auth.models import User
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns
from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc import sco_cache
@ -65,9 +68,9 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_users
def _default_sem_title(F):
"""Default title for a semestre in formation F"""
return F["titre"]
def _default_sem_title(formation):
"""Default title for a semestre in formation"""
return formation.titre
def formsemestre_createwithmodules():
@ -140,6 +143,7 @@ def do_formsemestre_createwithmodules(edit=False):
if edit:
formsemestre_id = int(vals["formsemestre_id"])
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not current_user.has_permission(Permission.ScoImplement):
if not edit:
# il faut ScoImplement pour creer un semestre
@ -161,26 +165,25 @@ def do_formsemestre_createwithmodules(edit=False):
allowed_user_names = list(uid2display.values()) + [""]
#
formation_id = int(vals["formation_id"])
F = sco_formations.formation_list(args={"formation_id": formation_id})
if not F:
formation = Formation.query.get(formation_id)
if formation is None:
raise ScoValueError("Formation inexistante !")
F = F[0]
if not edit:
initvalues = {"titre": _default_sem_title(F)}
initvalues = {"titre": _default_sem_title(formation)}
semestre_id = int(vals["semestre_id"])
sem_module_ids = set()
module_ids_set = set()
else:
# setup form init values
initvalues = sem
semestre_id = initvalues["semestre_id"]
# add associated modules to tf-checked:
ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
sem_module_ids = set([x["module_id"] for x in ams])
initvalues["tf-checked"] = ["MI" + str(x["module_id"]) for x in ams]
for x in ams:
initvalues["MI" + str(x["module_id"])] = uid2display.get(
x["responsable_id"],
f"inconnu numéro {x['responsable_id']} resp. de {x['moduleimpl_id']} !",
module_ids_existing = [modimpl.module.id for modimpl in formsemestre.modimpls]
module_ids_set = set(module_ids_existing)
initvalues["tf-checked"] = ["MI" + str(x) for x in module_ids_existing]
for modimpl in formsemestre.modimpls:
initvalues[f"MI{modimpl.module.id}"] = uid2display.get(
modimpl.responsable_id,
f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !",
)
initvalues["responsable_id"] = uid2display.get(
@ -192,49 +195,38 @@ def do_formsemestre_createwithmodules(edit=False):
)
# Liste des ID de semestres
if F["type_parcours"] is not None:
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
if formation.type_parcours is not None:
parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours)
NB_SEM = parcours.NB_SEM
else:
NB_SEM = 10 # fallback, max 10 semestres
if NB_SEM == 1:
semestre_id_list = [-1]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
if edit and formation.is_apc():
# en APC, ne permet pas de changer de semestre
semestre_id_list = [formsemestre.semestre_id]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
semestre_id_labels = []
for sid in semestre_id_list:
if sid == -1:
semestre_id_labels.append("pas de semestres")
else:
semestre_id_labels.append(f"S{sid}")
# Liste des modules dans ce semestre de cette formation
# on pourrait faire un simple module_list( )
# mais si on veut l'ordre du PPN (groupe par UE et matieres) il faut:
mods = [] # liste de dicts
uelist = sco_edit_ue.ue_list({"formation_id": formation_id})
for ue in uelist:
matlist = sco_edit_matiere.matiere_list({"ue_id": ue["ue_id"]})
for mat in matlist:
modsmat = sco_edit_module.module_list({"matiere_id": mat["matiere_id"]})
# XXX debug checks
for m in modsmat:
if m["ue_id"] != ue["ue_id"]:
log(
"XXX createwithmodules: m.ue_id=%s != u.ue_id=%s !"
% (m["ue_id"], ue["ue_id"])
)
if m["formation_id"] != formation_id:
log(
"XXX createwithmodules: formation_id=%s\n\tm=%s"
% (formation_id, str(m))
)
if m["formation_id"] != ue["formation_id"]:
log(
"XXX createwithmodules: formation_id=%s\n\tue=%s\tm=%s"
% (formation_id, str(ue), str(m))
)
# /debug
mods = mods + modsmat
# Liste des modules dans cette formation
if formation.is_apc():
modules = formation.modules.order_by(Module.module_type, Module.numero)
else:
modules = (
Module.query.filter(
Module.formation_id == formation_id, UniteEns.id == Module.ue_id
)
.order_by(Module.module_type, UniteEns.numero, Module.numero)
.all()
)
mods = [mod.to_dict() for mod in modules]
# Pour regroupement des modules par semestres:
semestre_ids = {}
for mod in mods:
@ -319,7 +311,7 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans
le titre: ils seront automatiquement ajoutés <input type="button"
value="remettre titre par défaut" onClick="document.tf.titre.value='%s';"/>"""
% _default_sem_title(F),
% _default_sem_title(formation),
},
),
(
@ -340,6 +332,9 @@ def do_formsemestre_createwithmodules(edit=False):
"title": "Semestre dans la formation",
"allowed_values": semestre_id_list,
"labels": semestre_id_labels,
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc()
else "",
},
),
)
@ -549,7 +544,12 @@ def do_formsemestre_createwithmodules(edit=False):
)
)
for mod in mods:
if mod["semestre_id"] == semestre_id:
if mod["semestre_id"] == semestre_id and (
(not edit) # creation => tous modules
or (not formation.is_apc()) # pas BUT, on peux mixer les semestres
or (semestre_id == formsemestre.semestre_id) # module du semestre
or (mod["module_id"] in module_ids_set) # module déjà présent
):
nbmod += 1
if edit:
select_name = "%s!group_id" % mod["module_id"]
@ -560,7 +560,7 @@ def do_formsemestre_createwithmodules(edit=False):
else:
return ""
if mod["module_id"] in sem_module_ids:
if mod["module_id"] in module_ids_set:
disabled = "disabled"
else:
disabled = ""
@ -684,12 +684,13 @@ def do_formsemestre_createwithmodules(edit=False):
msg = '<ul class="tf-msg"><li class="tf-msg">Code étape Apogée manquant</li></ul>'
if tf[0] == 0 or msg:
return (
'<p>Formation <a class="discretelink" href="ue_table?formation_id=%(formation_id)s"><em>%(titre)s</em> (%(acronyme)s), version %(version)s, code %(formation_code)s</a></p>'
% F
+ msg
+ str(tf[1])
)
return f"""<p>Formation <a class="discretelink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}"><em>{formation.titre}</em> ({formation.acronyme}), version {formation.version}, code {formation.formation_code}</a>
</p>
{msg}
{tf[1]}
"""
elif tf[0] == -1:
return "<h4>annulation</h4>"
else:
@ -735,42 +736,58 @@ def do_formsemestre_createwithmodules(edit=False):
etape=tf[2]["etape_apo" + str(n)], vdi=tf[2]["vdi_apo" + str(n)]
)
)
# Modules sélectionnés:
# (retire le "MI" du début du nom de champs)
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
if not edit:
# creation du semestre
if formation.is_apc():
_formsemestre_check_module_list(
module_ids_checked, tf[2]["semestre_id"]
)
# création du semestre
formsemestre_id = sco_formsemestre.do_formsemestre_create(tf[2])
# creation des modules
for module_id in tf[2]["tf-checked"]:
assert module_id[:2] == "MI"
# création des modules
for module_id in module_ids_checked:
modargs = {
"module_id": int(module_id[2:]),
"module_id": module_id,
"formsemestre_id": formsemestre_id,
"responsable_id": tf[2][module_id],
"responsable_id": tf[2][f"MI{module_id}"],
}
_ = sco_moduleimpl.do_moduleimpl_create(modargs)
flash("Nouveau semestre créé")
return flask.redirect(
"formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
% formsemestre_id
url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
else:
# modification du semestre:
# Modification du semestre:
# on doit creer les modules nouvellement selectionnés
# modifier ceux a modifier, et DETRUIRE ceux qui ne sont plus selectionnés.
# Note: la destruction echouera s'il y a des objets dependants
# (eg des evaluations définies)
# nouveaux modules
# (retire le "MI" du début du nom de champs)
checkedmods = [int(x[2:]) for x in tf[2]["tf-checked"]]
# modifier ceux à modifier, et DETRUIRE ceux qui ne sont plus selectionnés.
# Note: la destruction échouera s'il y a des objets dépendants
# (eg des évaluations définies)
module_ids_tocreate = [
x for x in module_ids_checked if not x in module_ids_existing
]
if formation.is_apc():
_formsemestre_check_module_list(
module_ids_tocreate, tf[2]["semestre_id"]
)
# modules existants à modifier
module_ids_toedit = [
x for x in module_ids_checked if x in module_ids_existing
]
# modules à détruire
module_ids_todelete = [
x for x in module_ids_existing if not x in module_ids_checked
]
#
sco_formsemestre.do_formsemestre_edit(tf[2])
ams = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
existingmods = [x["module_id"] for x in ams]
mods_tocreate = [x for x in checkedmods if not x in existingmods]
# modules a existants a modifier
mods_toedit = [x for x in checkedmods if x in existingmods]
# modules a detruire
mods_todelete = [x for x in existingmods if not x in checkedmods]
#
msg = []
for module_id in mods_tocreate:
for module_id in module_ids_tocreate:
modargs = {
"module_id": module_id,
"formsemestre_id": formsemestre_id,
@ -808,9 +825,11 @@ def do_formsemestre_createwithmodules(edit=False):
% (module_id, moduleimpl_id)
)
#
ok, diag = formsemestre_delete_moduleimpls(formsemestre_id, mods_todelete)
ok, diag = formsemestre_delete_moduleimpls(
formsemestre_id, module_ids_todelete
)
msg += diag
for module_id in mods_toedit:
for module_id in module_ids_toedit:
moduleimpl_id = sco_moduleimpl.moduleimpl_list(
formsemestre_id=formsemestre_id, module_id=module_id
)[0]["moduleimpl_id"]
@ -847,6 +866,22 @@ def do_formsemestre_createwithmodules(edit=False):
)
def _formsemestre_check_module_list(module_ids, semestre_idx):
"""En APC: Vérifie que tous les modules de la liste
sont dans le semestre indiqué.
Sinon, raise ScoValueError.
"""
# vérification de la cohérence / modules / semestre
mod_sems_idx = {
Module.query.get_or_404(module_id).ue.semestre_idx for module_id in module_ids
}
if mod_sems_idx and mod_sems_idx != {semestre_idx}:
raise ScoValueError(
"Les modules sélectionnés ne sont pas tous dans le semestre choisi !",
dest_url="javascript:history.back();",
)
def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
"""Delete moduleimpls
module_ids_to_del: list of module_id (warning: not moduleimpl)

View File

@ -108,10 +108,10 @@ def _build_menu_stats(formsemestre_id):
"enabled": True,
},
{
"title": "Documents Avis Poursuite Etudes",
"title": "Documents Avis Poursuite Etudes (xp)",
"endpoint": "notes.pe_view_sem_recap",
"args": {"formsemestre_id": formsemestre_id},
"enabled": current_app.config["TESTING"] or current_app.config["DEBUG"],
"enabled": True, # current_app.config["TESTING"] or current_app.config["DEBUG"],
},
{
"title": 'Table "débouchés"',
@ -1107,6 +1107,7 @@ _TABLEAU_MODULES_HEAD = """
<th class="formsemestre_status">Module</th>
<th class="formsemestre_status">Inscrits</th>
<th class="resp">Responsable</th>
<th class="coef">Coefs.</th>
<th class="evals">Évaluations</th>
</tr>
"""
@ -1213,7 +1214,21 @@ def formsemestre_tableau_modules(
sco_users.user_info(modimpl["responsable_id"])["prenomnom"],
)
)
H.append("<td>")
if mod.module_type in (ModuleType.RESSOURCE, ModuleType.SAE):
coefs = mod.ue_coefs_list()
for coef in coefs:
if coef[1] > 0:
H.append(
f"""<span class="mod_coef_indicator"
title="{coef[0].acronyme}"
style="background: {
coef[0].color if coef[0].color is not None else 'blue'
}"></span>"""
)
else:
H.append(f"""<span class="mod_coef_indicator_zero"></span>""")
H.append("</td>")
if mod.module_type in (
None, # ne devrait pas être nécessaire car la migration a remplacé les NULLs
ModuleType.STANDARD,

View File

@ -748,7 +748,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
)
# Choix code semestre:
codes = list(sco_codes_parcours.CODES_EXPL.keys())
codes = list(sco_codes_parcours.CODES_JURY_SEM)
codes.sort() # fortuitement, cet ordre convient bien !
H.append(

View File

@ -87,7 +87,7 @@ groupEditor = ndb.EditableTable(
group_list = groupEditor.list
def get_group(group_id):
def get_group(group_id: int):
"""Returns group object, with partition"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
@ -687,6 +687,11 @@ def setGroups(
group_id = fs[0].strip()
if not group_id:
continue
try:
group_id = int(group_id)
except ValueError as exc:
log("setGroups: ignoring invalid group_id={group_id}")
continue
group = get_group(group_id)
# Anciens membres du groupe:
old_members = get_group_members(group_id)

View File

@ -49,9 +49,11 @@ from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoValueError
def list_authorized_etuds_by_sem(sem, delai=274):
def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
"""Liste des etudiants autorisés à s'inscrire dans sem.
delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
ignore_jury: si vrai, considère tous les étudiants comem autorisés, même
s'ils n'ont pas de décision de jury.
"""
src_sems = list_source_sems(sem, delai=delai)
inscrits = list_inscrits(sem["formsemestre_id"])
@ -59,7 +61,12 @@ def list_authorized_etuds_by_sem(sem, delai=274):
candidats = {} # etudid : etud (tous les etudiants candidats)
nb = 0 # debug
for src in src_sems:
liste = list_etuds_from_sem(src, sem)
if ignore_jury:
# liste de tous les inscrits au semestre (sans dems)
liste = list_inscrits(src["formsemestre_id"]).values()
else:
# liste des étudiants autorisés par le jury à s'inscrire ici
liste = list_etuds_from_sem(src, sem)
liste_filtree = []
for e in liste:
# Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
@ -125,7 +132,7 @@ def list_inscrits(formsemestre_id, with_dems=False):
return inscr
def list_etuds_from_sem(src, dst):
def list_etuds_from_sem(src, dst) -> list[dict]:
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
target = dst["semestre_id"]
dpv = sco_pvjury.dict_pvjury(src["formsemestre_id"])
@ -224,7 +231,7 @@ def do_desinscrit(sem, etudids):
)
def list_source_sems(sem, delai=None):
def list_source_sems(sem, delai=None) -> list[dict]:
"""Liste des semestres sources
sem est le semestre destination
"""
@ -265,6 +272,7 @@ def formsemestre_inscr_passage(
inscrit_groupes=False,
submitted=False,
dialog_confirmed=False,
ignore_jury=False,
):
"""Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id).
@ -280,6 +288,7 @@ def formsemestre_inscr_passage(
"""
inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# -- check lock
if not sem["etat"]:
@ -295,7 +304,9 @@ def formsemestre_inscr_passage(
elif etuds and isinstance(etuds[0], str):
etuds = [int(x) for x in etuds]
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(
sem, ignore_jury=ignore_jury
)
etuds_set = set(etuds)
candidats_set = set(candidats)
inscrits_set = set(inscrits)
@ -323,6 +334,7 @@ def formsemestre_inscr_passage(
candidats_non_inscrits,
inscrits_ailleurs,
inscrit_groupes=inscrit_groupes,
ignore_jury=ignore_jury,
)
else:
if not dialog_confirmed:
@ -363,6 +375,7 @@ def formsemestre_inscr_passage(
"formsemestre_id": formsemestre_id,
"etuds": ",".join([str(x) for x in etuds]),
"inscrit_groupes": inscrit_groupes,
"ignore_jury": ignore_jury,
"submitted": 1,
},
)
@ -411,18 +424,23 @@ def build_page(
candidats_non_inscrits,
inscrits_ailleurs,
inscrit_groupes=False,
ignore_jury=False,
):
inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
if inscrit_groupes:
inscrit_groupes_checked = " checked"
else:
inscrit_groupes_checked = ""
if ignore_jury:
ignore_jury_checked = " checked"
else:
ignore_jury_checked = ""
H = [
html_sco_header.html_sem_header(
"Passages dans le semestre", with_page_header=False
),
"""<form method="post" action="%s">""" % request.base_url,
"""<form name="f" method="post" action="%s">""" % request.base_url,
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a>
@ -430,6 +448,8 @@ def build_page(
% sem, # "
"""<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
% inscrit_groupes_checked,
"""<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" %s>inclure tous les étudiants (même sans décision de jury)</input>"""
% ignore_jury_checked,
"""<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits
et %d candidats supplémentaires
</div>"""

View File

@ -37,7 +37,10 @@ from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.comp import res_sem
from app.comp import moy_mod
from app.comp.moy_mod import ModuleImplResults
from app.comp.res_common import NotesTableCompat
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
@ -432,7 +435,7 @@ def _make_table_notes(
if is_apc:
# Ajoute une colonne par UE
_add_apc_columns(
moduleimpl_id,
modimpl,
evals_poids,
ues,
rows,
@ -815,7 +818,7 @@ def _add_moymod_column(
def _add_apc_columns(
moduleimpl_id,
modimpl,
evals_poids,
ues,
rows,
@ -834,18 +837,23 @@ def _add_apc_columns(
# => On recharge tout dans les nouveaux modèles
# rows est une liste de dict avec une clé "etudid"
# on va y ajouter une clé par UE du semestre
modimpl = ModuleImpl.query.get(moduleimpl_id)
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
moduleimpl_id
)
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
nt: NotesTableCompat = res_sem.load_formsemestre_result(modimpl.formsemestre)
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl.id]
# XXX A ENLEVER TODO
# modimpl = ModuleImpl.query.get(moduleimpl_id)
# evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
# moduleimpl_id
# )
# etuds_moy_module = moy_mod.compute_module_moy(
# evals_notes, evals_poids, evaluations, evaluations_completes
# )
if is_conforme:
# valeur des moyennes vers les UEs:
for row in rows:
for ue in ues:
moy_ue = etuds_moy_module[ue.id].get(row["etudid"], "?")
moy_ue = modimpl_results.etuds_moy_module[ue.id].get(row["etudid"], "?")
row[f"moy_ue_{ue.id}"] = scu.fmt_note(moy_ue, keep_numeric=keep_numeric)
row[f"_moy_ue_{ue.id}_class"] = "moy_ue"
# Nom et coefs des UE (lignes titres):

View File

@ -567,17 +567,17 @@ def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT mi.moduleimpl_id
"""SELECT mi.id
FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
WHERE sem.formsemestre_id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.formsemestre_id
AND mod.module_id = mi.module_id
WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.id
AND mod.id = mi.module_id
AND mod.ue_id = %(ue_id)s
""",
{"formsemestre_id": formsemestre_id, "ue_id": ue_id},
)
res = cursor.dictfetchall()
for moduleimpl_id in [x["moduleimpl_id"] for x in res]:
for moduleimpl_id in [x["id"] for x in res]:
sco_moduleimpl.do_moduleimpl_inscription_create(
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
formsemestre_id=formsemestre_id,

View File

@ -171,7 +171,9 @@ def _ue_coefs_html(coefs_lst) -> str:
"""
+ "\n".join(
[
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue.acronyme}</div>"""
f"""<div style="--coef:{coef};
{'background-color: ' + ue.color + ';' if ue.color else ''}
"><div>{coef}</div>{ue.acronyme}</div>"""
for ue, coef in coefs_lst
]
)
@ -286,21 +288,16 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
'<tr><td class="fichetitre2" colspan="4">Règle de calcul: <span class="formula" title="mode de calcul de la moyenne du module">moyenne=<tt>%s</tt></span>'
% M["computation_expr"]
)
if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
H.append(
'<span class="fl"><a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id=%s">modifier</a></span>'
% moduleimpl_id
)
H.append('<span class="warning">inutilisée dans cette version de ScoDoc</span>')
H.append("</td></tr>")
else:
H.append(
'<tr><td colspan="4"><em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
'<tr><td colspan="4">' # <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
)
if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
H.append(
' (<a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id=%s">changer</a>)'
% moduleimpl_id
)
# if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
# H.append(
# f' (<a class="stdlink" href="edit_moduleimpl_expr?moduleimpl_id={moduleimpl_id}">changer</a>)'
# )
H.append("</td></tr>")
H.append(
'<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink" href="view_module_abs?moduleimpl_id=%s">Absences dans ce module</a></span>'
@ -399,7 +396,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
eval_index = len(mod_evals) - 1
first_eval = True
for eval in mod_evals:
evaluation = Evaluation.query.get(eval["evaluation_id"]) # TODO unifier
evaluation: Evaluation = Evaluation.query.get(
eval["evaluation_id"]
) # TODO unifier
etat = sco_evaluations.do_evaluation_etat(
eval["evaluation_id"],
partition_id=partition_id,

View File

@ -37,7 +37,7 @@ _SCO_PERMISSIONS = (
(1 << 21, "ScoEditPVJury", "Éditer les PV de jury"),
# ajouter maquettes Apogee (=> chef dept et secr):
(1 << 22, "ScoEditApo", "Ajouter des maquettes Apogées"),
# application relations entreprises
# Application relations entreprises
(1 << 23, "RelationsEntreprisesView", "Voir l'application relations entreprises"),
(1 << 24, "RelationsEntreprisesChange", "Modifier les entreprises"),
(

View File

@ -169,7 +169,9 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
if doc:
break
if not doc:
raise ScoValueError("pas de réponse du portail ! (timeout=%s)" % portal_timeout)
raise ScoValueError(
f"pas de réponse du portail ! <br>(timeout={portal_timeout}, requête: <tt>{req}</tt>)"
)
etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
# Filtre sur annee inscription Apogee:

View File

@ -111,8 +111,9 @@ get_base_preferences(formsemestre_id)
"""
import flask
from flask import g, url_for, request
from flask_login import current_user
from flask import g, request, current_app
# from flask_login import current_user
from app.models import Departement
from app.scodoc import sco_cache
@ -1537,7 +1538,7 @@ class BasePreferences(object):
(
"email_from_addr",
{
"initvalue": "noreply@scodoc.example.com",
"initvalue": current_app.config["SCODOC_MAIL_FROM"],
"title": "adresse mail origine",
"size": 40,
"explanation": "adresse expéditeur pour les envois par mails (bulletins)",
@ -2018,7 +2019,7 @@ class BasePreferences(object):
H = [
html_sco_header.sco_header(page_title="Préférences"),
"<h2>Préférences globales pour %s</h2>" % scu.ScoURL(),
# f"""<p><a href="{url_for("scolar.config_logos", scodoc_dept=g.scodoc_dept)
# f"""<p><a href="{url_for("scodoc.configure_logos", scodoc_dept=g.scodoc_dept)
# }">modification des logos du département (pour documents pdf)</a></p>"""
# if current_user.is_administrator()
# else "",

View File

@ -566,7 +566,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
if "prev_decision" in row and row["prev_decision"]:
counts[row["prev_decision"]] += 0
# Légende des codes
codes = list(counts.keys()) # sco_codes_parcours.CODES_EXPL.keys()
codes = list(counts.keys())
codes.sort()
H.append("<h3>Explication des codes</h3>")
lines = []

View File

@ -58,8 +58,31 @@ SCO_ROLES_DEFAULTS = {
# il peut ajouter des tags sur les formations:
# (doit avoir un rôle Ens en plus !)
"RespPe": (p.ScoEditFormationTags,),
# Rôles pour l'application relations entreprises
# ObservateurEntreprise est un observateur de l'application entreprise
"ObservateurEntreprise": (p.RelationsEntreprisesView,),
# UtilisateurEntreprise est un utilisateur de l'application entreprise (droit de modification)
"UtilisateurEntreprise": (p.RelationsEntreprisesView, p.RelationsEntreprisesChange),
# AdminEntreprise est un admin de l'application entreprise (toutes les actions possibles de l'application)
"AdminEntreprise": (
p.RelationsEntreprisesView,
p.RelationsEntreprisesChange,
p.RelationsEntreprisesExport,
p.RelationsEntreprisesSend,
p.RelationsEntreprisesValidate,
),
# Super Admin est un root: création/suppression de départements
# _tous_ les droits
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
"SuperAdmin": p.ALL_PERMISSIONS,
}
# Les rôles accessibles via la page d'admin utilisateurs
# - associés à un département:
ROLES_ATTRIBUABLES_DEPT = ("Ens", "Secr", "Admin", "RespPe")
# - globaux: (ne peuvent être attribués que par un SuperAdmin)
ROLES_ATTRIBUABLES_SCODOC = (
"ObservateurEntreprise",
"UtilisateurEntreprise",
"AdminEntreprise",
)

View File

@ -153,7 +153,10 @@ def _check_notes(notes, evaluation, mod):
for (etudid, note) in notes:
note = str(note).strip().upper()
etudid = int(etudid) #
try:
etudid = int(etudid) #
except ValueError as exc:
raise ScoValueError(f"Code étudiant ({etudid}) invalide")
if note[:3] == "DEM":
continue # skip !
if note:
@ -487,10 +490,10 @@ def notes_add(
}
for (etudid, value) in notes:
if check_inscription and (etudid not in inscrits):
raise NoteProcessError("etudiant non inscrit dans ce module")
if not ((value is None) or (type(value) == type(1.0))):
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
if (value is not None) and not isinstance(value, float):
raise NoteProcessError(
"etudiant %s: valeur de note invalide (%s)" % (etudid, value)
f"etudiant {etudid}: valeur de note invalide ({value})"
)
# Recherche notes existantes
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)

View File

@ -181,7 +181,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
r = ndb.SimpleDictFetch(
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
notes_modules mod, identite i, "user" u
WHERE mi.id = e.moduleimpl_id
@ -202,6 +202,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"value",
"user_name",
"titre",
"evaluation_id",
"description",
"jour",
"comment",
@ -214,6 +215,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"value": "Note",
"comment": "Remarque",
"user_name": "Enseignant",
"evaluation_id": "evaluation_id",
"titre": "Module",
"description": "Evaluation",
"jour": "Date éval.",

View File

@ -1,298 +1,322 @@
/* Bulletin BUT, Seb. L. 2021-12-06 */
/*******************/
/* Styles généraux */
/*******************/
.wait{
width: 60px;
height: 6px;
margin: auto;
background: #424242; /* la réponse à tout */
animation: wait .4s infinite alternate;
}
@keyframes wait{
100%{transform: translateY(40px) rotate(1turn);}
}
main{
--couleurPrincipale: rgb(240,250,255);
--couleurFondTitresUE: rgb(206,255,235);
--couleurFondTitresRes: rgb(125, 170, 255);
--couleurFondTitresSAE: rgb(211, 255, 255);
--couleurSecondaire: #fec;
--couleurIntense: #c09;
--couleurSurlignage: rgba(232, 255, 132, 0.47);
max-width: 1000px;
margin: auto;
display: none;
}
.ready .wait{display: none;}
.ready main{display: block;}
h2{
margin: 0;
color: black;
}
section{
background: #FFF;
border-radius: 16px;
border: 1px solid #AAA;
padding: 16px 32px;
margin: 8px 0;
}
section>div:nth-child(1){
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.CTA_Liste{
display: flex;
gap: 4px;
align-items: center;
background: var(--couleurIntense);
color: #FFF;
padding: 4px 8px;
border-radius: 4px;
box-shadow: 0 2px 2px rgba(0,0,0,0.26);
cursor: pointer;
}
.CTA_Liste>svg{
transition: 0.2s;
}
.CTA_Liste:hover{
outline: 2px solid #424242;
}
.listeOff svg{
transform: rotate(180deg);
}
.listeOff .syntheseModule,
.listeOff .eval{
display: none;
}
.moduleOnOff>.syntheseModule,
.moduleOnOff>.eval{
display: none;
}
.listeOff .moduleOnOff>.syntheseModule,
.listeOff .moduleOnOff>.eval{
display: flex !important;
}
.listeOff .ue::before,
.listeOff .module::before,
.moduleOnOff .ue::before,
.moduleOnOff .module::before{
transform: rotate(0);
}
.listeOff .moduleOnOff .ue::before,
.listeOff .moduleOnOff .module::before{
transform: rotate(180deg) !important;
}
/***********************/
/* Options d'affichage */
/***********************/
.hide_abs .absences,
.hide_abs_modules .module>.absences,
.hide_coef .synthese em,
.hide_coef .eval>em,
.hide_date_inscr .dateInscription,
.hide_ects .ects{
display: none;
}
.module>.absences,
.module .moyenne,
.module .info{
display: none;
}
/************/
/* Etudiant */
/************/
.info_etudiant{
color: #000;
text-decoration: none;
}
.etudiant{
display: flex;
align-items: center;
gap: 16px;
border-color: var(--couleurPrincipale);
background: var(--couleurPrincipale);
color: rgb(0, 0, 0);
}
.civilite{
font-weight: bold;
font-size: 130%;
}
/************/
/* Semestre */
/************/
.flex{
display: flex;
gap: 16px;
}
.infoSemestre{
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 4px;
flex: none;
}
.infoSemestre>div{
border: 1px solid var(--couleurIntense);
padding: 4px 8px;
border-radius: 4px;
display: grid;
grid-template-columns: auto auto;
column-gap: 4px;
}
.infoSemestre>div:nth-child(1){
margin-right: auto;
}
.infoSemestre>div>div:nth-child(even){
text-align: right;
}
.rang{
text-decoration: underline var(--couleurIntense);
}
.decision{
margin: 5px 0;
font-weight: bold;
font-size: 20px;
text-decoration: underline var(--couleurIntense);
}
.enteteSemestre{
color: black;
font-weight: bold;
font-size: 20px;
margin-bottom: 4px;
}
/***************/
/* Synthèse */
/***************/
.synthese .ue,
.synthese h3{
background: var(--couleurFondTitresUE);
}
.synthese em,
.eval em{
opacity: 0.6;
min-width: 80px;
display: inline-block;
}
/***************/
/* Evaluations */
/***************/
.module, .ue {
background: var(--couleurSecondaire);
color: #000;
padding: 4px 32px;
border-radius: 4px;
display: flex;
gap: 16px;
margin: 4px 0 2px 0;
overflow-x: auto;
overflow-y: hidden;
cursor: pointer;
position: relative;
}
.module::before, .ue::before {
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='black'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
width: 26px;
height: 26px;
position: absolute;
bottom: 0;
left: 50%;
margin-left: -13px;
transform: rotate(180deg);
transition: 0.2s;
}
h3{
display: flex;
align-items: center;
margin: 0 auto 0 0;
position: sticky;
left: -32px;
z-index: 1;
font-size: 16px;
background: var(--couleurSecondaire);
}
.sae .module, .sae h3{
background: var(--couleurFondTitresSAE);
}
.moyenne{
font-weight: bold;
text-align: right;
}
.info{
opacity: 0.9;
}
.syntheseModule{
cursor: pointer;
}
.eval, .syntheseModule{
position: relative;
display: flex;
justify-content: space-between;
margin: 0 0 0 28px;
padding: 0px 4px;
border-bottom: 1px solid #aaa;
}
.eval>div, .syntheseModule>div{
display: flex;
gap: 4px;
}
.eval:hover, .syntheseModule:hover{
background: var(--couleurSurlignage);
/* color: #FFF; */
}
.complement{
pointer-events:none;
position: absolute;
bottom: 100%;
right: 0;
padding: 8px;
border-radius: 4px;
background: #FFF;
color: #000;
border: 1px solid var(--couleurIntense);
opacity: 0;
display: grid !important;
grid-template-columns: auto auto;
gap: 0 !important;
column-gap: 4px !important;
}
.eval:hover .complement{
opacity: 1;
z-index: 1;
}
.complement>div:nth-child(even){
text-align: right;
}
.complement>div:nth-child(1),
.complement>div:nth-child(2){
font-weight: bold;
}
.complement>div:nth-child(1),
.complement>div:nth-child(7){
margin-bottom: 8px;
}
.absences{
display: grid;
grid-template-columns: auto auto;
column-gap: 4px;
text-align: right;
border-left: 1px solid;
padding-left: 16px;
}
.absences>div:nth-child(1),
.absences>div:nth-child(2){
font-weight: bold;
}
/* Bulletin BUT, Seb. L. 2021-12-06 */
/*******************/
/* Styles généraux */
/*******************/
.wait{
width: 60px;
height: 6px;
margin: auto;
background: #424242; /* la réponse à tout */
animation: wait .4s infinite alternate;
}
@keyframes wait{
100%{transform: translateY(40px) rotate(1turn);}
}
main{
--couleurPrincipale: rgb(240,250,255);
--couleurFondTitresUE: rgb(206,255,235);
--couleurFondTitresRes: rgb(125, 170, 255);
--couleurFondTitresSAE: rgb(211, 255, 255);
--couleurSecondaire: #fec;
--couleurIntense: #c09;
--couleurSurlignage: rgba(232, 255, 132, 0.47);
max-width: 1000px;
margin: auto;
display: none;
}
.ready .wait{display: none;}
.ready main{display: block;}
h2{
margin: 0;
color: black;
}
section{
background: #FFF;
border-radius: 16px;
border: 1px solid #AAA;
padding: 16px 32px;
margin: 8px 0;
}
section>div:nth-child(1){
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.CTA_Liste{
display: flex;
gap: 4px;
align-items: center;
background: var(--couleurIntense);
color: #FFF;
padding: 4px 8px;
border-radius: 4px;
box-shadow: 0 2px 2px rgba(0,0,0,0.26);
cursor: pointer;
}
.CTA_Liste>svg{
transition: 0.2s;
}
.CTA_Liste:hover{
outline: 2px solid #424242;
}
.listeOff svg{
transform: rotate(180deg);
}
.listeOff .syntheseModule,
.listeOff .eval{
display: none;
}
.moduleOnOff>.syntheseModule,
.moduleOnOff>.eval{
display: none;
}
.listeOff .moduleOnOff>.syntheseModule,
.listeOff .moduleOnOff>.eval{
display: flex !important;
}
.listeOff .ue::before,
.listeOff .module::before,
.moduleOnOff .ue::before,
.moduleOnOff .module::before{
transform: rotate(0);
}
.listeOff .moduleOnOff .ue::before,
.listeOff .moduleOnOff .module::before{
transform: rotate(180deg) !important;
}
/***********************/
/* Options d'affichage */
/***********************/
.hide_abs .absencesRecap,
/*.hide_abs .absences,*/
.hide_abs_modules .module>.absences,
.hide_coef .synthese em,
.hide_coef .eval>em,
.hide_date_inscr .dateInscription,
.hide_ects .ects{
display: none;
}
/*.module>.absences,*/
.module .moyenne,
.module .info{
display: none;
}
/************/
/* Etudiant */
/************/
.info_etudiant{
color: #000;
text-decoration: none;
}
.etudiant{
display: flex;
align-items: center;
gap: 16px;
border-color: var(--couleurPrincipale);
background: var(--couleurPrincipale);
color: rgb(0, 0, 0);
}
.civilite{
font-weight: bold;
font-size: 130%;
}
/************/
/* Semestre */
/************/
.flex{
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.infoSemestre{
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 4px;
}
.infoSemestre>div{
border: 1px solid var(--couleurIntense);
padding: 4px 8px;
border-radius: 4px;
display: grid;
grid-template-columns: auto auto;
column-gap: 4px;
flex: none;
}
.infoSemestre>div:nth-child(1){
margin-right: auto;
}
.infoSemestre>div>div:nth-child(even){
text-align: right;
}
.rang{
text-decoration: underline var(--couleurIntense);
}
.decision{
margin: 5px 0;
font-weight: bold;
font-size: 20px;
text-decoration: underline var(--couleurIntense);
}
.enteteSemestre{
color: black;
font-weight: bold;
font-size: 20px;
margin-bottom: 4px;
}
/***************/
/* Zone custom */
/***************/
.custom:empty{
display: none;
}
/***************/
/* Synthèse */
/***************/
.synthese .ue,
.synthese h3{
background: var(--couleurFondTitresUE);
}
.synthese em,
.eval em{
opacity: 0.6;
min-width: 80px;
display: inline-block;
}
.ueBonus,
.ueBonus h3{
background: var(--couleurFondTitresSAE) !important;
color: #000 !important;
}
/***************/
/* Evaluations */
/***************/
.evaluations>div,
.sae>div{
scroll-margin-top: 60px;
}
.module, .ue {
background: var(--couleurSecondaire);
color: #000;
padding: 4px 32px;
border-radius: 4px;
display: flex;
gap: 16px;
margin: 4px 0 2px 0;
overflow-x: auto;
overflow-y: hidden;
cursor: pointer;
position: relative;
}
.module::before, .ue::before {
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
width: 26px;
height: 26px;
position: absolute;
bottom: 0;
left: calc(50% - 13px);
transform: rotate(180deg);
transition: 0.2s;
}
@media screen and (max-width: 1000px) {
/* Placer le chevron à gauche au lieu du milieu */
.module::before, .ue::before {
left: 2px;
bottom: calc(50% - 13px);
}
}
h3{
display: flex;
align-items: center;
margin: 0 auto 0 0;
position: sticky;
left: -32px;
z-index: 1;
font-size: 16px;
background: var(--couleurSecondaire);
}
.sae .module, .sae h3{
background: var(--couleurFondTitresSAE);
}
.moyenne{
font-weight: bold;
text-align: right;
}
.info{
opacity: 0.9;
}
.syntheseModule{
cursor: pointer;
}
.eval, .syntheseModule{
position: relative;
display: flex;
justify-content: space-between;
margin: 0 0 0 28px;
padding: 0px 4px;
border-bottom: 1px solid #aaa;
}
.eval>div, .syntheseModule>div{
display: flex;
gap: 4px;
}
.eval:hover, .syntheseModule:hover{
background: var(--couleurSurlignage);
/* color: #FFF; */
}
.complement{
pointer-events:none;
position: absolute;
bottom: 100%;
right: 0;
padding: 8px;
border-radius: 4px;
background: #FFF;
color: #000;
border: 1px solid var(--couleurIntense);
opacity: 0;
display: grid !important;
grid-template-columns: auto auto;
gap: 0 !important;
column-gap: 4px !important;
}
.eval:hover .complement{
opacity: 1;
z-index: 1;
}
.complement>div:nth-child(even){
text-align: right;
}
.complement>div:nth-child(1),
.complement>div:nth-child(2){
font-weight: bold;
}
.complement>div:nth-child(1),
.complement>div:nth-child(7){
margin-bottom: 8px;
}
/*.absences{
display: grid;
grid-template-columns: auto auto;
column-gap: 4px;
text-align: right;
border-left: 1px solid;
padding-left: 16px;
}
.absences>div:nth-child(1),
.absences>div:nth-child(2){
font-weight: bold;
}*/

View File

@ -881,6 +881,19 @@ div.sco_help {
span.wtf-field ul.errors li {
color: red;
}
#bonus_description {
color:rgb(6, 73, 6);
padding: 5px;
margin-top:5px;
border: 2px solid blue;
border-radius: 5px;
background-color: cornsilk;
}
#bonus_description div.bonus_description_head{
font-weight: bold;
}
.configuration_logo div.img {
}
@ -1308,6 +1321,20 @@ td.formsemestre_status_cell {
white-space: nowrap;
}
span.mod_coef_indicator, span.ue_color_indicator {
display:inline-block;
width: 10px;
height: 10px;
}
span.mod_coef_indicator_zero {
display:inline-block;
width: 9px;
height: 9px;
border: 1px solid rgb(156, 156, 156);
}
span.status_ue_acro { font-weight: bold; }
span.status_ue_title { font-style: italic; padding-left: 1cm;}
span.status_module_cat { font-weight: bold; }
@ -1499,6 +1526,16 @@ table.moduleimpl_evaluations td.eval_poids {
color:rgb(0, 0, 255);
}
span.eval_coef_ue {
color:rgb(6, 73, 6);
font-style: normal;
font-size: 80%;
margin-right: 2em;
}
span.eval_coef_ue_titre {
}
/* Formulaire edition des partitions */
form#editpart table {
border: 1px solid gray;

View File

@ -1,5 +1,5 @@
function submit_form() {
$("#configuration_form").submit();
$("#config_logos_form").submit();
}
$(function () {

View File

@ -3,8 +3,26 @@
$().ready(function () {
update_ue_list();
$("#tf_ue_code").bind("keyup", update_ue_list);
$("select#tf_type").change(function () {
update_bonus_description();
});
update_bonus_description();
});
function update_bonus_description() {
var ue_type = $("#tf_type")[0].value;
if (ue_type == "1") { /* UE SPORT */
$("#bonus_description").show();
var query = "/ScoDoc/get_bonus_description/default";
$.get(query, '', function (data) {
$("#bonus_description").html(data);
});
} else {
$("#bonus_description").html("");
$("#bonus_description").hide();
}
}
function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value;

View File

@ -15,13 +15,10 @@ class releveBUT extends HTMLElement {
/* Style du module */
const styles = document.createElement('link');
styles.setAttribute('rel', 'stylesheet');
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css');
/* variante "ScoDoc" ou "Passerelle" (ENT) ? */
if (location.href.split("/")[3] == "ScoDoc") { /* un peu osé... */
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css');
if (location.href.split("/")[3] == "ScoDoc") {
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css'); // Scodoc
} else {
// Passerelle
styles.setAttribute('href', '/assets/styles/releve-but.css');
styles.setAttribute('href', '/assets/styles/releve-but.css'); // Passerelle
}
this.shadow.appendChild(styles);
}
@ -49,6 +46,8 @@ class releveBUT extends HTMLElement {
this.showSynthese(data);
this.showEvaluations(data);
this.showCustom(data);
this.setOptions(data.options);
this.shadow.querySelectorAll(".CTA_Liste").forEach(e => {
@ -57,7 +56,7 @@ class releveBUT extends HTMLElement {
this.shadow.querySelectorAll(".ue, .module").forEach(e => {
e.addEventListener("click", this.moduleOnOff)
})
this.shadow.querySelectorAll(".syntheseModule").forEach(e => {
this.shadow.querySelectorAll(":not(.ueBonus)+.syntheseModule").forEach(e => {
e.addEventListener("click", this.goTo)
})
@ -77,6 +76,11 @@ class releveBUT extends HTMLElement {
<div class=infoEtudiant></div>
</section>
<!--------------------------------------------------------------------------------------->
<!-- Zone spéciale pour que les IUT puisse ajouter des infos locales sur la passerelle -->
<!--------------------------------------------------------------------------------------->
<section class=custom></section>
<!--------------------------->
<!-- Semestre -->
<!--------------------------->
@ -169,8 +173,8 @@ class releveBUT extends HTMLElement {
output += `
</div>
<div class=numerosEtudiant>
Numéro étudiant : ${data.etudiant.code_nip} -
Code INE : ${data.etudiant.code_ine}
Numéro étudiant : ${data.etudiant.code_nip || "~"} -
Code INE : ${data.etudiant.code_ine || "~"}
</div>
<div>${data.formation.titre}</div>
`;
@ -183,6 +187,13 @@ class releveBUT extends HTMLElement {
this.shadow.querySelector(".infoEtudiant").innerHTML = output;
}
/*******************************/
/* Affichage local */
/*******************************/
showCustom(data) {
this.shadow.querySelector(".custom").innerHTML = data.custom || "";
}
/*******************************/
/* Information sur le semestre */
/*******************************/
@ -196,6 +207,11 @@ class releveBUT extends HTMLElement {
<div>Max. promo. :</div><div>${data.semestre.notes.max}</div>
<div>Moy. promo. :</div><div>${data.semestre.notes.moy}</div>
<div>Min. promo. :</div><div>${data.semestre.notes.min}</div>
</div>
<div class=absencesRecap>
<div class=enteteSemestre>Absences</div>
<div class=enteteSemestre>N.J. ${data.semestre.absences?.injustifie ?? "-"}</div>
<div style="grid-column: 2">Total ${data.semestre.absences?.total ?? "-"}</div>
</div>`;
/*${data.semestre.groupes.map(groupe => {
return `
@ -210,7 +226,7 @@ class releveBUT extends HTMLElement {
}).join("")
}*/
this.shadow.querySelector(".infoSemestre").innerHTML = output;
/*this.shadow.querySelector(".decision").innerHTML = data.semestre.decision.code;*/
this.shadow.querySelector(".decision").innerHTML = data.semestre.decision?.code || "";
}
/*******************************/
@ -219,32 +235,44 @@ class releveBUT extends HTMLElement {
showSynthese(data) {
let output = ``;
Object.entries(data.ues).forEach(([ue, dataUE]) => {
output += `
<div>
<div class=ue>
<h3>
${(dataUE.competence) ? dataUE.competence + " - " : ""}${ue}
</h3>
<div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div>
<div class=info>
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0}
<span class=ects>&nbsp;-
ECTS&nbsp;:&nbsp;${dataUE.ECTS.acquis}&nbsp;/&nbsp;${dataUE.ECTS.total}
</span>
</div>
</div>
<div class=absences>
<div>Abs&nbsp;N.J.</div><div>${dataUE.absences?.injustifie || 0}</div>
<div>Total</div><div>${dataUE.absences?.total || 0}</div>
if (dataUE.type == 1) { // UE Sport / Bonus
output += `
<div>
<div class="ue ueBonus">
<h3>Bonus</h3>
<div>${dataUE.bonus_description}</div>
</div>
${this.ueSport(dataUE.modules)}
</div>
${this.synthese(data, dataUE.ressources)}
${this.synthese(data, dataUE.saes)}
</div>
`;
`;
} else {
output += `
<div>
<div class=ue>
<h3>
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
</h3>
<div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div>
<div class=info>
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0}
<span class=ects>&nbsp;-
ECTS&nbsp;:&nbsp;${dataUE.ECTS.acquis}&nbsp;/&nbsp;${dataUE.ECTS.total}
</span>
</div>
</div>`;
/*<div class=absences>
<div>Abs&nbsp;N.J.</div><div>${dataUE.absences?.injustifie || 0}</div>
<div>Total</div><div>${dataUE.absences?.total || 0}</div>
</div>*/
output += `
</div>
${this.synthese(data, dataUE.ressources)}
${this.synthese(data, dataUE.saes)}
</div>
`;
}
});
this.shadow.querySelector(".synthese").innerHTML = output;
}
@ -252,7 +280,7 @@ class releveBUT extends HTMLElement {
let output = "";
Object.entries(modules).forEach(([module, dataModule]) => {
let titre = data.ressources[module]?.titre || data.saes[module]?.titre;
let url = data.ressources[module]?.url || data.saes[module]?.url;
//let url = data.ressources[module]?.url || data.saes[module]?.url;
output += `
<div class=syntheseModule data-module="${module.replace(/[^a-zA-Z0-9]/g, "")}">
<div>${module}&nbsp;- ${titre}</div>
@ -265,6 +293,23 @@ class releveBUT extends HTMLElement {
})
return output;
}
ueSport(modules) {
let output = "";
Object.values(modules).forEach((module) => {
Object.values(module.evaluations).forEach((evaluation) => {
output += `
<div class=syntheseModule>
<div>${module.titre} - ${evaluation.description}</div>
<div>
${evaluation.note.value ?? "-"}
<em>Coef.&nbsp;${evaluation.coef}</em>
</div>
</div>
`;
})
})
return output;
}
/*******************************/
/* Evaluations */
@ -305,7 +350,7 @@ class releveBUT extends HTMLElement {
evaluations.forEach((evaluation) => {
output += `
<div class=eval>
<div>${this.URL(evaluation.url, evaluation.description)}</div>
<div>${this.URL(evaluation.url, evaluation.description || "Évaluation")}</div>
<div>
${evaluation.note.value}
<em>Coef.&nbsp;${evaluation.coef}</em>
@ -363,4 +408,4 @@ class releveBUT extends HTMLElement {
}
}
customElements.define('releve-but', releveBUT);
customElements.define('releve-but', releveBUT);

View File

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h1>Configuration des codes de décision exportés vers Apogée</h1>
<div class="help">
<p>Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury
et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées
dans les exports Apogée.
<p>
<p>Ne les modifier que si vous savez ce que vous faites !
</p>
</div>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,125 @@
{# -*- mode: jinja-html -*- #}
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% macro render_field(field, with_label=True) %}
<div>
{% if with_label %}
<span class="wtf-field">{{ field.label }} :</span>
{% endif %}
<span class="wtf-field">{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</span>
</div>
{% endmacro %}
{% macro render_add_logo(add_logo_form) %}
<div class="logo-add">
<h3>Ajouter un logo</h3>
{{ add_logo_form.hidden_tag() }}
{{ render_field(add_logo_form.name) }}
{{ render_field(add_logo_form.upload) }}
{{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }}
</div>
{% endmacro %}
{% macro render_logo(dept_form, logo_form) %}
<div class="logo-edit">
{{ logo_form.hidden_tag() }}
{% if logo_form.titre %}
<tr class="logo-edit">
<td colspan="3" class="titre">
<div class="nom">
<h3>{{ logo_form.titre }}</h3>
</div>
<div class="description">{{ logo_form.description or "" }}</div>
</td>
</tr>
{% else %}
<tr class="logo-edit">
<td colspan="3" class="titre">
<span class="nom">
<h3>Logo personalisé: {{ logo_form.logo_id.data }}</h3>
</span>
<span class="description">{{ logo_form.description or "" }}</span>
</td>
</tr>
{% endif %}
<tr>
<td style="padding-right: 20px; ">
<div class="img-container">
<img src="{{ logo_form.logo.get_url_small() }}" alt="pas de logo chargé" />
</div>
</td>
<td class="img-data">
<h3>{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})</h3>
Taille: {{ logo_form.logo.size }} px
{% if logo_form.logo.mm %} &nbsp; / &nbsp; {{ logo_form.logo.mm }} mm {% endif %}<br />
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br />
Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</span>
</td>
<td class="" img-action">
<p>Modifier l'image</p>
<span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span>
{% if logo_form.can_delete %}
<p>Supprimer l'image</p>
{{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
{% endif %}
</td>
</tr>
</div>
{% endmacro %}
{% macro render_logos(dept_form) %}
<table>
{% for logo_entry in dept_form.logos.entries %}
{% set logo_form = logo_entry.form %}
{{ render_logo(dept_form, logo_form) }}
{% else %}
<p class="logo-edit">
<h3>Aucun logo défini en propre à ce département</h3>
</p>
{% endfor %}
</table>
{% endmacro %}
{% block app_content %}
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/js/config_logos.js"></script>
<form id="config_logos_form" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
<div class="configuration_logo">
<h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %}
{% set dept_form = dept_entry.form %}
{{ dept_entry.form.hidden_tag() }}
{% if dept_entry.form.is_local() %}
<div class="departement">
<h2>Département {{ dept_form.dept_name.data }}</h2>
<h3>Logos locaux</h3>
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br />
Les logos du département se substituent aux logos de même nom définis globalement:</div>
</div>
{% else %}
<div class="departement">
<h2>Logos généraux</h2>
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
mais peuvent être redéfinies localement au niveau de chaque département
(il suffit de définir un logo local de même nom)</div>
</div>
{% endif %}
{{ render_logos(dept_form) }}
{{ render_add_logo(dept_form.add_logo.form) }}
{% endfor %}
</div>
</form>
{% endblock %}

View File

@ -19,102 +19,53 @@
</div>
{% endmacro %}
{% macro render_add_logo(add_logo_form) %}
<div class="logo-add">
<h3>Ajouter un logo</h3>
{{ add_logo_form.hidden_tag() }}
{{ render_field(add_logo_form.name) }}
{{ render_field(add_logo_form.upload) }}
{{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }}
</div>
{% endmacro %}
{% macro render_logo(dept_form, logo_form) %}
<div class="logo-edit">
{{ logo_form.hidden_tag() }}
{% if logo_form.titre %}
<tr class="logo-edit">
<td colspan="3" class="titre">
<div class="nom"><h3>{{ logo_form.titre }}</h3></div>
<div class="description">{{ logo_form.description or "" }}</div>
</td>
</tr>
{% else %}
<tr class="logo-edit">
<td colspan="3" class="titre">
<span class="nom"><h3>Logo personalisé: {{ logo_form.logo_id.data }}</h3></span>
<span class="description">{{ logo_form.description or "" }}</span>
</td>
</tr>
{% endif %}
<tr>
<td style="padding-right: 20px; ">
<div class="img-container">
<img src="{{ logo_form.logo.get_url_small() }}" alt="pas de logo chargé" /></div>
</td><td class="img-data">
<h3>{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})</h3>
Taille: {{ logo_form.logo.size }} px
{% if logo_form.logo.mm %} &nbsp; / &nbsp; {{ logo_form.logo.mm }} mm {% endif %}<br/>
Aspect ratio: {{ logo_form.logo.aspect_ratio }}<br/>
Usage: <span style="font-family: system-ui">{{ logo_form.logo.get_usage() }}</span>
</td><td class=""img-action">
<p>Modifier l'image</p>
<span class="wtf-field">{{ render_field(logo_form.upload, False, onchange="submit_form()") }}</span>
{% if logo_form.can_delete %}
<p>Supprimer l'image</p>
{{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
{% endif %}
</td>
</tr>
</div>
{% endmacro %}
{% macro render_logos(dept_form) %}
<table>
{% for logo_entry in dept_form.logos.entries %}
{% set logo_form = logo_entry.form %}
{{ render_logo(dept_form, logo_form) }}
{% else %}
<p class="logo-edit"><h3>Aucun logo défini en propre à ce département</h3></p>
{% endfor %}
</table>
{% endmacro %}
{% block app_content %}
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
<script src="/ScoDoc/static/js/configuration.js"></script>
<form id="configuration_form" class="sco-form" action="" method="post" enctype="multipart/form-data" novalidate>
{{ form.hidden_tag() }}
<div class="configuration_logo">
<h1>Configuration générale</h1>
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
{{ render_field(form.bonus_sport_func_name, onChange="submit_form()")}}
<h1>Configuration générale</h1>
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
<div id="bonus_description"></div>
<h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %}
{% set dept_form = dept_entry.form %}
{{ dept_entry.form.hidden_tag() }}
{% if dept_entry.form.is_local() %}
<div class="departement">
<h2>Département {{ dept_form.dept_name.data }}</h2>
<h3>Logos locaux</h3>
<div class="sco_help">Les paramètres donnés sont spécifiques à ce département.<br/>
Les logos du département se substituent aux logos de même nom définis globalement:</div>
</div>
{% else %}
<div class="departement">
<h2>Logos généraux</h2>
<div class="sco_help">Les images de cette section sont utilisé pour tous les départements,
mais peuvent être redéfinies localement au niveau de chaque département
(il suffit de définir un logo local de même nom)</div>
</div>
{% endif %}
{{ render_logos(dept_form) }}
{{ render_add_logo(dept_form.add_logo.form) }}
{% endfor %}
<h1>Gestion des images: logos, signatures, ...</h1>
<div class="sco_help">Ces images peuvent être intégrées dans les documents
générés par ScoDoc: bulletins, PV, etc.</div>
<p><a href="{{url_for('scodoc.configure_logos')}}">configuration des images et logos</a>
</p>
<h1>Exports Apogée</h1>
<p><a href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a></p>
</div>
</form>
{% endblock %}
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
function update_bonus_description() {
var query = "/ScoDoc/get_bonus_description/" + $("#configuration_form select")[0].value;
$.get(query, '', function (data) {
$("#bonus_description").html(data);
});
}
$(function()
{
$("#configuration_form select").change(function(){
update_bonus_description();
});
update_bonus_description();
});
</script>
{% endblock %}

View File

@ -71,12 +71,12 @@
</li>
{% endfor %}
{% if editable and formation.ues.count() and formation.ues[0].matieres.count() %}
{% if editable and matiere_parent %}
<li><a class="stdlink" href="{{
url_for("notes.module_create",
scodoc_dept=g.scodoc_dept,
module_type=module_type|int,
matiere_id=formation.ues[0].matieres.first().id
matiere_id=matiere_parent.id
)}}"
>{{create_element_msg}}</a>
</li>

View File

@ -30,6 +30,8 @@
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a>
<span class="ue_type_{{ue.type}}">
<span class="ue_color_indicator" style="background:{{
ue.color if ue.color is not none else 'blue'}}"></span>
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
>{{ue.titre}}</a>

View File

@ -24,4 +24,24 @@
<a href="https://scodoc.org/BUT" target="_blank">la documentation</a>.
</p>
{%endif%}
{% if formsemestres %}
<p class="help">
Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
</p>
<h4>Semestres utilisant ce module:</h4>
<ul>
{%for formsemestre in formsemestres %}
<li><a class="stdlink" href="{{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}">{{formsemestre.titre_mois()}}</a>
</li>
{% endfor %}
</ul>
{%endif%}
</div>

View File

@ -290,20 +290,26 @@ def formsemestre_bulletinetud(
if etudid:
etud = models.Identite.query.get_or_404(etudid)
elif code_nip:
etud = models.Identite.query.filter_by(
code_nip=str(code_nip)
).first_or_404()
etud = (
models.Identite.query.filter_by(code_nip=str(code_nip))
.filter_by(dept_id=formsemestre.dept_id)
.first_or_404()
)
elif code_ine:
etud = models.Identite.query.filter_by(
code_ine=str(code_ine)
).first_or_404()
etud = (
models.Identite.query.filter_by(code_ine=str(code_ine))
.filter_by(dept_id=formsemestre.dept_id)
.first_or_404()
)
else:
raise ScoValueError(
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine"
)
if format == "json":
r = bulletin_but.BulletinBUT(formsemestre)
return jsonify(r.bulletin_etud(etud, formsemestre))
return jsonify(
r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing)
)
elif format == "html":
return render_template(
"but/bulletin.html",
@ -314,6 +320,7 @@ def formsemestre_bulletinetud(
formsemestre_id=formsemestre_id,
etudid=etudid,
format="json",
force_publishing=1, # pour ScoDoc lui même
),
sco=ScoData(),
)

View File

@ -32,50 +32,40 @@ Emmanuel Viennet, 2021
"""
import datetime
import io
import wtforms.validators
from app.auth.models import User
import os
import re
import flask
from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request
from flask.app import Flask
import flask_login
from flask_login.utils import login_required, current_user
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from werkzeug.exceptions import BadRequest, NotFound
from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList
from wtforms.fields import IntegerField
from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from PIL import Image as PILImage
from werkzeug.exceptions import BadRequest, NotFound
import app
from app import db
from app.forms.main import config_forms
from app.auth.models import User
from app.forms.main import config_logos, config_main
from app.forms.main.create_dept import CreateDeptForm
from app.forms.main.config_apo import CodesDecisionsForm
from app import models
from app.models import Departement, Identite
from app.models import departements
from app.models import FormSemestre, FormSemestreInscription
import sco_version
from app.scodoc import sco_logos
from app.models import ScoDocSiteConfig
from app.scodoc import sco_codes_parcours, sco_logos
from app.scodoc import sco_find_etud
from app.scodoc import sco_utils as scu
from app.decorators import (
admin_required,
scodoc7func,
scodoc,
permission_required_compat_scodoc7,
permission_required,
)
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp
from PIL import Image as PILImage
import sco_version
@bp.route("/")
@ -133,6 +123,28 @@ def toggle_dept_vis(dept_id):
return redirect(url_for("scodoc.index"))
@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"])
@admin_required
def config_codes_decisions():
"""Form config codes decisions"""
form = CodesDecisionsForm()
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index"))
if form.validate_on_submit():
for code in models.config.CODES_SCODOC_TO_APO:
ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data)
flash(f"Codes décisions enregistrés.")
return redirect(url_for("scodoc.index"))
elif request.method == "GET":
for code in models.config.CODES_SCODOC_TO_APO:
getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code)
return render_template(
"config_codes_decisions.html",
form=form,
title="Configuration des codes de décisions",
)
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required
def table_etud_in_accessible_depts():
@ -239,10 +251,37 @@ def about(scodoc_dept=None):
@bp.route("/ScoDoc/configuration", methods=["GET", "POST"])
@admin_required
def configuration():
auth_name = str(current_user)
"Page de configuration globale"
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name)
return config_forms.configuration()
raise AccessDenied("invalid user (%s) must be SuperAdmin" % current_user)
return config_main.configuration()
@bp.route("/ScoDoc/get_bonus_description/<bonus_name>", methods=["GET"])
def get_bonus_description(bonus_name: str):
"description text/html du bonus"
if bonus_name == "default":
bonus_name = ""
bonus_class = ScoDocSiteConfig.get_bonus_sport_class_from_name(bonus_name)
text = bonus_class.__doc__
fields = re.split(r"\n\n", text, maxsplit=1)
if len(fields) > 1:
first_line, text = fields
else:
first_line, text = "", fields[0]
return f"""<div class="bonus_description_head">{first_line}</div>
<div>{text}</div>
"""
@bp.route("/ScoDoc/configure_logos", methods=["GET", "POST"])
@admin_required
def configure_logos():
"Page de configuration des logos (globale)"
if not current_user.is_administrator():
raise AccessDenied("invalid user (%s) must be SuperAdmin" % current_user)
return config_logos.config_logos()
SMALL_SIZE = (200, 200)
@ -257,14 +296,16 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
suffix = logo.suffix
if small:
with PILImage.open(logo.filepath) as im:
im.thumbnail(SMALL_SIZE)
stream = io.BytesIO()
# on garde le même format (on pourrait plus simplement générer systématiquement du JPEG)
fmt = { # adapt suffix to be compliant with PIL save format
"PNG": "PNG",
"JPG": "JPEG",
"JPEG": "JPEG",
}[suffix.upper()]
if fmt == "JPEG":
im = im.convert("RGB")
im.thumbnail(SMALL_SIZE)
stream = io.BytesIO()
im.save(stream, fmt)
stream.seek(0)
return send_file(stream, mimetype=f"image/{fmt}")

View File

@ -62,7 +62,7 @@ from app.decorators import (
permission_required,
)
from app.scodoc import html_sco_header, sco_import_users, sco_excel
from app.scodoc import html_sco_header, sco_import_users, sco_excel, sco_roles_default
from app.scodoc import sco_users
from app.scodoc import sco_utils as scu
from app.scodoc import sco_xml
@ -81,7 +81,7 @@ _l = _
class ChangePasswordForm(FlaskForm):
user_name = HiddenField()
old_password = PasswordField(_l("Identifiez-vous"))
new_password = PasswordField(_l("Nouveau mot de passe"))
new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur"))
bis_password = PasswordField(
_l("Répéter"),
validators=[
@ -150,11 +150,12 @@ def user_info(user_name, format="json"):
@permission_required(Permission.ScoUsersAdmin)
@scodoc7func
def create_user_form(user_name=None, edit=0, all_roles=1):
"form. création ou edition utilisateur"
"form. création ou édition utilisateur"
if user_name is not None: # scodoc7func converti en int !
user_name = str(user_name)
Role.insert_roles() # assure la mise à jour des rôles en base
auth_dept = current_user.dept
from_mail = current_user.email
from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email
initvalues = {}
edit = int(edit)
all_roles = int(all_roles)
@ -191,7 +192,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
else:
# Les rôles standards créés à l'initialisation de ScoDoc:
standard_roles = [
Role.get_named_role(r) for r in ("Ens", "Secr", "Admin", "RespPe")
Role.get_named_role(r) for r in sco_roles_default.ROLES_ATTRIBUABLES_DEPT
]
# Départements auxquels ont peut associer des rôles via ce dialogue:
# si SuperAdmin, tous les rôles standards dans tous les départements
@ -215,6 +216,11 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
editable_roles_set = {
(r, dept) for r in standard_roles for dept in administrable_dept_acronyms
}
if current_user.is_administrator():
editable_roles_set |= {
(Role.get_named_role(r), "")
for r in sco_roles_default.ROLES_ATTRIBUABLES_SCODOC
}
#
if not edit:
submitlabel = "Créer utilisateur"
@ -577,8 +583,8 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
# A: envoi de welcome + procedure de reset
# B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
# C: Aucun envoi (mot de passe saisi dans le formulaire)
if vals["welcome"] == "1":
if vals["reset_password:list"] == "1":
if vals["welcome"] != "1":
if vals["reset_password"] != "1":
mode = Mode.WELCOME_AND_CHANGE_PASSWORD
else:
mode = Mode.WELCOME_ONLY

View File

@ -26,6 +26,9 @@ class Config:
SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin"
ADMINS = [SCODOC_ADMIN_MAIL]
SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL")
# Le "from" des mails émis. Attention: peut être remplacée par la préférence email_from_addr:
SCODOC_MAIL_FROM = os.environ.get("SCODOC_MAIL_FROM") or ("no-reply@" + MAIL_SERVER)
BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc")
SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data")

View File

@ -0,0 +1,84 @@
"""augmente taille codes Apogée
Revision ID: 28874ed6af64
Revises: f40fbaf5831c
Create Date: 2022-01-19 22:57:59.678313
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "28874ed6af64"
down_revision = "f40fbaf5831c"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"notes_formsemestre_etapes",
"etape_apo",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
op.alter_column(
"notes_formsemestre_inscription",
"etape",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
op.alter_column(
"notes_modules",
"code_apogee",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
op.alter_column(
"notes_ue",
"code_apogee",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"notes_ue",
"code_apogee",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
op.alter_column(
"notes_modules",
"code_apogee",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
op.alter_column(
"notes_formsemestre_inscription",
"etape",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
op.alter_column(
"notes_formsemestre_etapes",
"etape_apo",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""couleur UE
Revision ID: c95d5a3bf0de
Revises: 28874ed6af64
Create Date: 2022-01-24 21:44:55.205544
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "c95d5a3bf0de"
down_revision = "28874ed6af64"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("notes_ue", sa.Column("color", sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("notes_ue", "color")
# ### end Alembic commands ###

View File

@ -21,9 +21,8 @@ from app import clear_scodoc_cache
from app import models
from app.auth.models import User, Role, UserRole
from app.models import ScoPreference
from app.scodoc.sco_logos import make_logo_local
from app.models import Formation, UniteEns, Module
from app.models import Formation, UniteEns, Matiere, Module
from app.models import FormSemestre, FormSemestreInscription
from app.models import ModuleImpl, ModuleImplInscription
from app.models import Identite
@ -63,6 +62,7 @@ def make_shell_context():
"logout_user": logout_user,
"mapp": mapp,
"models": models,
"Matiere": Matiere,
"Module": Module,
"ModuleImpl": ModuleImpl,
"ModuleImplInscription": ModuleImplInscription,
@ -133,7 +133,7 @@ def user_create(username, role, dept, nom=None, prenom=None): # user-create
"Create a new user"
r = Role.get_named_role(role)
if not r:
sys.stderr.write("user_create: role {r} does not exists\n".format(r=role))
sys.stderr.write("user_create: role {r} does not exist\n".format(r=role))
return 1
u = User.query.filter_by(user_name=username).first()
if u:
@ -289,20 +289,28 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=
db.session.commit()
def abort_if_false(ctx, param, value):
if not value:
ctx.abort()
@app.cli.command()
@click.option(
"--yes",
is_flag=True,
callback=abort_if_false,
expose_value=False,
prompt=f"""Attention: Cela va effacer toutes les données du département
(étudiants, notes, formations, etc)
Voulez-vous vraiment continuer ?
""",
)
@click.argument("dept")
def delete_dept(dept): # delete-dept
"""Delete existing departement"""
from app.scodoc import notesdb as ndb
from app.scodoc import sco_dept
click.confirm(
f"""Attention: Cela va effacer toutes les données du département {dept}
(étudiants, notes, formations, etc)
Voulez-vous vraiment continuer ?
""",
abort=True,
)
db.reflect()
ndb.open_db_connection()
d = models.Departement.query.filter_by(acronym=dept).first()

View File

@ -17,7 +17,7 @@
% ************************************************************
% En-tête de l'avis
% ************************************************************
\begin{entete}{logos/logo_header}
\begin{entete}{logos/header}
\textbf{\Huge{Avis de Poursuites d'Etudes}} \\
\ligne \\
\normalsize{Département **DeptFullName**} \\

View File

@ -170,6 +170,11 @@ def import_scodoc7_dept(dept_id: str, dept_db_uri=None):
logging.info(f"connecting to database {dept_db_uri}")
cnx = psycopg2.connect(dept_db_uri)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
# FIX : des dates aberrantes (dans le futur) peuvent tenir en SQL mais pas en Python
cursor.execute(
"""UPDATE scolar_events SET event_date='2021-09-30' WHERE event_date > '2200-01-01'"""
)
cnx.commit()
# Create dept:
dept = models.Departement(acronym=dept_id, description="migré de ScoDoc7")
db.session.add(dept)
@ -374,6 +379,8 @@ def convert_object(
new_ref = id_from_scodoc7[old_ref]
elif (not is_table) and table_name in {
"scolog",
"entreprise_correspondant",
"entreprise_contact",
"etud_annotations",
"notes_notes_log",
"scolar_news",
@ -389,7 +396,6 @@ def convert_object(
new_ref = None
elif is_table and table_name in {
"notes_semset_formsemestre",
"entreprise_contact",
}:
# pour anciennes installs où des relations n'avait pas été déclarées clés étrangères
# eg: notes_semset_formsemestre.semset_id n'était pas une clé