Compare commits

..

34 Commits

Author SHA1 Message Date
leonard_montalbano
1cd7a84b15 test et récupération de dept, formsemestre et etu en variable global pour les tests suivant 2022-03-01 16:00:41 +01:00
leonard_montalbano
1c271bbad4 Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-03-01 12:58:47 +01:00
523ad7ad2a Modif bonus La Rochelle 2022-03-01 10:40:38 +01:00
f0e731d151 Fix: bulletin classique quand coef UE None 2022-03-01 10:33:53 +01:00
10c96ad683 PE: check submitted template (utf8) 2022-03-01 10:21:15 +01:00
6943ccb872 typo (sel. modules BUT) 2022-03-01 10:16:34 +01:00
c5c0b510ec filename export formations 2022-03-01 09:48:37 +01:00
7edd051183 Fix: bonus St Quentin / Ville d'Avray 2022-03-01 09:34:18 +01:00
13b40936b8 Nouveau calcul (correct?) de la moyenne de matière en classic 2022-02-28 20:02:10 +01:00
b56a20643d largeur colonne codes modules 2022-02-28 20:01:24 +01:00
e993599b39 Restreint edition modules semestres BUT aux module du même sem. 2022-02-28 17:57:12 +01:00
8b5a996571 Semestre BUT: ne propose pas indice -1 2022-02-28 16:28:08 +01:00
0e7f2f4deb flash 2022-02-28 16:27:27 +01:00
8330009dcf En BUT, remet S1 si semestre non spécifié 2022-02-28 16:26:13 +01:00
732a4c5ce5 code cleaning 2022-02-28 16:25:18 +01:00
leonard_montalbano
f0bdb5e9bd Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc into api 2022-02-28 15:44:26 +01:00
bee7b74f17 Fichier oublié (flask flash) 2022-02-28 15:18:21 +01:00
f7c90397a8 Enhance scodoc7 decorator: FileStorage arguments 2022-02-28 15:12:32 +01:00
5aa896f793 Bonus Aisne St Quentin + fix bonus Ville d'Avray 2022-02-28 15:08:32 +01:00
546e10c83a Finalise calcul moy. gen. indicative BUT 2022-02-28 15:07:48 +01:00
e1db9c542b Messages flash flask sur ancioennes pages ScoDoc + warning ECTS BUT 2022-02-28 11:47:39 +01:00
ef408e5d8e Gestion calcul moy gen et capit. BUT si ECTS manquants 2022-02-28 11:00:24 +01:00
68680e89d3 Exception si erreur connexion vers assistance 2022-02-28 09:22:17 +01:00
00fa91e598 Calcul moyenne gen. BUT avec ECTS 2022-02-27 20:32:38 +01:00
29b5d54d22 Prise en compte UE capitalisées lorsque non inscrit dans le sem. courant. Affichage sur bulletins classiques. Capitalisation en BUT avec ECTS. 2022-02-27 20:12:20 +01:00
6b8410e43b cosmetic: edit prog. 2022-02-27 17:49:39 +01:00
091d34dd88 Améliore creation UE 2022-02-27 10:19:25 +01:00
1dfccb6737 Modif bonus Roanne 2022-02-27 09:45:15 +01:00
40f823ee7c Fix: edition module 2022-02-26 20:35:34 +01:00
c0494d8d71 exception handling (export Apo) 2022-02-26 20:22:18 +01:00
c1c9f22a31 exception -handling 2022-02-26 20:11:22 +01:00
dbab59039c Fix: recherche images fond de page (logos) 2022-02-26 11:00:08 +01:00
9b27503d01 Fix: gestion logos 2022-02-26 10:15:00 +01:00
aa609aa0cf Améliore form. logos (validation des noms) + messages flash 2022-02-26 10:09:14 +01:00
40 changed files with 755 additions and 240 deletions

View File

@ -70,7 +70,7 @@ from app.scodoc.sco_saisie_notes import notes_add
@bp.route("/departements", methods=["GET"])
@token_auth.login_required
#@token_auth.login_required
def departements():
"""
Retourne la liste des ids de départements
@ -125,7 +125,7 @@ def liste_etudiants(dept: str, formsemestre_id=None): # XXX TODO A REVOIR
@bp.route("/departements/<string:dept>/semestres_courant", methods=["GET"])
@token_auth.login_required
# @token_auth.login_required
def liste_semestres_courant(dept: str):
"""
Liste des semestres actifs d'un départements donné
@ -142,7 +142,8 @@ def liste_semestres_courant(dept: str):
semestres = models.FormSemestre.query.filter_by(dept_id=id_dept, etat=True).all()
# Mise en forme des données
data = semestres[0].to_dict()
data = [d.to_dict() for d in semestres]
return jsonify(data)
@ -1184,11 +1185,205 @@ SCODOC_PASSWORD = ""
SCODOC_URL = ""
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
r0 = requests.post(
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
# r0 = requests.post(
# SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
# )
# token = r0.json()["token"]
# HEADERS = {"Authorization": f"Bearer {token}"}
DEPT = None
FORMSEMESTRE = None
ETU = None
@bp.route("/test_dept", methods=["GET"])
def get_departement():
"""
Retourne un département pour les tests
"""
r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
token = r0.json()["token"]
HEADERS = {"Authorization": f"Bearer {token}"}
if r.status_code == 200:
dept_id = r.json()[0]
# print(dept_id)
dept = models.Departement.query.filter_by(id=dept_id).first()
dept = dept.to_dict()
if "id" in dept:
if "acronym" in dept:
if "description" in dept:
if "visible" in dept:
if "date_creation" in dept:
global DEPT
DEPT = dept
return error_response(200, "OK")
else:
return error_response(501, "date_creation field missing")
else:
return error_response(501, "visible field missing")
else:
return error_response(501, "description field missing")
else:
return error_response(501, "acronym field missing")
else:
return error_response(501, "id field missing")
return error_response(409, "La requête ne peut être traitée en létat actuel")
@bp.route("/test_formsemestre", methods=["GET"])
def get_formsemestre():
"""
Retourne un formsemestre pour les tests
"""
global DEPT
dept_acronym = DEPT["acronym"]
r = requests.get(
SCODOC_URL + "/ScoDoc/api/departements/" + dept_acronym + "/semestres_courant",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
if r.status_code == 200:
formsemestre = r.json()[0]
# print(r.json()[0])
if "titre" in formsemestre:
if "gestion_semestrielle" in formsemestre:
if "scodoc7_id" in formsemestre:
if "date_debut" in formsemestre:
if "bul_bgcolor" in formsemestre:
if "date_fin" in formsemestre:
if "resp_can_edit" in formsemestre:
if "semestre_id" in formsemestre:
if "bul_hide_xml" in formsemestre:
if "elt_annee_apo" in formsemestre:
if "block_moyennes" in formsemestre:
if "formsemestre_id" in formsemestre:
if "titre_num" in formsemestre:
if "date_debut_iso" in formsemestre:
if "date_fin_iso" in formsemestre:
if "responsables" in formsemestre:
global FORMSEMESTRE
FORMSEMESTRE = formsemestre
# print(FORMSEMESTRE)
return error_response(200, "OK")
else:
return error_response(501,
"responsables field "
"missing")
else:
return error_response(501,
"date_fin_iso field missing")
else:
return error_response(501,
"date_debut_iso field missing")
else:
return error_response(501,
"titre_num field missing")
else:
return error_response(501,
"formsemestre_id field missing")
else:
return error_response(501,
"block_moyennes field missing")
else:
return error_response(501,
"elt_annee_apo field missing")
else:
return error_response(501,
"bul_hide_xml field missing")
else:
return error_response(501,
"semestre_id field missing")
else:
return error_response(501,
"resp_can_edit field missing")
else:
return error_response(501,
"date_fin field missing")
else:
return error_response(501,
"bul_bgcolor field missing")
else:
return error_response(501,
"date_debut field missing")
else:
return error_response(501,
"scodoc7_id field missing")
else:
return error_response(501,
"gestion_semestrielle field missing")
else:
return error_response(501,
"titre field missing")
return error_response(409, "La requête ne peut être traitée en létat actuel")
@bp.route("/test_etu", methods=["GET"])
def get_etudiant():
"""
Retourne un étudiant pour les tests
"""
# print(DEPT.get_data().decode("utf-8"))
# dept = DEPT
r = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
if r.status_code == 200:
etu = r.json()[0]
if "civilite" in etu:
if "code_ine" in etu:
if "code_nip" in etu:
if "date_naissance" in etu:
if "email" in etu:
if "emailperso" in etu:
if "etudid" in etu:
if "nom" in etu:
if "prenom" in etu:
global ETU
ETU = etu
# print(ETU)
return error_response(200, "OK")
else:
return error_response(501, "prenom field missing")
else:
return error_response(501, "nom field missing")
else:
return error_response(501, "etudid field missing")
else:
return error_response(501, "emailperso field missing")
else:
return error_response(501, "email field missing")
else:
return error_response(501, "date_naissance field missing")
else:
return error_response(501, "code_nip field missing")
else:
return error_response(501, "code_ine field missing")
else:
return error_response(501, "civilite field missing")
return error_response(409, "La requête ne peut être traitée en létat actuel")
def test_routes_departements():
@ -1196,7 +1391,7 @@ def test_routes_departements():
Test les routes de la partie Département
"""
# departements
r1 = requests.post(
r1 = requests.get(
SCODOC_URL + "/ScoDoc/api/departements",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
@ -1230,31 +1425,37 @@ def test_routes_etudiants():
"""
Test les routes de la partie Etudiants
"""
r1 = requests.post(
SCODOC_URL + "/ScoDoc/api",
# etudiants
r1 = requests.get(
SCODOC_URL + "/ScoDoc/api/etudiants",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiants_courant
r2 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant
r3 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_formsemestres
r4 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_bulletin_semestre
r5 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)
)
# etudiant_groups
r6 = requests.post(
SCODOC_URL + "/ScoDoc/api",
auth=(SCODOC_USER, SCODOC_PASSWORD)

View File

@ -198,7 +198,10 @@ class BonusSportAdditif(BonusSport):
à 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
seuil_moy_gen = 10.0 # seuls les bonus au dessus du seuil sont pris en compte
seuil_comptage = (
None # les points au dessus du seuil sont comptés (defaut: seuil_moy_gen)
)
proportion_point = 0.05 # multiplie les points au dessus du seuil
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@ -211,10 +214,13 @@ class BonusSportAdditif(BonusSport):
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
seuil_comptage = (
self.seuil_moy_gen if self.seuil_comptage is None else self.seuil_comptage
)
bonus_moy_arr = np.sum(
np.where(
sem_modimpl_moys_inscrits > self.seuil_moy_gen,
(sem_modimpl_moys_inscrits - self.seuil_moy_gen)
(sem_modimpl_moys_inscrits - self.seuil_comptage)
* self.proportion_point,
0.0,
),
@ -228,6 +234,10 @@ class BonusSportAdditif(BonusSport):
else: # necessaire pour éviter bonus négatifs !
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
self.bonus_additif(bonus_moy_arr)
def bonus_additif(self, bonus_moy_arr: np.array):
"Set bonus_ues et bonus_moy_gen"
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale
@ -306,6 +316,50 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0
class BonusAisneStQuentin(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université de St Quentin non rattachés à une unité d'enseignement.
</p>
<ul>
<li>Si la note est >= 10 et < 12.1, bonus de 0.1 point</li>
<li>Si la note est >= 12.1 et < 14.1, bonus de 0.2 point</li>
<li>Si la note est >= 14.1 et < 16.1, bonus de 0.3 point</li>
<li>Si la note est >= 16.1 et < 18.1, bonus de 0.4 point</li>
<li>Si la note est >= 18.1, bonus de 0.5 point</li>
</ul>
<p>
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE).
</p>
"""
name = "bonus_iutstq"
displayed_name = "IUT de Saint-Quentin"
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10] = 0.1
self.bonus_additif(bonus_moy_arr)
class BonusAmiens(BonusSportAdditif):
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
@ -559,8 +613,9 @@ class BonusLaRochelle(BonusSportAdditif):
name = "bonus_iutlr"
displayed_name = "IUT de La Rochelle"
seuil_moy_gen = 10.0 # tous les points sont comptés
proportion_point = 0.01
seuil_moy_gen = 10.0 # si bonus > 10,
seuil_comptage = 0.0 # tous les points sont comptés
proportion_point = 0.01 # 1%
class BonusLeHavre(BonusSportMultiplicatif):
@ -705,6 +760,7 @@ class BonusRoanne(BonusSportAdditif):
seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
proportion_point = 1
class BonusStDenis(BonusSportAdditif):
@ -773,21 +829,21 @@ class BonusVilleAvray(BonusSport):
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
np.nan_to_num(bonus_moy_arr, nan=0.0, copy=False)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
# 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.
self.bonus_additif(bonus_moy_arr)
class BonusIUTV(BonusSportAdditif):

View File

@ -40,13 +40,11 @@ def compute_mat_moys_classic(
modimpl_mask = np.array(
[m.module.matiere.id == matiere_id for m in formsemestre.modimpls_sorted]
)
etud_moy_gen, _, _ = moy_ue.compute_ue_moys_classic(
formsemestre,
etud_moy_mat = moy_ue.compute_mat_moys_classic(
sem_matrix=sem_matrix,
ues=ues,
modimpl_inscr_df=modimpl_inscr_df,
modimpl_coefs=modimpl_coefs,
modimpl_mask=modimpl_mask,
)
matiere_moy[matiere_id] = etud_moy_gen
matiere_moy[matiere_id] = etud_moy_mat
return matiere_moy

View File

@ -30,8 +30,10 @@
import numpy as np
import pandas as pd
from flask import flash
def compute_sem_moys_apc(
def compute_sem_moys_apc_using_coefs(
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
@ -48,6 +50,28 @@ def compute_sem_moys_apc(
return moy_gen
def compute_sem_moys_apc_using_ects(
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
ects: liste de floats ou None, 1 par UE
Result: panda Series, index etudid, valeur float (moyenne générale)
"""
try:
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
except TypeError:
if None in ects:
flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""")
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
else:
raise
return moy_gen
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos.

View File

@ -294,7 +294,8 @@ def compute_ue_moys_classic(
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> tuple[pd.Series, pd.DataFrame, pd.DataFrame]:
"""Calcul de la moyenne d'UE en mode classique.
"""Calcul de la moyenne d'UE et de la moy. générale en mode classique (DUT, LMD, ...).
La moyenne d'UE est un nombre (note/20), ou NI ou NA ou ERR
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
@ -363,7 +364,7 @@ def compute_ue_moys_classic(
modimpl_coefs_etuds_no_nan_stacked = np.stack(
[modimpl_coefs_etuds_no_nan.T] * nb_ues
)
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions
# nb_ue x nb_etuds x nb_mods : coefs prenant en compte NaN et inscriptions:
coefs = (modimpl_coefs_etuds_no_nan_stacked * ue_modules).swapaxes(1, 2)
if coefs.dtype == np.object: # arrive sur des tableaux vides
coefs = coefs.astype(np.float)
@ -408,6 +409,68 @@ def compute_ue_moys_classic(
return etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
def compute_mat_moys_classic(
sem_matrix: np.array,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs: np.array,
modimpl_mask: np.array,
) -> pd.Series:
"""Calcul de la moyenne sur un sous-enemble de modules en formation CLASSIQUE
La moyenne est un nombre (note/20 ou NaN.
Le masque modimpl_mask est un tableau de booléens (un par modimpl) qui
permet de sélectionner un sous-ensemble de modules (ceux de la matière d'intérêt).
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)
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: pd.Series, index etudid
"""
if (not len(modimpl_mask)) or (
sem_matrix.shape[0] == 0
): # aucun module ou aucun étudiant
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df
return pd.Series(
[0.0] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
)
# 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
# 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:
# Annule les notes:
sem_matrix_inscrits = np.where(modimpl_inscr, sem_matrix_no_nan, 0.0)
# Annule les coefs des modules où l'étudiant n'est pas inscrit:
modimpl_coefs_etuds = np.where(
modimpl_inscr, np.stack([modimpl_coefs.T] * nb_etuds), 0.0
)
# Annule les coefs des modules NaN (nb_etuds x nb_mods)
modimpl_coefs_etuds_no_nan = np.where(
np.isnan(sem_matrix), 0.0, modimpl_coefs_etuds
)
if modimpl_coefs_etuds_no_nan.dtype == np.object: # arrive sur des tableaux vides
modimpl_coefs_etuds_no_nan = modimpl_coefs_etuds_no_nan.astype(np.float)
etud_moy_mat = (modimpl_coefs_etuds_no_nan * sem_matrix_inscrits).sum(
axis=1
) / modimpl_coefs_etuds_no_nan.sum(axis=1)
return pd.Series(etud_moy_mat, index=modimpl_inscr_df.index)
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,

View File

@ -14,7 +14,7 @@ from app import log
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.models import ScoDocSiteConfig, formsemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
@ -73,7 +73,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
)
# Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame(
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
# --- Modules de MALUS sur les UEs
@ -103,8 +103,13 @@ class ResultatsSemestreBUT(NotesTableCompat):
# Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyennes 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
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
# self.etud_moy_ue, self.modimpl_coefs_df
# )
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
self.etud_moy_ue,
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
formation_id=self.formsemestre.formation_id,
)
# --- UE capitalisées
self.apply_capitalisation()

View File

@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np
import pandas as pd
from flask import g, flash, url_for
from app import log
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models import FormSemestre, FormSemestreUECoef
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -191,7 +195,7 @@ class ResultatsSemestre(ResultatsCache):
if ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
if not np.isnan(ue_cap["moy"]) and coef:
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
@ -206,12 +210,18 @@ class ResultatsSemestre(ResultatsCache):
0.0, min(self.etud_moy_gen[etudid], 20.0)
)
def _get_etud_ue_cap(self, etudid, ue):
""""""
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
Résultat:
Si pas capitalisée: None
Si capitalisée: un dict, avec les colonnes de validation.
"""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
if ue_cap.empty:
return None
if isinstance(ue_cap, pd.DataFrame):
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
@ -219,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
return None
# converti la Series en dict, afin que les np.int64 reviennent en int
return ue_cap.to_dict()
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
@ -248,22 +259,45 @@ class ResultatsSemestre(ResultatsCache):
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue
is_capitalized = False # si l'UE prise en compte est une UE capitalisée
was_capitalized = (
False # s'il y a precedemment une UE capitalisée (pas forcement meilleure)
)
# s'il y a precedemment une UE capitalisée (pas forcement meilleure):
was_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if (
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
# Coef l'UE dans le semestre courant:
if self.is_apc:
# utilise les ECTS comme coef.
coef_ue = ue.ects
else:
# formations classiques
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
if self.is_apc:
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
coef_ue = ue_capitalized.ects
if coef_ue is None:
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
raise ScoValueError(
f"""L'UE capitalisée {ue_capitalized.acronyme}
du semestre {orig_sem.titre_annee()}
n'a pas d'indication d'ECTS.
Corrigez ou faite corriger le programme
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
"""
)
else:
# Coefs de l'UE capitalisée en formation classique:
# va chercher le coef dans le semestre d'origine
coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue(
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
)
return {
"is_capitalized": is_capitalized,
@ -385,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre):
"""Stats (moy/min/max) sur la moyenne générale"""
return StatsMoyenne(self.etud_moy_gen)
def get_ues_stat_dict(self, filter_sport=False): # was get_ues()
def get_ues_stat_dict(
self, filter_sport=False, check_apc_ects=True
) -> list[dict]: # was get_ues()
"""Liste des UEs, ordonnée par numero.
Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE }
"""
ues = []
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
ues_dict = []
for ue in ues:
d = ue.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
ues_dict.append(d)
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
g.checked_apc_ects = True
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
flash(
"""Calcul moyenne générale impossible: ECTS des UE manquants !""",
category="danger",
)
return ues_dict
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
"""Liste des modules pour une UE (ou toutes si ue_id==None),

View File

@ -8,11 +8,13 @@
"""
from flask import g
from app import db
from app.comp.jury import ValidationsSemestre
from app.comp.res_common import ResultatsSemestre
from app.comp.res_classic import ResultatsSemestreClassic
from app.comp.res_but import ResultatsSemestreBUT
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
@ -23,6 +25,13 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it.
"""
is_apc = formsemestre.formation.is_apc()
if is_apc and formsemestre.semestre_id == -1:
formsemestre.semestre_id = 1
db.session.add(formsemestre)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre.id)
# --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_results_cache = {}
@ -30,11 +39,7 @@ def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id]
klass = (
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
klass = ResultatsSemestreBUT if is_apc else ResultatsSemestreClassic
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id]

View File

@ -193,7 +193,7 @@ def scodoc7func(func):
# necessary for db ids and boolean values
try:
v = int(v)
except ValueError:
except (ValueError, TypeError):
pass
pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)

View File

@ -30,17 +30,15 @@ Formulaires configuration logos
Contrib @jmp, dec 21
"""
import re
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms import SubmitField, FormField, validators, FieldList
from wtforms import ValidationError
from wtforms.fields.simple import StringField, HiddenField
from app import AccessDenied
from app.models import Departement
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
@ -49,10 +47,11 @@ from app.scodoc.sco_config_actions import (
LogoInsert,
)
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -111,6 +110,15 @@ def dept_key_to_id(dept_key):
return dept_key
def logo_name_validator(message=None):
def validate_logo_name(form, field):
name = field.data if field.data else ""
if not scu.is_valid_filename(name):
raise ValidationError(message)
return validate_logo_name
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
@ -118,11 +126,7 @@ class AddLogoForm(FlaskForm):
name = StringField(
label="Nom",
validators=[
validators.regexp(
r"^[a-zA-Z0-9-_]*$",
re.IGNORECASE,
"Ne doit comporter que lettres, chiffres, _ ou -",
),
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
@ -373,11 +377,11 @@ def config_logos():
if action:
action.execute()
flash(action.message)
return redirect(
url_for(
"scodoc.configure_logos",
)
)
return redirect(url_for("scodoc.configure_logos"))
else:
if not form.validate():
scu.flash_errors(form)
return render_template(
"config_logos.html",
scodoc_dept=None,

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls
"""
import pandas as pd
import flask_sqlalchemy
from app import db
from app.comp import df_cache
@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
)
@classmethod
def nb_inscriptions_dans_ue(
def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
) -> flask_sqlalchemy.BaseQuery:
"""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()
)
@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 cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()
@classmethod
def sum_coefs_modimpl_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> float:
"""Somme des coefficients des modules auxquels l'étudiant est inscrit
dans l'UE du semestre indiqué.
N'utilise que les coefficients, donc inadapté aux formations APC.
"""
return sum(
[
inscr.modimpl.module.coefficient
for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
]
)

View File

@ -54,13 +54,15 @@ class UniteEns(db.Model):
'EXTERNE' if self.is_external else ''})>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
"""as a dict, with the same conversions as in ScoDoc7
(except ECTS: keep None)
"""
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
e["ects"] = e["ects"] if e["ects"] else 0.0
e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e

View File

@ -36,6 +36,7 @@
"""
from flask import send_file, request
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
from app.scodoc import sco_formsemestre
@ -97,8 +98,12 @@ def pe_view_sem_recap(
template_latex = ""
# template fourni via le formulaire Web
if avis_tmpl_file:
template_latex = avis_tmpl_file.read().decode('utf-8')
template_latex = template_latex
try:
template_latex = avis_tmpl_file.read().decode("utf-8")
except UnicodeDecodeError as e:
raise ScoValueError(
"Données (template) invalides (caractères non UTF8 ?)"
) from e
else:
# template indiqué dans préférences ScoDoc ?
template_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
@ -114,7 +119,7 @@ def pe_view_sem_recap(
footer_latex = ""
# template fourni via le formulaire Web
if footer_tmpl_file:
footer_latex = footer_tmpl_file.read().decode('utf-8')
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

@ -30,7 +30,7 @@
import html
from flask import g
from flask import render_template
from flask import request
from flask_login import current_user
@ -280,6 +280,9 @@ def sco_header(
if not no_side_bar:
H.append(html_sidebar.sidebar())
H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.html"))
#
# Barre menu semestre:
H.append(formsemestre_page_title())

View File

@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models import FormSemestre, Identite
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
NAR,
RAT,
)
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud
@ -454,6 +453,12 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid):
"""Calcul résultat apo semestre"""
if decision is None:
etud = Identite.query.get(etudid)
nomprenom = etud.nomprenom if etud else "(inconnu)"
raise ScoValueError(
f"decision absente pour l'étudiant {nomprenom} ({etudid})"
)
# resultat du semestre
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid)

View File

@ -291,15 +291,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
):
) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
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"])
@ -321,9 +323,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
if ue_status["coef_ue"] != None:
u["coef_ue_txt"] = scu.fmt_coef(ue_status["coef_ue"])
else:
# C'est un bug:
log("u=" + pprint.pformat(u))
raise Exception("invalid None coef for ue")
u["coef_ue_txt"] = "-"
if (
dpv

View File

@ -33,17 +33,12 @@
"""
# API ScoDoc8 pour les caches:
# sco_cache.NotesTableCache.get( formsemestre_id)
# => sco_cache.NotesTableCache.get(formsemestre_id)
# API pour les caches:
# sco_cache.MyCache.get( formsemestre_id)
# => sco_cache.MyCache.get(formsemestre_id)
#
# sco_core.inval_cache(formsemestre_id=None, pdfonly=False, formsemestre_id_list=None)
# => deprecated, NotesTableCache.invalidate_formsemestre(formsemestre_id=None, pdfonly=False)
#
#
# Nouvelles fonctions:
# sco_cache.NotesTableCache.delete(formsemestre_id)
# sco_cache.NotesTableCache.delete_many(formsemestre_id_list)
# sco_cache.MyCache.delete(formsemestre_id)
# sco_cache.MyCache.delete_many(formsemestre_id_list)
#
# Bulletins PDF:
# sco_cache.SemBulletinsPDFCache.get(formsemestre_id, version)
@ -203,49 +198,6 @@ class SemInscriptionsCache(ScoDocCache):
duration = 12 * 60 * 60 # ttl 12h
class NotesTableCache(ScoDocCache):
"""Cache pour les NotesTable
Clé: formsemestre_id
Valeur: NotesTable instance
"""
prefix = "NT"
@classmethod
def get(cls, formsemestre_id, compute=True):
"""Returns NotesTable for this formsemestre
Search in local cache (g.nt_cache) or global app cache (eg REDIS)
If not in cache:
If compute is True, build it and cache it
Else return None
"""
# try local cache (same request)
if not hasattr(g, "nt_cache"):
g.nt_cache = {}
else:
if formsemestre_id in g.nt_cache:
return g.nt_cache[formsemestre_id]
# try REDIS
key = cls._get_key(formsemestre_id)
nt = CACHE.get(key)
if nt:
g.nt_cache[formsemestre_id] = nt # cache locally (same request)
return nt
if not compute:
return None
# Recompute requested table:
from app.scodoc import notes_table
t0 = time.time()
nt = notes_table.NotesTable(formsemestre_id)
t1 = time.time()
_ = cls.set(formsemestre_id, nt) # cache in REDIS
t2 = time.time()
log(f"cached formsemestre_id={formsemestre_id} ({(t1-t0):g}s +{(t2-t1):g}s)")
g.nt_cache[formsemestre_id] = nt
return nt
def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=False)
formsemestre_id=None, pdfonly=False
):
@ -278,22 +230,24 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
if not pdfonly:
# Delete cached notes and evaluations
NotesTableCache.delete_many(formsemestre_ids)
if formsemestre_id:
for fid in formsemestre_ids:
EvaluationCache.invalidate_sem(fid)
if hasattr(g, "nt_cache") and fid in g.nt_cache:
del g.nt_cache[fid]
if (
hasattr(g, "formsemestre_results_cache")
and fid in g.formsemestre_results_cache
):
del g.formsemestre_results_cache[fid]
else:
# optimization when we invalidate all evaluations:
EvaluationCache.invalidate_all_sems()
if hasattr(g, "nt_cache"):
del g.nt_cache
if hasattr(g, "formsemestre_results_cache"):
del g.formsemestre_results_cache
SemInscriptionsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
class DefferedSemCacheManager:

View File

@ -282,7 +282,7 @@ class TypeParcours(object):
return [
ue_status
for ue_status in ues_status
if ue_status["coef_ue"] > 0
if ue_status["coef_ue"]
and isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
]

View File

@ -51,6 +51,7 @@ import fcntl
import subprocess
import requests
from flask import flash
from flask_login import current_user
import app.scodoc.notesdb as ndb
@ -124,6 +125,7 @@ def sco_dump_and_send_db():
fcntl.flock(x, fcntl.LOCK_UN)
log("sco_dump_and_send_db: done.")
flash("Données envoyées au serveur d'assistance")
return "\n".join(H) + html_sco_header.sco_footer()
@ -186,6 +188,7 @@ def _send_db(ano_db_name):
log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".dump", dump)}
try:
r = requests.post(
scu.SCO_DUMP_UP_URL,
files=files,
@ -198,6 +201,15 @@ def _send_db(ano_db_name):
"sco_fullversion": scu.get_scodoc_version(),
},
)
except requests.exceptions.ConnectionError as exc:
raise ScoValueError(
"""
Impossible de joindre le serveur d'assistance (scodoc.org).
Veuillez contacter le service informatique de votre établissement pour
corriger la configuration de ScoDoc. Dans la plupart des cas, il
s'agit d'un proxy mal configuré.
"""
) from exc
return r

View File

@ -551,7 +551,11 @@ def module_edit(module_id=None):
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
) | {a_module.module_type or scu.ModuleType.STANDARD}
) | {
scu.ModuleType(a_module.module_type)
if a_module.module_type
else scu.ModuleType.STANDARD
}
descr = [
(

View File

@ -29,7 +29,7 @@
"""
import flask
from flask import url_for, render_template
from flask import flash, render_template, url_for
from flask import g, request
from flask_login import current_user
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
input_formators={
"type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str,
"ects": ndb.float_null_is_null,
},
output_formators={
"numero": ndb.int_null_is_zero,
@ -107,8 +108,6 @@ def ue_list(*args, **kw):
def do_ue_create(args):
"create an ue"
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion()
# check duplicates
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
@ -117,6 +116,14 @@ def do_ue_create(args):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
if not "ue_code" in args:
# évite les conflits de code
while True:
cursor = db.session.execute("select notes_newid_ucod();")
code = cursor.fetchone()[0]
if UniteEns.query.filter_by(ue_code=code).count() == 0:
break
args["ue_code"] = code
# create
ue_id = _ueEditor.create(cnx, args)
@ -128,6 +135,8 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs()
# news
ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
@ -339,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float",
"title": "ECTS",
"explanation": "nombre de crédits ECTS",
"allow_null": not is_apc, # ects requis en APC
},
),
(
@ -462,8 +472,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"semestre_id": tf[2]["semestre_idx"],
},
)
flash("UE créée")
else:
do_ue_edit(tf[2])
flash("UE modifiée")
return flask.redirect(
url_for(
"notes.ue_table",
@ -746,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
)
else:
H.append('<div class="formation_classic_infos">')
H.append(
_ue_table_ues(
parcours,
@ -775,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</ul>
"""
)
H.append("</div>")
H.append("</div>") # formation_ue_list
if ues_externes:
@ -924,10 +937,10 @@ def _ue_table_ues(
cur_ue_semestre_id = None
iue = 0
for ue in ues:
if ue["ects"]:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
if ue["ects"] is None:
ue["ects_str"] = ""
else:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
if editable:
klass = "span_apo_edit"
else:
@ -1286,7 +1299,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
# On ne peut pas supprimer le code UE:
if "ue_code" in args and not args["ue_code"]:
del args["ue_code"]

View File

@ -39,6 +39,7 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
@ -221,7 +222,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase
try:
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
except ScoException:
etuds = []
else:
code_nip = code_nip or expnom
if code_nip:

View File

@ -151,8 +151,14 @@ def formation_export(
if mod["ects"] is None:
del mod["ects"]
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult(
F, name="formation", format=format, force_outer_xml_tag=False, attached=True
F,
name="formation",
format=format,
force_outer_xml_tag=False,
attached=True,
filename=filename,
)

View File

@ -78,7 +78,7 @@ def formsemestre_createwithmodules():
H = [
html_sco_header.sco_header(
page_title="Création d'un semestre",
javascripts=["libjs/AutoSuggest.js"],
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
),
@ -99,7 +99,7 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
javascripts=["libjs/AutoSuggest.js"],
javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
)
@ -213,7 +213,10 @@ def do_formsemestre_createwithmodules(edit=False):
# 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_list = list(range(1, NB_SEM + 1))
if not formation.is_apc():
# propose "pas de semestre" seulement en classique
semestre_id_list.insert(0, -1)
semestre_id_labels = []
for sid in semestre_id_list:
@ -341,6 +344,9 @@ def do_formsemestre_createwithmodules(edit=False):
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
if formation.is_apc()
else "",
"attributes": ['onchange="change_semestre_id();"']
if formation.is_apc()
else "",
},
),
)
@ -493,7 +499,8 @@ def do_formsemestre_createwithmodules(edit=False):
{
"input_type": "boolcheckbox",
"title": "",
"explanation": "Autoriser tous les enseignants associés à un module à y créer des évaluations",
"explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""",
},
),
(
@ -534,11 +541,19 @@ def do_formsemestre_createwithmodules(edit=False):
]
nbmod = 0
if edit:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"
else:
templ_sep = "<tr><td>%(label)s</td><td><b>Responsable</b></td></tr>"
for semestre_id in semestre_ids:
if formation.is_apc():
# pour restreindre l'édition aux module du semestre sélectionné
tr_class = f'class="sem{semestre_id}"'
else:
tr_class = ""
if edit:
templ_sep = f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td><td><b>Inscrire</b></td></tr>"""
else:
templ_sep = (
f"""<tr {tr_class}><td>%(label)s</td><td><b>Responsable</b></td></tr>"""
)
modform.append(
(
"sep",
@ -588,12 +603,12 @@ def do_formsemestre_createwithmodules(edit=False):
)
fcg += "</select>"
itemtemplate = (
"""<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
+ fcg
+ "</td></tr>"
)
else:
itemtemplate = """<tr><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
modform.append(
(
"MI" + str(mod["module_id"]),

View File

@ -987,7 +987,6 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML"""
# porté du DTML
cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
@ -1078,7 +1077,7 @@ def formsemestre_status(formsemestre_id=None):
"</p>",
]
if use_ue_coefs:
if use_ue_coefs and not formsemestre.formation.is_apc():
H.append(
"""
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>

View File

@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table(
else:
H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit:
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [
ue
for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
]
for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"]
else:
code = ""
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
ue_status = etud_ue_status[ue["ue_id"]]
moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings
if code == ADM:

View File

@ -151,6 +151,8 @@ class Logo:
Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
"""
self.logoname = secure_filename(logoname)
if not self.logoname:
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
self.scodoc_dept_id = dept_id
self.prefix = prefix or ""
if self.scodoc_dept_id:

View File

@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object):
# pour le DUT, le dernier est toujours S4.
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
# (licences et autres formations en 1 seule session))
self.semestre_non_terminal = (
self.sem["semestre_id"] != self.parcours.NB_SEM
) # True | False
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant:

View File

@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate):
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None
logo = find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=""
)
if logo is None:
# Also try to use PV background
logo = find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
logoname="letter_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=""
)
if logo is not None:
self.background_image_filename = logo.filepath

View File

@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate):
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:

View File

@ -50,7 +50,7 @@ import pydot
import requests
from flask import g, request
from flask import url_for, make_response, jsonify
from flask import flash, url_for, make_response, jsonify
from config import Config
from app import log
@ -616,6 +616,16 @@ def bul_filename(sem, etud, format):
return filename
def flash_errors(form):
"""Flashes form errors (version sommaire)"""
for field, errors in form.errors.items():
flash(
"Erreur: voir le champs %s" % (getattr(form, field).label.text,),
"warning",
)
# see https://getbootstrap.com/docs/4.0/components/alerts/
def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)
@ -635,21 +645,30 @@ class ScoDocJSONEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, o)
def sendJSON(data, attached=False):
def sendJSON(data, attached=False, filename=None):
js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
return send_file(
js, filename="sco_data.json", mime=JSON_MIMETYPE, attached=attached
js, filename=filename or "sco_data.json", mime=JSON_MIMETYPE, attached=attached
)
def sendXML(data, tagname=None, force_outer_xml_tag=True, attached=False, quote=True):
def sendXML(
data,
tagname=None,
force_outer_xml_tag=True,
attached=False,
quote=True,
filename=None,
):
if type(data) != list:
data = [data] # always list-of-dicts
if force_outer_xml_tag:
data = [{tagname: data}]
tagname += "_list"
doc = sco_xml.simple_dictlist2xml(data, tagname=tagname, quote=quote)
return send_file(doc, filename="sco_data.xml", mime=XML_MIMETYPE, attached=attached)
return send_file(
doc, filename=filename or "sco_data.xml", mime=XML_MIMETYPE, attached=attached
)
def sendResult(
@ -659,6 +678,7 @@ def sendResult(
force_outer_xml_tag=True,
attached=False,
quote_xml=True,
filename=None,
):
if (format is None) or (format == "html"):
return data
@ -669,9 +689,10 @@ def sendResult(
force_outer_xml_tag=force_outer_xml_tag,
attached=attached,
quote=quote_xml,
filename=filename,
)
elif format == "json":
return sendJSON(data, attached=attached)
return sendJSON(data, attached=attached, filename=filename)
else:
raise ValueError("invalid format: %s" % format)

View File

@ -138,7 +138,7 @@ div.head_message {
border-radius: 8px;
font-family : arial, verdana, sans-serif ;
font-weight: bold;
width: 40%;
width: 70%;
text-align: center;
}
@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */
}
div.logo-logo {
margin-left: -5px;
text-align: center ;
}
div.logo-logo img {
box-sizing: content-box;
margin-top: -10px;
width: 128px;
margin-top: 10px; /* -10px */
width: 135px; /* 128px */
padding-right: 5px;
margin-left: -75px;
}
div.sidebar-bottom {
margin-top: 10px;
@ -1297,7 +1297,7 @@ th.formsemestre_status_inscrits {
text-align: center;
}
td.formsemestre_status_code {
width: 2em;
/* width: 2em; */
padding-right: 1em;
}
@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list {
padding-top: 5px;
padding-bottom: 5px;
}
span.missing_ue_ects {
color: red;
font-weight: bold;
}
li.module_malus span.formation_module_tit {
color: red;
font-weight: bold;
@ -1703,8 +1706,11 @@ ul.notes_ue_list {
padding-bottom: 1em;
font-weight: bold;
}
.formation_classic_infos ul.notes_ue_list {
padding-top: 0px;
}
li.notes_ue_list {
.formation_classic_infos li.notes_ue_list {
margin-top: 9px;
list-style-type: none;
border: 1px solid maroon;
@ -1761,6 +1767,11 @@ ul.notes_module_list {
font-style: normal;
}
div.ue_list_tit_sem {
font-size: 120%;
font-weight: bold;
}
.notes_ue_list a.stdlink {
color: #001084;
text-decoration: underline;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -0,0 +1,14 @@
// Formulaire formsemestre_createwithmodules
function change_semestre_id() {
var semestre_id = $("#tf_semestre_id")[0].value;
for (var i = -1; i < 12; i++) {
$(".sem" + i).hide();
}
$(".sem" + semestre_id).show();
}
$(window).on('load', function () {
change_semestre_id();
});

View File

@ -57,12 +57,10 @@
{% block content %}
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{# application content needs to be provided in the app_content block #}

View File

@ -0,0 +1,9 @@
{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #}
{# -*- mode: jinja-html -*- #}
<div class="head_message_container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="head_message alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
</div>

View File

@ -38,7 +38,8 @@
{% set virg = joiner(", ") %}
<span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
{{ virg() }}{{ue.ects or 0}} ECTS)
{{ virg() }}{{ue.ects if ue.ects is not none
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS)
</span>
</span>

View File

@ -23,12 +23,10 @@
<div id="gtrcontent" class="gtrcontent">
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% if sco.sem %}

View File

@ -35,7 +35,7 @@ from operator import itemgetter
from xml.etree import ElementTree
import flask
from flask import flash, jsonify, render_template, url_for
from flask import abort, flash, jsonify, render_template, url_for
from flask import current_app, g, request
from flask_login import current_user
from werkzeug.utils import redirect
@ -68,10 +68,14 @@ from app.scodoc import sco_utils as scu
from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoException,
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header
from app.pe import pe_view
from app.scodoc import sco_abs
@ -2672,12 +2676,15 @@ def check_integrity_all():
def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
):
try:
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom

View File

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