master #320

Closed
pascal.bouron wants to merge 3 commits from (deleted):master into master
224 changed files with 20398 additions and 3859 deletions
Showing only changes of commit 360da6a038 - Show all commits

2
.gitignore vendored
View File

@ -169,5 +169,7 @@ Thumbs.db
.vscode/
*.code-workspace
# PyCharm
.idea/
copy

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

@ -200,6 +200,10 @@ def create_app(config_class=DevConfig):
app.register_blueprint(auth_bp, url_prefix="/auth")
from app.entreprises import bp as entreprises_bp
app.register_blueprint(entreprises_bp, url_prefix="/ScoDoc/entreprises")
from app.views import scodoc_bp
from app.views import scolar_bp
from app.views import notes_bp

View File

@ -49,10 +49,11 @@ from app.api.auth import token_auth
from app.api.errors import error_response
from app import models
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import ApcReferentielCompetences
from app.scodoc.sco_permissions import Permission
@bp.route("list_depts", methods=["GET"])
@bp.route("/list_depts", methods=["GET"])
@token_auth.login_required
def list_depts():
depts = models.Departement.query.filter_by(visible=True).all()
@ -66,7 +67,7 @@ def etudiants():
"""Liste de tous les étudiants actuellement inscrits à un semestre
en cours.
"""
# Vérification de l'accès: permission Observateir sur tous les départements
# Vérification de l'accès: permission Observateur sur tous les départements
# (c'est un exemple à compléter)
if not g.current_user.has_permission(Permission.ScoObservateur, None):
return error_response(401, message="accès interdit")
@ -78,3 +79,413 @@ def etudiants():
FormSemestre.date_fin >= func.now(),
)
return jsonify([e.to_dict_bul(include_urls=False) for e in query])
######################## Departements ##################################
@bp.route("/departements", methods=["GET"])
@token_auth.login_required
def departements():
"""
Liste des ids de départements
"""
depts = models.Departement.query.filter_by(visible=True).all()
data = [d.id for d in depts]
return jsonify(data)
@bp.route("/departements/<string:dept>/etudiants/liste/<int:sem_id>", methods=["GET"])
@token_auth.login_required
def liste_etudiants(dept, *args, sem_id): # XXX TODO A REVOIR
"""
Liste des étudiants d'un département
"""
# Test si le sem_id à été renseigné ou non
if sem_id is not None:
# Récupération du/des depts
list_depts = models.Departement.query.filter(
models.Departement.acronym == dept,
models.FormSemestre.semestre_id == sem_id,
)
list_etuds = []
for dept in list_depts:
# Récupération des étudiants d'un département
x = models.Identite.query.filter(models.Identite.dept_id == dept.getId())
for y in x:
# Ajout des étudiants dans la liste global
list_etuds.append(y)
else:
list_depts = models.Departement.query.filter(
models.Departement.acronym == dept,
models.FormSemestre.semestre_id == models.Departement.formsemestres,
)
list_etuds = []
for dept in list_depts:
x = models.Identite.query.filter(models.Identite.dept_id == dept.getId())
for y in x:
list_etuds.append(y)
data = [d.to_dict() for d in list_etuds]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/departements/<string:dept>/semestres_actifs", methods=["GET"])
@token_auth.login_required
def liste_semestres_actifs(dept): # TODO : changer nom
"""
Liste des semestres actifs d'un départements donné
"""
# Récupération de l'id du dept
dept_id = models.Departement.query.filter(models.Departement.acronym == dept)
# Puis ici récupération du FormSemestre correspondant
depts_actifs = models.FormSemestre.query.filter_by(
etat=True,
dept_id=dept_id,
)
data = [da.to_dict() for da in depts_actifs]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/referentiel_competences/<int:referentiel_competence_id>")
@token_auth.login_required
def referentiel_competences(referentiel_competence_id):
"""
Le référentiel de compétences
"""
ref = ApcReferentielCompetences.query.get_or_404(referentiel_competence_id)
return jsonify(ref.to_dict())
####################### Etudiants ##################################
@bp.route("/etudiant/<int:etudid>", methods=["GET"])
@token_auth.login_required
def etudiant(etudid):
"""
Un dictionnaire avec les informations de l'étudiant correspondant à l'id passé en paramètres.
"""
etud: Identite = Identite.query.get_or_404(etudid)
return jsonify(etud.to_dict_bul())
@bp.route("/etudiant/<int:etudid>/semestre/<int:sem_id>/bulletin", methods=["GET"])
@token_auth.login_required
def etudiant_bulletin_semestre(etudid, sem_id):
"""
Le bulletin d'un étudiant en fonction de son id et d'un semestre donné
"""
# return jsonify(models.BulAppreciations.query.filter_by(etudid=etudid, formsemestre_id=sem_id))
return error_response(501, message="Not implemented")
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/nip/<int:NIP>/releve",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/id/<int:etudid>/releve",
methods=["GET"],
)
@bp.route(
"/formsemestre/<int:formsemestre_id>/departements/<string:dept>/etudiant/ine/<int:numScodoc>/releve",
methods=["GET"],
)
@token_auth.login_required
def etudiant_bulletin(formsemestre_id, dept, etudid, format="json", *args, size):
"""
Un bulletin de note
"""
formsemestres = models.FormSemestre.query.filter_by(id=formsemestre_id)
depts = models.Departement.query.filter_by(acronym=dept)
etud = ""
data = []
if args[0] == "short":
pass
elif args[0] == "selectevals":
pass
elif args[0] == "long":
pass
else:
return "erreur"
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route(
"/etudiant/<int:etudid>/semestre/<int:formsemestre_id>/groups", methods=["GET"]
)
@token_auth.login_required
def etudiant_groups(etudid: int, formsemestre_id: int):
"""
Liste des groupes auxquels appartient l'étudiant dans le semestre indiqué
"""
semestre = models.FormSemestre.query.filter_by(id=formsemestre_id)
etudiant = models.Identite.query.filter_by(id=etudid)
groups = models.Partition.query.filter(
models.Partition.formsemestre_id == semestre,
models.GroupDescr.etudiants == etudiant,
)
data = [d.to_dict() for d in groups]
# return jsonify(data)
return error_response(501, message="Not implemented")
#######################" Programmes de formations #########################
@bp.route("/formations", methods=["GET"])
@bp.route("/formations/<int:formation_id>", methods=["GET"])
@token_auth.login_required
def formations(formation_id: int):
"""
Liste des formations
"""
formations = models.Formation.query.filter_by(id=formation_id)
data = [d.to_dict() for d in formations]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/formations/formation_export/<int:formation_id>", methods=["GET"])
@token_auth.login_required
def formation_export(formation_id: int, export_ids=False):
"""
La formation, avec UE, matières, modules
"""
return error_response(501, message="Not implemented")
###################### UE #######################################
@bp.route(
"/departements/<string:dept>/formations/programme/<string:sem_id>", methods=["GET"]
)
@token_auth.login_required
def eus(dept: str, sem_id: int):
"""
Liste des UES, ressources et SAE d'un semestre
"""
return error_response(501, message="Not implemented")
######## Semestres de formation ###############
@bp.route("/formations/formsemestre/<int:formsemestre_id>", methods=["GET"])
@bp.route("/formations/apo/<int:etape_apo>", methods=["GET"])
@token_auth.login_required
def formsemestre(
id: int,
):
"""
Information sur les formsemestres
"""
return error_response(501, message="Not implemented")
############ Modules de formation ##############
@bp.route("/formations/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
@bp.route(
"/formations/moduleimpl/<int:moduleimpl_id>/formsemestre/<int:formsemestre_id>",
methods=["GET"],
)
@token_auth.login_required
def moduleimpl(id: int):
"""
Liste de moduleimpl
"""
return error_response(501, message="Not implemented")
########### Groupes et partitions ###############
@bp.route("/partitions/<int:formsemestre_id>", methods=["GET"])
@token_auth.login_required
def partition(formsemestre_id: int):
"""
La liste de toutes les partitions d'un formsemestre
"""
partitions = models.Partition.query.filter_by(id=formsemestre_id)
data = [d.to_dict() for d in partitions]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route(
"/partitions/formsemestre/<int:formsemestre_id>/groups/group_ids?with_codes=&all_groups=&etat=",
methods=["GET"],
)
@token_auth.login_required
def groups(formsemestre_id: int, group_ids: int):
"""
Liste des étudiants dans un groupe
"""
return error_response(501, message="Not implemented")
@bp.route(
"/partitions/set_groups?partition_id=<int:partition_id>&groups=<int:groups>&groups_to_delete=<int:groups_to_delete>&groups_to_create=<int:groups_to_create>",
methods=["POST"],
)
@token_auth.login_required
def set_groups(
partition_id: int, groups: int, groups_to_delete: int, groups_to_create: int
):
"""
Set les groups
"""
return error_response(501, message="Not implemented")
####### Bulletins de notes ###########
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
@token_auth.login_required
def evaluations(moduleimpl_id: int):
"""
Liste des évaluations à partir de l'id d'un moduleimpl
"""
evals = models.Evaluation.query.filter_by(id=moduleimpl_id)
data = [d.to_dict() for d in evals]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/evaluations/eval_notes/<int:evaluation_id>", methods=["GET"])
@token_auth.login_required
def evaluation_notes(evaluation_id: int):
"""
Liste des notes à partir de l'id d'une évaluation donnée
"""
evals = models.Evaluation.query.filter_by(id=evaluation_id)
notes = evals.get_notes()
data = [d.to_dict() for d in notes]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route(
"/evaluations/eval_set_notes?eval_id=<int:eval_id>&etudid=<int:etudid>&note=<int:note>",
methods=["POST"],
)
@token_auth.login_required
def evaluation_set_notes(eval_id: int, etudid: int, note: float):
"""
Set les notes d'une évaluation pour un étudiant donnée
"""
return error_response(501, message="Not implemented")
############## Absences #############
@bp.route("/absences/<int:etudid>", methods=["GET"])
@bp.route("/absences/<int:etudid>/abs_just_only", methods=["GET"])
def absences(etudid: int):
"""
Liste des absences d'un étudiant donnée
"""
abs = models.Absence.query.filter_by(id=etudid)
data = [d.to_dict() for d in abs]
# return jsonify(data)
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_signale", methods=["POST"])
@token_auth.login_required
def abs_signale():
"""
Retourne un html
"""
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_annule", methods=["POST"])
@token_auth.login_required
def abs_annule():
"""
Retourne un html
"""
return error_response(501, message="Not implemented")
@bp.route("/absences/abs_annule_justif", methods=["POST"])
@token_auth.login_required
def abs_annule_justif():
"""
Retourne un html
"""
return error_response(501, message="Not implemented")
@bp.route(
"/absences/abs_group_etat/?group_ids=<int:group_ids>&date_debut=date_debut&date_fin=date_fin",
methods=["GET"],
)
@token_auth.login_required
def abs_groupe_etat(
group_ids: int, date_debut, date_fin, with_boursier=True, format="html"
):
"""
Liste des absences d'un ou plusieurs groupes entre deux dates
"""
return error_response(501, message="Not implemented")
################ Logos ################
@bp.route("/logos", methods=["GET"])
@token_auth.login_required
def liste_logos(format="json"):
"""
Liste des logos définis pour le site scodoc.
"""
return error_response(501, message="Not implemented")
@bp.route("/logos/<string:nom>", methods=["GET"])
@token_auth.login_required
def recup_logo_global(nom: str):
"""
Retourne l'image au format png ou jpg
"""
return error_response(501, message="Not implemented")
@bp.route("/departements/<string:dept>/logos", methods=["GET"])
@token_auth.login_required
def logo_dept(dept: str):
"""
Liste des logos définis pour le département visé.
"""
return error_response(501, message="Not implemented")
@bp.route("/departement/<string:dept>/logos/<string:nom>", methods=["GET"])
@token_auth.login_required
def recup_logo_dept_global(dept: str, nom: str):
"""
L'image format png ou jpg
"""
return error_response(501, message="Not implemented")

View File

@ -76,7 +76,9 @@ class User(UserMixin, db.Model):
"Departement",
foreign_keys=[Departement.acronym],
primaryjoin=(dept == Departement.acronym),
lazy="dynamic",
lazy="select",
passive_deletes="all",
uselist=False,
)
def __init__(self, **kwargs):
@ -112,6 +114,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.
@ -235,7 +238,7 @@ class User(UserMixin, db.Model):
def get_dept_id(self) -> int:
"returns user's department id, or None"
if self.dept:
return self._departement.first().id
return self._departement.id
return None
# Permissions management:

View File

@ -4,153 +4,118 @@
# See LICENSE
##############################################################################
from collections import defaultdict
"""Génération bulletin BUT
"""
import datetime
from flask import url_for, g
import numpy as np
import pandas as pd
from app.models.formsemestre import FormSemestre
from app import db
from app.comp import moy_ue, moy_sem, inscr_mod
from app.models import ModuleImpl
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_utils import jsnan, fmt_note
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import fmt_note
from app.comp.res_but import ResultatsSemestreBUT
class ResultatsSemestreBUT:
"""Structure légère pour stocker les résultats du semestre et
générer les bulletins.
__init__ : charge depuis le cache ou calcule
class BulletinBUT:
"""Génération du bulletin BUT.
Cette classe génère des dictionnaires avec toutes les informations
du bulletin, qui sont immédiatement traduisibles en JSON.
"""
_cached_attrs = (
"sem_cube",
"modimpl_inscr_df",
"modimpl_coefs_df",
"etud_moy_ue",
"modimpls_evals_poids",
"modimpls_evals_notes",
"etud_moy_gen",
"etud_moy_gen_ranks",
"modimpls_evaluations_complete",
)
def __init__(self, formsemestre):
self.formsemestre = formsemestre
self.ues = formsemestre.query_ues().all()
self.modimpls = formsemestre.modimpls.all()
self.etuds = self.formsemestre.get_inscrits(include_dem=False)
self.etud_index = {e.id: idx for idx, e in enumerate(self.etuds)}
self.saes = [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.SAE
]
self.ressources = [
m for m in self.modimpls if m.module.module_type == scu.ModuleType.RESSOURCE
]
if not self.load_cached():
self.compute()
self.store()
def load_cached(self) -> bool:
"Load cached dataframes, returns False si pas en cache"
data = ResultatsSemestreBUTCache.get(self.formsemestre.id)
if not data:
return False
for attr in self._cached_attrs:
setattr(self, attr, data[attr])
return True
def store(self):
"Cache our dataframes"
ResultatsSemestreBUTCache.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
)
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
(
self.sem_cube,
self.modimpls_evals_poids,
self.modimpls_evals_notes,
modimpls_evaluations,
self.modimpls_evaluations_complete,
) = 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
)
# 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)
self.etud_moy_ue = moy_ue.compute_ue_moys(
self.sem_cube,
self.etuds,
self.modimpls,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
)
self.etud_moy_gen = moy_sem.compute_sem_moys(
self.etud_moy_ue, self.modimpl_coefs_df
)
self.etud_moy_gen_ranks = moy_sem.comp_ranks_series(self.etud_moy_gen)
def __init__(self, formsemestre: FormSemestre):
""" """
self.res = ResultatsSemestreBUT(formsemestre)
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
"dict synthèse résultats dans l'UE pour les modules indiqués"
res = self.res
d = {}
etud_idx = self.etud_index[etud.id]
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
etud_moy_module = self.sem_cube[etud_idx] # module x UE
for mi in modimpls:
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
coef = self.modimpl_coefs_df[mi.id][ue.id]
if coef > 0:
d[mi.module.code] = {
"id": mi.id,
"coef": coef,
"moyenne": fmt_note(
etud_moy_module[
self.modimpl_coefs_df.columns.get_loc(mi.id)
][ue_idx]
),
}
etud_idx = res.etud_index[etud.id]
if ue.type != UE_SPORT:
ue_idx = res.modimpl_coefs_df.index.get_loc(ue.id)
etud_moy_module = res.sem_cube[etud_idx] # module x UE
for modimpl in modimpls:
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
if ue.type != UE_SPORT:
coef = res.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[
res.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"
res = self.res
d = {
"id": ue.id,
"titre": ue.titre,
"numero": ue.numero,
"type": ue.type,
"ECTS": {
"acquis": 0, # XXX TODO voir jury
"acquis": 0, # XXX TODO voir jury #sco92
"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
"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),
"moyenne": None,
# Le bonus sport appliqué sur cette UE
"bonus": fmt_note(res.bonus_ues[ue.id][etud.id])
if res.bonus_ues is not None and ue.id in res.bonus_ues
else fmt_note(0.0),
"malus": res.malus[ue.id][etud.id],
"capitalise": None, # "AAAA-MM-JJ" TODO #sco92
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
"saes": self.etud_ue_mod_results(etud, ue, res.saes),
}
if ue.type != UE_SPORT:
if sco_preferences.get_preference("bul_show_ue_rangs", res.formsemestre.id):
rangs, effectif = res.ue_rangs[ue.id]
rang = rangs[etud.id]
else:
rang, effectif = "", 0
d["moyenne"] = {
"value": fmt_note(res.etud_moy_ue[ue.id][etud.id]),
"min": fmt_note(res.etud_moy_ue[ue.id].min()),
"max": fmt_note(res.etud_moy_ue[ue.id].max()),
"moy": fmt_note(res.etud_moy_ue[ue.id].mean()),
"rang": rang,
"total": effectif, # nb etud avec note dans cette UE
}
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 res.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:
"""dict synthèse résultats des modules indiqués,
avec évaluations de chacun."""
res = self.res
d = {}
# etud_idx = self.etud_index[etud.id]
for mi in modimpls:
for modimpl in modimpls:
# mod_idx = self.modimpl_coefs_df.columns.get_loc(mi.id)
# # moyennes indicatives (moyennes de moyennes d'UE)
# try:
@ -158,24 +123,27 @@ class ResultatsSemestreBUT:
# np.nanmean(self.sem_cube[:, mod_idx, :], axis=1),
# copy=False,
# )
# except RuntimeWarning: # all nans in np.nanmean (sur certains etuds sans notes valides)
# except RuntimeWarning:
# # all nans in np.nanmean (sur certains etuds sans notes valides)
# pass
# try:
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
# except RuntimeWarning: # all nans in np.nanmean
# pass
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
d[mi.module.code] = {
"id": mi.id,
"titre": mi.module.titre,
"code_apogee": mi.module.code_apogee,
modimpl_results = res.modimpls_results[modimpl.id]
if res.modimpl_inscr_df[modimpl.id][etud.id]: # si inscrit
d[modimpl.module.code] = {
"id": modimpl.id,
"titre": modimpl.module.titre,
"code_apogee": modimpl.module.code_apogee,
"url": url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=mi.id,
moduleimpl_id=modimpl.id,
),
"moyenne": {
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
# # moyenne indicative de module: moyenne des UE,
# # ignorant celles sans notes (nan)
# "value": fmt_note(moy_indicative_mod),
# "min": fmt_note(moyennes_etuds.min()),
# "max": fmt_note(moyennes_etuds.max()),
@ -183,16 +151,22 @@ class ResultatsSemestreBUT:
},
"evaluations": [
self.etud_eval_results(etud, e)
for eidx, e in enumerate(mi.evaluations)
for e in modimpl.evaluations
if e.visibulletin
and self.modimpls_evaluations_complete[mi.id][eidx]
and (
modimpl_results.evaluations_etat[e.id].is_complete
or sco_preferences.get_preference(
"bul_show_all_evals", res.formsemestre.id
)
)
],
}
return d
def etud_eval_results(self, etud, e) -> dict:
"dict resultats d'un étudiant à une évaluation"
eval_notes = self.modimpls_evals_notes[e.moduleimpl_id][e.id] # pd.Series
# eval_notes est une pd.Series avec toutes les notes des étudiants inscrits
eval_notes = self.res.modimpls_results[e.moduleimpl_id].evals_notes[e.id]
notes_ok = eval_notes.where(eval_notes > scu.NOTES_ABSENCE).dropna()
d = {
"id": e.id,
@ -204,7 +178,7 @@ class ResultatsSemestreBUT:
"poids": {p.ue.acronyme: p.poids for p in e.ue_poids},
"note": {
"value": fmt_note(
self.modimpls_evals_notes[e.moduleimpl_id][e.id][etud.id],
eval_notes[etud.id],
note_max=e.note_max,
),
"min": fmt_note(notes_ok.min()),
@ -219,13 +193,43 @@ class 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"."""
res = self.res
if res.bonus_ues is None or res.bonus_ues.shape[1] == 0:
return ""
bonus_vect = res.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 res.ues
if res.modimpls_in_ue(ue.id, etudid)
and ue.id in res.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).
"""
res = self.res
etat_inscription = etud.etat_inscription(formsemestre.id)
nb_inscrits = self.res.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,
@ -235,19 +239,23 @@ class ResultatsSemestreBUT:
},
"formsemestre_id": formsemestre.id,
"etat_inscription": etat_inscription,
"options": bulletin_option_affichage(formsemestre),
"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(),
"date_fin": formsemestre.date_fin.isoformat(),
"annee_universitaire": self.formsemestre.annee_scolaire_str(),
"inscription": "TODO-MM-JJ", # XXX TODO
"annee_universitaire": formsemestre.annee_scolaire_str(),
"numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [], # XXX TODO
"absences": { # XXX TODO
"injustifie": 1,
"total": 33,
"absences": {
"injustifie": nbabs - nbabsjust,
"total": nbabs,
},
}
semestre_infos.update(
@ -257,23 +265,27 @@ class ResultatsSemestreBUT:
semestre_infos.update(
{
"notes": { # moyenne des moyennes générales du semestre
"value": fmt_note(self.etud_moy_gen[etud.id]),
"min": fmt_note(self.etud_moy_gen.min()),
"moy": fmt_note(self.etud_moy_gen.mean()),
"max": fmt_note(self.etud_moy_gen.max()),
"value": fmt_note(res.etud_moy_gen[etud.id]),
"min": fmt_note(res.etud_moy_gen.min()),
"moy": fmt_note(res.etud_moy_gen.mean()),
"max": fmt_note(res.etud_moy_gen.max()),
},
"rang": { # classement wrt moyenne général, indicatif
"value": self.etud_moy_gen_ranks[etud.id],
"total": len(self.etuds),
"value": res.etud_moy_gen_ranks[etud.id],
"total": nb_inscrits,
},
},
)
d.update(
{
"ressources": self.etud_mods_results(etud, self.ressources),
"saes": self.etud_mods_results(etud, self.saes),
"ressources": self.etud_mods_results(etud, res.ressources),
"saes": self.etud_mods_results(etud, res.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 res.ues
if self.res.modimpls_in_ue(
ue.id, etud.id
) # si l'UE comporte des modules auxquels on est inscrit
},
"semestre": semestre_infos,
},
@ -287,7 +299,7 @@ class ResultatsSemestreBUT:
"moy": "",
"max": "",
},
"rang": {"value": "DEM", "total": len(self.etuds)},
"rang": {"value": "DEM", "total": nb_inscrits},
}
)
d.update(
@ -300,125 +312,3 @@ class ResultatsSemestreBUT:
)
return d
def bulletin_option_affichage(formsemestre):
"dict avec les options d'affichages (préférences) pour ce semestre"
prefs = sco_preferences.SemPreferences(formsemestre.id)
fields = (
"bul_show_abs",
"bul_show_abs_modules",
"bul_show_ects",
"bul_show_codemodules",
"bul_show_matieres",
"bul_show_rangs",
"bul_show_ue_rangs",
"bul_show_mod_rangs",
"bul_show_moypromo",
"bul_show_minmax",
"bul_show_minmax_mod",
"bul_show_minmax_eval",
"bul_show_coef",
"bul_show_ue_cap_details",
"bul_show_ue_cap_current",
"bul_show_temporary",
"bul_temporary_txt",
"bul_show_uevalid",
"bul_show_date_inscr",
)
# on enlève le "bul_" de la clé:
return {field[4:]: prefs[field] for field in fields}
# Pour raccorder le code des anciens bulletins qui attendent une NoteTable
class APCNotesTableCompat:
"""Implementation partielle de NotesTable pour les formations APC
Accès aux notes et rangs.
"""
def __init__(self, formsemestre):
self.results = ResultatsSemestreBUT(formsemestre)
nb_etuds = len(self.results.etuds)
self.rangs = self.results.etud_moy_gen_ranks
self.moy_min = self.results.etud_moy_gen.min()
self.moy_max = self.results.etud_moy_gen.max()
self.moy_moy = self.results.etud_moy_gen.mean()
self.bonus = defaultdict(lambda: 0.0) # XXX
self.ue_rangs = {
u.id: (defaultdict(lambda: 0.0), nb_etuds) for u in self.results.ues
}
self.mod_rangs = {
m.id: (defaultdict(lambda: 0), nb_etuds) for m in self.results.modimpls
}
def get_ues(self):
ues = []
for ue in self.results.ues:
d = ue.to_dict()
d.update(
{
"max": self.results.etud_moy_ue[ue.id].max(),
"min": self.results.etud_moy_ue[ue.id].min(),
"moy": self.results.etud_moy_ue[ue.id].mean(),
"nb_moy": len(self.results.etud_moy_ue),
}
)
ues.append(d)
return ues
def get_modimpls(self):
return [m.to_dict() for m in self.results.modimpls]
def get_etud_moy_gen(self, etudid):
return self.results.etud_moy_gen[etudid]
def get_moduleimpls_attente(self):
return [] # XXX TODO
def get_etud_rang(self, etudid):
return self.rangs[etudid]
def get_etud_rang_group(self, etudid, group_id):
return (None, 0) # XXX unimplemented TODO
def get_etud_ue_status(self, etudid, ue_id):
return {
"cur_moy_ue": self.results.etud_moy_ue[ue_id][etudid],
"is_capitalized": False, # XXX TODO
}
def get_etud_mod_moy(self, moduleimpl_id, etudid):
mod_idx = self.results.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
etud_idx = self.results.etud_index[etudid]
# moyenne sur les UE:
self.results.sem_cube[etud_idx, mod_idx].mean()
def get_mod_stats(self, moduleimpl_id):
return {
"moy": "-",
"max": "-",
"min": "-",
"nb_notes": "-",
"nb_missing": "-",
"nb_valid_evals": "-",
}
def get_evals_in_mod(self, moduleimpl_id):
mi = ModuleImpl.query.get(moduleimpl_id)
evals_results = []
for e in mi.evaluations:
d = e.to_dict()
d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin
d["jour"] = e.jour # datetime
d["notes"] = {
etud.id: {
"etudid": etud.id,
"value": self.results.modimpls_evals_notes[e.moduleimpl_id][e.id][
etud.id
],
}
for etud in self.results.etuds
}
evals_results.append(d)
return evals_results

View File

@ -69,10 +69,11 @@ def bulletin_but_xml_compat(
% (formsemestre_id, etudid)
)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
etud = Identite.query.get_or_404(etudid)
etud: Identite = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
nb_inscrits = len(results.etuds)
etat_inscription = etud.etat_inscription(formsemestre.id)
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
# etat_inscription = etud.etat_inscription(formsemestre.id)
etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat
if (not formsemestre.bul_hide_xml) or force_publishing:
published = 1
else:
@ -109,8 +110,8 @@ def bulletin_but_xml_compat(
code_ine=etud.code_ine or "",
nom=scu.quote_xml_attr(etud.nom),
prenom=scu.quote_xml_attr(etud.prenom),
civilite=scu.quote_xml_attr(etud.civilite_str()),
sexe=scu.quote_xml_attr(etud.civilite_str()), # compat
civilite=scu.quote_xml_attr(etud.civilite_str),
sexe=scu.quote_xml_attr(etud.civilite_str), # compat
photo_url=scu.quote_xml_attr(sco_photos.get_etud_photo_url(etud.id)),
email=scu.quote_xml_attr(etud.get_first_email() or ""),
emailperso=scu.quote_xml_attr(etud.get_first_email("emailperso") or ""),
@ -133,14 +134,18 @@ 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
doc.append(Element("bonus_sport_culture", value=str(bonus)))
# Liste les UE / modules /evals
for ue in results.ues:
for ue in results.ues: # avec bonus
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
nb_inscrits_ue = (
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
@ -156,25 +161,31 @@ def bulletin_but_xml_compat(
doc.append(x_ue)
if ue.type != sco_codes_parcours.UE_SPORT:
v = results.etud_moy_ue[ue.id][etud.id]
vmin = results.etud_moy_ue[ue.id].min()
vmax = results.etud_moy_ue[ue.id].max()
else:
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
v = results.bonus or 0.0
vmin = vmax = 0.0
x_ue.append(
Element(
"note",
value=scu.fmt_note(v),
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
min=scu.fmt_note(vmin),
max=scu.fmt_note(vmax),
)
)
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
x_ue.append(Element("rang", value=str(rang_ue)))
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
# Liste les modules rattachés à cette UE
for modimpl in results.modimpls:
for modimpl in results.formsemestre.modimpls:
# Liste ici uniquement les modules rattachés à cette UE
if modimpl.module.ue.id == ue.id:
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
# mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
try:
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
except KeyError:
coef = 0.0
x_mod = Element(
"module",
id=str(modimpl.id),
@ -187,16 +198,6 @@ def bulletin_but_xml_compat(
modimpl.module.code_apogee or ""
),
)
x_ue.append(x_mod)
x_mod.append(
Element(
"note",
value=mod_moy,
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
)
)
# XXX TODO rangs et effectifs
# --- notes de chaque eval:
if version != "short":
@ -219,17 +220,23 @@ def bulletin_but_xml_compat(
note_max_origin=str(e.note_max),
)
x_mod.append(x_eval)
x_eval.append(
Element(
"note",
value=scu.fmt_note(
results.modimpls_evals_notes[
e.moduleimpl_id
][e.id][etud.id],
note_max=e.note_max,
),
try:
x_eval.append(
Element(
"note",
value=scu.fmt_note(
results.modimpls_results[
e.moduleimpl_id
].evals_notes[e.id][etud.id],
note_max=e.note_max,
),
)
)
)
except KeyError:
x_eval.append(
Element("note", value="", note_max="")
)
# XXX TODO: Evaluations incomplètes ou futures: XXX
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
@ -322,13 +329,3 @@ def bulletin_but_xml_compat(
return None
else:
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
"""
formsemestre_id=718
etudid=12496
from app.but.bulletin_but import *
mapp.set_sco_dept("RT")
sem = FormSemestre.query.get(formsemestre_id)
r = ResultatsSemestreBUT(sem)
"""

View File

@ -19,10 +19,12 @@ class FormationRefCompForm(FlaskForm):
class RefCompLoadForm(FlaskForm):
referentiel_standard = SelectField(
"Choisir un référentiel de compétences officiel BUT"
)
upload = FileField(
label="Sélectionner un fichier XML Orébut",
label="Ou bien sélectionner un fichier XML au format Orébut",
validators=[
FileRequired(),
FileAllowed(
[
"xml",
@ -33,3 +35,13 @@ class RefCompLoadForm(FlaskForm):
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler")
def validate(self):
if not super().validate():
return False
if (self.referentiel_standard.data == "0") == (not self.upload.data):
self.referentiel_standard.errors.append(
"Choisir soit un référentiel, soit un fichier xml"
)
return False
return True

View File

@ -6,6 +6,8 @@
from xml.etree import ElementTree
from typing import TextIO
import sqlalchemy
from app import db
from app.models.but_refcomp import (
@ -19,7 +21,7 @@ from app.models.but_refcomp import (
ApcAnneeParcours,
ApcParcoursNiveauCompetence,
)
from app.scodoc.sco_exceptions import ScoFormatError
from app.scodoc.sco_exceptions import ScoFormatError, ScoValueError
def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
@ -27,10 +29,20 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
peut lever TypeError ou ScoFormatError
Résultat: instance de ApcReferentielCompetences
"""
# Vérifie que le même fichier n'a pas déjà été chargé:
if ApcReferentielCompetences.query.filter_by(
scodoc_orig_filename=orig_filename, dept_id=dept_id
).count():
raise ScoValueError(
f"""Un référentiel a déjà été chargé d'un fichier de même nom.
({orig_filename})
Supprimez-le ou changer le nom du fichier."""
)
try:
root = ElementTree.XML(xml_data)
except ElementTree.ParseError:
raise ScoFormatError("fichier XML Orébut invalide")
except ElementTree.ParseError as exc:
raise ScoFormatError(f"fichier XML Orébut invalide (2): {exc.args}")
if root.tag != "referentiel_competence":
raise ScoFormatError("élément racine 'referentiel_competence' manquant")
args = ApcReferentielCompetences.attr_from_xml(root.attrib)
@ -42,7 +54,16 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
if not competences:
raise ScoFormatError("élément 'competences' manquant")
for competence in competences.findall("competence"):
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
try:
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
db.session.flush()
except sqlalchemy.exc.IntegrityError:
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
db.session.rollback()
raise ScoValueError(
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
"""
)
ref.competences.append(c)
# --- SITUATIONS
situations = competence.find("situations")
@ -54,8 +75,8 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
composantes = competence.find("composantes_essentielles")
for composante in composantes:
libelle = "".join(composante.itertext()).strip()
ce = ApcComposanteEssentielle(libelle=libelle)
c.composantes_essentielles.append(ce)
compo_ess = ApcComposanteEssentielle(libelle=libelle)
c.composantes_essentielles.append(compo_ess)
# --- NIVEAUX (années)
niveaux = competence.find("niveaux")
for niveau in niveaux:
@ -77,16 +98,14 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
a = ApcAnneeParcours(**ApcAnneeParcours.attr_from_xml(annee.attrib))
parc.annees.append(a)
for competence in annee.findall("competence"):
nom = competence.attrib["nom"]
comp_id_orebut = competence.attrib["id"]
niveau = int(competence.attrib["niveau"])
# Retrouve la competence
comp = ref.competences.filter_by(titre=nom).all()
if len(comp) == 0:
raise ScoFormatError(f"competence {nom} référencée mais on définie")
elif len(comp) > 1:
raise ScoFormatError(f"competence {nom} ambigüe")
comp = ref.competences.filter_by(id_orebut=comp_id_orebut).first()
if comp is None:
raise ScoFormatError(f"competence {comp_id_orebut} non définie")
ass = ApcParcoursNiveauCompetence(
niveau=niveau, annee_parcours=a, competence=comp[0]
niveau=niveau, annee_parcours=a, competence=comp
)
db.session.add(ass)

44
app/comp/aux_stats.py Normal file
View File

@ -0,0 +1,44 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Quelques classes auxiliaires pour les calculs des notes
"""
import numpy as np
class StatsMoyenne:
"""Une moyenne d'un ensemble étudiants sur quelque chose
(moyenne générale d'un semestre, d'un module, d'un groupe...)
et les statistiques associées: min, max, moy, effectif
"""
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)
"""
try:
if vals is None or len(vals) == 0 or np.isnan(vals).all():
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))
except TypeError: # que des NaN dans un array d'objets, ou ce genre de choses exotiques...
self.moy = self.min = self.max = self.size = self.nb_vals = 0
def to_dict(self):
"Tous les attributs dans un dict"
return {
"min": self.min,
"max": self.max,
"moy": self.moy,
"size": self.size,
"nb_vals": self.nb_vals,
}

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

@ -0,0 +1,717 @@
##############################################################################
# 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 math
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
]
)
if not len(modimpl_mask):
modimpl_mask = np.s_[:] # il n'y a rien, on prend tout donc rien
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]
if nb_etuds == 0 or nb_mod_sport == 0:
return # no bonus at all
# 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] * 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
if nb_etuds == 0:
modimpl_coefs_etuds = modimpl_inscr_spo # vide
else:
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.DataFrame:
"""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:
"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
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
)
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"""
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:
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 BonusAnnecy(BonusSport):
"""Calcul bonus modules optionnels (sport), règle IUT d'Annecy.
Il peut y avoir plusieurs modules de bonus.
Prend pour chaque étudiant la meilleure de ses notes bonus et
ajoute à chaque UE :
0.05 point si >=10,
0.1 point si >=12,
0.15 point si >=14,
0.2 point si >=16,
0.25 point si >=18.
"""
name = "bonus_iut_annecy"
displayed_name = "IUT d'Annecy"
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
# if math.prod(sem_modimpl_moys_inscrits.shape) == 0:
# return # no etuds or no mod sport
# Prend la note de chaque modimpl, sans considération d'UE
if len(sem_modimpl_moys_inscrits.shape) > 2: # apc
sem_modimpl_moys_inscrits = sem_modimpl_moys_inscrits[:, :, 0]
# ici sem_modimpl_moys_inscrits est nb_etuds x nb_mods_bonus, en APC et en classic
note_bonus_max = np.max(sem_modimpl_moys_inscrits, axis=1) # 1d, nb_etuds
bonus = np.zeros(note_bonus_max.shape)
bonus[note_bonus_max >= 18.0] = 0.25
bonus[note_bonus_max >= 16.0] = 0.20
bonus[note_bonus_max >= 14.0] = 0.15
bonus[note_bonus_max >= 12.0] = 0.10
bonus[note_bonus_max >= 10.0] = 0.05
# Bonus moyenne générale et sur les UE
self.bonus_moy_gen = pd.Series(bonus, index=self.etuds_idx, dtype=float)
ues_idx = [ue.id for ue in self.formsemestre.query_ues(with_sport=False)]
nb_ues_no_bonus = len(ues_idx)
self.bonus_ues = pd.DataFrame(
np.stack([bonus] * nb_ues_no_bonus, axis=1),
columns=ues_idx,
index=self.etuds_idx,
dtype=float,
)
class BonusBethune(BonusSportMultiplicatif):
"""Calcul bonus modules optionnels (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 optionnels (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 optionnels (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"
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 optionnels (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 optionnels (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 optionnels (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 optionnels (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 optionnels (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 optionnels (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

@ -43,7 +43,7 @@ class ModuleCoefsCache(sco_cache.ScoDocCache):
class EvaluationsPoidsCache(sco_cache.ScoDocCache):
"""Cache for poids evals
Clé: moduleimpl_id
Valeur: DataFrame (df_load_evaluations_poids)
Valeur: DataFrame (load_evaluations_poids)
"""
prefix = "EPC"

View File

@ -16,13 +16,13 @@ from app import models
#
def df_load_modimpl_inscr(formsemestre) -> pd.DataFrame:
"""Charge la matrice des inscriptions aux modules du semestre
rows: etudid
rows: etudid (inscrits au semestre, avec DEM et DEF)
columns: moduleimpl_id (en chaîne)
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]
etudids = [i.etudid for i in formsemestre.get_inscrits(include_dem=False)]
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:
ins_df = pd.read_sql_query(
@ -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(

154
app/comp/jury.py Normal file
View File

@ -0,0 +1,154 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Stockage des décisions de jury
"""
import pandas as pd
from app import db
from app.models import FormSemestre, ScolarFormSemestreValidation
from app.comp.res_cache import ResultatsCache
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
class ValidationsSemestre(ResultatsCache):
""" """
_cached_attrs = (
"decisions_jury",
"decisions_jury_ues",
"ue_capitalisees",
)
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre, sco_cache.ValidationsSemestreCache)
self.decisions_jury = {}
"""Décisions prises dans ce semestre:
{ etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}"""
self.decisions_jury_ues = {}
"""Décisions sur des UEs dans ce semestre:
{ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
"""
self.ue_capitalisees: pd.DataFrame = None
"""DataFrame, index etudid
formsemestre_id : origine de l'UE capitalisée
is_external : vrai si validation effectuée dans un semestre extérieur
ue_id : dans le semestre origine (pas toujours de la même formation)
ue_code : code de l'UE, moy_ue : note enregistrée,
event_date : date de la validation (jury)."""
if not self.load_cached():
self.compute()
self.store()
def compute(self):
"Charge les résultats de jury et UEs capitalisées"
self.ue_capitalisees = formsemestre_get_ue_capitalisees(self.formsemestre)
self.comp_decisions_jury()
def comp_decisions_jury(self):
"""Cherche les decisions du jury pour le semestre (pas les UE).
Calcule les attributs:
decisions_jury = { etudid : { 'code' : None|ATT|..., 'assidu' : 0|1 }}
decision_jury_ues={ etudid : { ue_id : { 'code' : Note|ADM|CMP, 'event_date' }}}
Si la décision n'a pas été prise, la clé etudid n'est pas présente.
Si l'étudiant est défaillant, pas de décisions d'UE.
"""
# repris de NotesTable.comp_decisions_jury pour la compatibilité
decisions_jury_q = ScolarFormSemestreValidation.query.filter_by(
formsemestre_id=self.formsemestre.id
)
decisions_jury = {}
for decision in decisions_jury_q.filter(
ScolarFormSemestreValidation.ue_id == None # slt dec. sem.
):
decisions_jury[decision.etudid] = {
"code": decision.code,
"assidu": decision.assidu,
"compense_formsemestre_id": decision.compense_formsemestre_id,
"event_date": decision.event_date.strftime("%d/%m/%Y"),
}
self.decisions_jury = decisions_jury
# UEs:
decisions_jury_ues = {}
for decision in decisions_jury_q.filter(
ScolarFormSemestreValidation.ue_id != None # slt dec. sem.
):
if decision.etudid not in decisions_jury_ues:
decisions_jury_ues[decision.etudid] = {}
# Calcul des ECTS associés à cette UE:
if sco_codes_parcours.code_ue_validant(decision.code):
ects = decision.ue.ects or 0.0 # 0 if None
else:
ects = 0.0
decisions_jury_ues[decision.etudid][decision.ue.id] = {
"code": decision.code,
"ects": ects, # 0. si UE non validée
"event_date": decision.event_date.strftime("%d/%m/%Y"),
}
self.decisions_jury_ues = decisions_jury_ues
def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame:
"""Liste des UE capitalisées (ADM) utilisables dans ce formsemestre
Recherche dans les semestres des formations de même code, avec le même semestre_id
et une date de début antérieure que celle du formsemestre.
Prend aussi les UE externes validées.
Attention: fonction très coûteuse, cacher le résultat.
Résultat: DataFrame avec
etudid (index)
formsemestre_id : origine de l'UE capitalisée
is_external : vrai si validation effectuée dans un semestre extérieur
ue_id : dans le semestre origine (pas toujours de la même formation)
ue_code : code de l'UE
moy_ue :
event_date :
} ]
"""
query = """
SELECT DISTINCT SFV.*, ue.ue_code
FROM
notes_ue ue,
notes_formations nf,
notes_formations nf2,
scolar_formsemestre_validation SFV,
notes_formsemestre sem,
notes_formsemestre_inscription ins
WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and ins.etudid = SFV.etudid
and ins.formsemestre_id = %(formsemestre_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
"""
params = {
"formation_id": formsemestre.formation.id,
"formsemestre_id": formsemestre.id,
"semestre_id": formsemestre.semestre_id,
"date_debut": formsemestre.date_debut,
}
df = pd.read_sql_query(query, db.engine, params=params, index_col="etudid")
return df

View File

@ -27,128 +27,160 @@
"""Fonctions de calcul des moyennes de modules (modules, ressources ou SAÉ)
Pour les formations classiques et le BUT
Rappel: pour éviter les confusions, on appelera *poids* les coefficients d'une
évaluation dans un module, et *coefficients* ceux utilisés pour le calcul de la
moyenne générale d'une UE.
"""
from dataclasses import dataclass
import numpy as np
import pandas as pd
from pandas.core.frame import DataFrame
from app import db
from app import log
from app import models
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import ModuleType
def df_load_evaluations_poids(
moduleimpl_id: int, default_poids=1.0
) -> 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)
@dataclass
class EvaluationEtat:
"""Classe pour stocker quelques infos sur les résultats d'une évaluation"""
evaluation_id: int
nb_attente: int
is_complete: bool
class ModuleImplResults:
"""Classe commune à toutes les formations (standard et APC).
Les notes des étudiants d'un moduleimpl.
Les poids des évals sont à part car on en a besoin sans les notes pour les
tableaux de bord.
Les attributs sont tous des objets simples cachables dans Redis;
les caches sont gérés par ResultatsSemestre.
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
df = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for eval_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
try:
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
except KeyError as exc:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
if default_poids is not None:
df.fillna(value=default_poids, inplace=True)
return df, ues
def __init__(self, moduleimpl: ModuleImpl):
self.moduleimpl_id = moduleimpl.id
self.module_id = moduleimpl.module.id
self.etudids = None
"liste des étudiants inscrits au SEMESTRE (incluant dem et def)"
def check_moduleimpl_conformity(
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
) -> bool:
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
au PN.
Un module est dit *conforme* si et seulement si la somme des poids de ses
évaluations vers une UE de coefficient non nul est non nulle.
"""
nb_evals, nb_ues = evals_poids.shape
if nb_evals == 0:
return True # modules vides conformes
if nb_ues == 0:
return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues:
# bug ?
log(
"check_moduleimpl_conformity: nb ue incoherent (moduleimpl.id={moduleimpl.id})"
)
return False
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all(
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)
== module_evals_poids
)
return check
self.nb_inscrits_module = None
"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 = {}
"{ evaluation.id : bool } indique si à prendre en compte ou non."
self.evaluations_etat = {}
"{ evaluation_id: EvaluationEtat }"
self.en_attente = False
"Vrai si au moins une évaluation a une note en attente"
#
self.evals_notes = None
"""DataFrame, colonnes: EVALS, Lignes: etudid (inscrits au SEMESTRE)
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
"""
self.etuds_moy_module = None
"""DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE.
"""
self.load_notes()
self.etuds_use_session2 = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module vient de la session2"""
self.etuds_use_rattrapage = pd.Series(False, index=self.evals_notes.index)
"""1 bool par etud, indique si sa moyenne de module utilise la note de rattrapage"""
def load_notes(self): # ré-écriture de df_load_modimpl_notes
"""Charge toutes les notes de toutes les évaluations du module.
Dataframe evals_notes
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
"""Construit un dataframe avec toutes les notes de toutes les évaluations du module.
colonnes: le nom de la colonne est l'evaluation_id (int)
index (lignes): etudid (int)
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Résultat: (evals_notes, liste de évaluations du moduleimpl,
liste de booleens indiquant si l'évaluation est "complete")
Les notes sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, NON normalisée sur 20)
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
absent: NOTES_ABSENCE (NULL en bd)
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
Évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise en compte immédiate" (publish_incomplete)
Les notes renvoyées sont "brutes" (séries de floats) et peuvent prendre les valeurs:
note : float (valeur enregistrée brute, non normalisée sur 20)
pas de note: NaN (rien en bd, ou étudiant non inscrit au module)
absent: NOTES_ABSENCE (NULL en bd)
excusé: NOTES_NEUTRALISE (voir sco_utils)
attente: NOTES_ATTENTE
Évaluation "attente" (prise en compte dans les calculs, mais il y
manque des notes) ssi il y a des étudiants inscrits au semestre et au module
qui ont des notes ATT.
"""
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
self.etudids = self._etudids()
L'évaluation "complete" (prise en compte dans les calculs) si:
- soit tous les étudiants inscrits au module ont des notes
- soit elle a été déclarée "à prise ne compte immédiate" (publish_incomplete)
N'utilise pas de cache ScoDoc.
"""
# L'index du dataframe est la liste des étudiants inscrits au semestre,
# sans les démissionnaires
etudids = [
e.etudid
for e in ModuleImpl.query.get(moduleimpl_id).formsemestre.get_inscrits(
include_dem=False
)
]
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
# --- Calcul nombre d'inscrits pour détermnier si évaluation "complete":
if evaluations:
# --- 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 evaluations[0].moduleimpl.inscriptions
}.intersection(etudids)
nb_inscrits_module = len(inscrits_module)
else:
nb_inscrits_module = 0
# empty df with all students:
evals_notes = pd.DataFrame(index=etudids, dtype=float)
evaluations_completes = []
for evaluation in evaluations:
inscrits_module = {ins.etud.id for ins in moduleimpl.inscriptions}.intersection(
moduleimpl.formsemestre.etudids_actifs
)
self.nb_inscrits_module = len(inscrits_module)
# dataFrame vide, index = tous les inscrits au SEMESTRE
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
self.evaluations_completes = []
self.evaluations_completes_dict = {}
self.en_attente = False
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 évaluation déclarée "à prise en compte immédiate"
# Les évaluations de rattrapage et 2eme session sont toujours incomplètes
# car on calcule leur moyenne à part.
is_complete = (evaluation.evaluation_type == scu.EVALUATION_NORMALE) and (
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
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge ne garde que les étudiants inscrits au module
# et met à NULL les notes non présentes
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
)
# Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.)
nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE)
self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
)
if nb_att > 0:
self.en_attente = True
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
[int(x) for x in evals_notes.columns], dtype="int"
)
self.evals_notes = evals_notes
def _load_evaluation_notes(self, evaluation: Evaluation) -> pd.DataFrame:
"""Charge les notes de l'évaluation
Resultat: dataframe, index: etudid ayant une note, valeur: note brute.
"""
eval_df = pd.read_sql_query(
"""SELECT n.etudid, n.value AS "%(evaluation_id)s"
FROM notes_notes n, notes_moduleimpl_inscription i
WHERE evaluation_id=%(evaluation_id)s
AND n.etudid = i.etudid
AND i.moduleimpl_id = %(moduleimpl_id)s
ORDER BY n.etudid
""",
db.engine,
params={
@ -158,90 +190,306 @@ def df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
index_col="etudid",
)
eval_df[str(evaluation.id)] = pd.to_numeric(eval_df[str(evaluation.id)])
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
is_complete = (
len(set(eval_df.index).intersection(etudids)) == nb_inscrits_module
) or evaluation.publish_incomplete
evaluations_completes.append(is_complete)
# NULL en base => ABS (= -999)
eval_df.fillna(scu.NOTES_ABSENCE, inplace=True)
# Ce merge met à NULL les élements non présents
# (notes non saisies ou etuds non inscrits au module):
evals_notes = evals_notes.merge(
eval_df, how="left", left_index=True, right_index=True
return eval_df
def _etudids(self):
"""L'index du dataframe est la liste de tous les étudiants inscrits au semestre"""
return [
inscr.etudid
for inscr in ModuleImpl.query.get(
self.moduleimpl_id
).formsemestre.inscriptions
]
def get_evaluations_coefs(self, moduleimpl: ModuleImpl) -> np.array:
"""Coefficients des évaluations, met à zéro ceux des évals incomplètes.
Résultat: 2d-array of floats, shape (nb_evals, 1)
"""
return (
np.array(
[e.coefficient for e in moduleimpl.evaluations],
dtype=float,
)
* self.evaluations_completes
).reshape(-1, 1)
# was _list_notes_evals_titles
def get_evaluations_completes(self, moduleimpl: ModuleImpl) -> list:
"Liste des évaluations complètes"
return [
e for e in moduleimpl.evaluations if self.evaluations_completes_dict[e.id]
]
def get_eval_notes_sur_20(self, moduleimpl: ModuleImpl) -> np.array:
"""Les notes des évaluations,
remplace les ATT, EXC, ABS, NaN par zéro et mets les notes sur 20.
Résultat: 2d array of floats, shape nb_etuds x nb_evaluations
"""
return np.where(
self.evals_notes.values > scu.NOTES_ABSENCE, self.evals_notes.values, 0.0
) / [e.note_max / 20.0 for e in moduleimpl.evaluations]
def get_evaluation_rattrapage(self, moduleimpl: ModuleImpl):
"""L'évaluation de rattrapage de ce module, ou None s'il n'en a pas.
Rattrapage: la moyenne du module est la meilleure note entre moyenne
des autres évals et la note eval rattrapage.
"""
eval_list = [
e
for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_RATTRAPAGE
]
if eval_list:
return eval_list[0]
return None
def get_evaluation_session2(self, moduleimpl: ModuleImpl):
"""L'évaluation de deuxième session de ce module, ou None s'il n'en a pas.
Session 2: remplace la note de moyenne des autres évals.
"""
eval_list = [
e
for e in moduleimpl.evaluations
if e.evaluation_type == scu.EVALUATION_SESSION2
]
if eval_list:
return eval_list[0]
return None
class ModuleImplResultsAPC(ModuleImplResults):
"Calcul des moyennes de modules à la mode BUT"
def compute_module_moy(
self,
evals_poids_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
Argument: evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef vers cette UE.
"""
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
nb_etuds, nb_evals = self.evals_notes.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
if nb_ues == 0:
return pd.DataFrame(index=self.evals_notes.index, columns=[])
evals_coefs = self.get_evaluations_coefs(modimpl)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
# Les poids des évals pour chaque étudiant: là où il a des notes
# non neutralisées
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
# Note: les NaN sont remplacés par des 0 dans evals_notes
# et dans dans evals_poids_etuds
# (rappel: la comparaison est toujours false face à un NaN)
# shape: (nb_etuds, nb_evals, nb_ues)
poids_stacked = np.stack([evals_poids] * nb_etuds)
evals_poids_etuds = np.where(
np.stack([self.evals_notes.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked,
0,
)
# Force columns names to integers (evaluation ids)
evals_notes.columns = pd.Int64Index(
[int(x) for x in evals_notes.columns], dtype="int64"
)
return evals_notes, evaluations, evaluations_completes
# Calcule la moyenne pondérée sur les notes disponibles:
evals_notes_stacked = np.stack([evals_notes_20] * nb_ues, axis=2)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, axis=1)
# Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2:
notes_session2 = self.evals_notes[eval_session2.id].values
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
etuds_moy_module = np.where(
etuds_use_session2[:, np.newaxis],
np.tile(
(notes_session2 / (eval_session2.note_max / 20.0))[:, np.newaxis],
nb_ues,
),
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
else:
# Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl)
if eval_rat:
notes_rat = self.evals_notes[eval_rat.id].values
# remplace les notes invalides (ATT, EXC...) par des NaN
notes_rat = np.where(
notes_rat > scu.NOTES_ABSENCE,
notes_rat / (eval_rat.note_max / 20.0),
np.nan,
)
# prend le max
etuds_use_rattrapage = notes_rat > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage[:, np.newaxis],
np.tile(notes_rat[:, np.newaxis], nb_ues),
etuds_moy_module,
)
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
)
self.etuds_moy_module = pd.DataFrame(
etuds_moy_module,
index=self.evals_notes.index,
columns=evals_poids_df.columns,
)
return self.etuds_moy_module
def compute_module_moy(
evals_notes_df: pd.DataFrame,
evals_poids_df: pd.DataFrame,
evaluations: list,
evaluations_completes: list,
) -> pd.DataFrame:
"""Calcule les moyennes des étudiants dans ce module
- evals_notes : DataFrame, colonnes: EVALS, Lignes: etudid
valeur: notes brutes, float ou NOTES_ATTENTE, NOTES_NEUTRALISE,
NOTES_ABSENCE.
Les NaN désignent les notes manquantes (non saisies).
- evals_poids: DataFrame, colonnes: UEs, Lignes: EVALs
- evaluations: séquence d'évaluations (utilisées pour le coef et
le barème)
- evaluations_completes: séquence de booléens indiquant les
évals à prendre en compte.
Résultat: DataFrame, colonnes UE, lignes etud
= la note de l'étudiant dans chaque UE pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant à des notes)
ne donnent pas de coef vers cette UE.
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: 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)
"""
nb_etuds, nb_evals = evals_notes_df.shape
nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids
if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
evals_coefs = (
np.array(
[e.coefficient for e in evaluations],
dtype=float,
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
evaluations = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id).all()
ues = modimpl.formsemestre.query_ues(with_sport=False).all()
ue_ids = [ue.id for ue in ues]
evaluation_ids = [evaluation.id for evaluation in evaluations]
evals_poids = pd.DataFrame(columns=ue_ids, index=evaluation_ids, dtype=float)
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id):
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
or modimpl.module.module_type == ModuleType.MALUS
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
def moduleimpl_is_conforme(
moduleimpl, evals_poids: pd.DataFrame, modules_coefficients: pd.DataFrame
) -> bool:
"""Vérifie que les évaluations de ce moduleimpl sont bien conformes
au PN.
Un module est dit *conforme* si et seulement si la somme des poids de ses
évaluations vers une UE de coefficient non nul est non nulle.
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:
return True # modules vides conformes
if nb_ues == 0:
return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues:
raise ValueError("moduleimpl_is_conforme: nb ue incoherent")
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all(
(modules_coefficients[moduleimpl.module_id].to_numpy() != 0)
== module_evals_poids
)
return check
class ModuleImplResultsClassic(ModuleImplResults):
"Calcul des moyennes de modules des formations classiques"
def compute_module_moy(self) -> pd.Series:
"""Calcule les moyennes des étudiants dans ce module
Résultat: Series, lignes etud
= la note (moyenne) de l'étudiant pour ce module.
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
ne donnent pas de coef.
"""
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
nb_etuds, nb_evals = self.evals_notes.shape
if nb_etuds == 0:
return pd.Series()
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
assert evals_coefs.shape == (nb_evals,)
evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
# Les coefs des évals pour chaque étudiant: là où il a des notes
# non neutralisées
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
# Note: les NaN sont remplacés par des 0 dans evals_notes
# et dans dans evals_poids_etuds
# (rappel: la comparaison est toujours False face à un NaN)
# shape: (nb_etuds, nb_evals)
coefs_stacked = np.stack([evals_coefs] * nb_etuds)
evals_coefs_etuds = np.where(
self.evals_notes.values > scu.NOTES_NEUTRALISE, coefs_stacked, 0
)
* evaluations_completes
).reshape(-1, 1)
evals_poids = evals_poids_df.values * evals_coefs
# -> evals_poids shape : (nb_evals, nb_ues)
assert evals_poids.shape == (nb_evals, nb_ues)
# Remplace les notes ATT, EXC, ABS, NaN par zéro et mets les notes sur 20:
evals_notes = np.where(
evals_notes_df.values > scu.NOTES_ABSENCE, evals_notes_df.values, 0.0
) / [e.note_max / 20.0 for e in evaluations]
# Les poids des évals pour les étudiant: là où il a des notes non neutralisées
# (ABS n'est pas neutralisée, mais ATTENTE et NEUTRALISE oui)
# Note: les NaN sont remplacés par des 0 dans evals_notes
# et dans dans evals_poids_etuds
# (rappel: la comparaison est toujours false face à un NaN)
# shape: (nb_etuds, nb_evals, nb_ues)
poids_stacked = np.stack([evals_poids] * nb_etuds)
evals_poids_etuds = np.where(
np.stack([evals_notes_df.values] * nb_ues, axis=2) > scu.NOTES_NEUTRALISE,
poids_stacked,
0,
)
# Calcule la moyenne pondérée sur les notes disponibles:
evals_notes_stacked = np.stack([evals_notes] * nb_ues, axis=2)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_poids_etuds * evals_notes_stacked, axis=1
) / np.sum(evals_poids_etuds, axis=1)
etuds_moy_module_df = pd.DataFrame(
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
)
return etuds_moy_module_df
# Calcule la moyenne pondérée sur les notes disponibles:
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etuds_moy_module = np.sum(
evals_coefs_etuds * evals_notes_20, axis=1
) / np.sum(evals_coefs_etuds, axis=1)
# Session2 : quand elle existe, remplace la note de module
eval_session2 = self.get_evaluation_session2(modimpl)
if eval_session2:
notes_session2 = self.evals_notes[eval_session2.id].values
# n'utilise que les notes valides (pas ATT, EXC, ABS, NaN)
etuds_use_session2 = notes_session2 > scu.NOTES_ABSENCE
etuds_moy_module = np.where(
etuds_use_session2,
notes_session2 / (eval_session2.note_max / 20.0),
etuds_moy_module,
)
self.etuds_use_session2 = pd.Series(
etuds_use_session2, index=self.evals_notes.index
)
else:
# Rattrapage: remplace la note de module ssi elle est supérieure
eval_rat = self.get_evaluation_rattrapage(modimpl)
if eval_rat:
notes_rat = self.evals_notes[eval_rat.id].values
# remplace les notes invalides (ATT, EXC...) par des NaN
notes_rat = np.where(
notes_rat > scu.NOTES_ABSENCE,
notes_rat / (eval_rat.note_max / 20.0),
np.nan,
)
# prend le max
etuds_use_rattrapage = notes_rat > etuds_moy_module
etuds_moy_module = np.where(
etuds_use_rattrapage, notes_rat, etuds_moy_module
)
self.etuds_use_rattrapage = pd.Series(
etuds_use_rattrapage, index=self.evals_notes.index
)
self.etuds_moy_module = pd.Series(
etuds_moy_module,
index=self.evals_notes.index,
)
return self.etuds_moy_module

View File

@ -31,12 +31,14 @@ import numpy as np
import pandas as pd
def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df):
"""Calcule la moyenne générale indicative
def compute_sem_moys_apc(
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
= 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)
"""
@ -46,13 +48,15 @@ def compute_sem_moys(etud_moy_ue_df, modimpl_coefs_df):
return moy_gen
def comp_ranks_series(notes: pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur numérique)
en tenant compte des ex-aequos
Le resultat est: { etudid : rang } rang est une chaine decrivant le rang
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.
Result: Series { etudid : rang:str } rang est une chaine decrivant le rang.
"""
notes = notes.sort_values(ascending=False) # Serie, tri par ordre décroissant
rangs = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
rangs_str = pd.Series(index=notes.index, dtype=str) # le rang est une chaîne
rangs_int = pd.Series(index=notes.index, dtype=int) # le rang numérique pour tris
N = len(notes)
nb_ex = 0 # nb d'ex-aequo consécutifs en cours
notes_i = notes.iat
@ -64,6 +68,7 @@ def comp_ranks_series(notes: pd.Series):
next = None
val = notes_i[i]
if nb_ex:
rangs_int[etudid] = i + 1 - nb_ex
srang = "%d ex" % (i + 1 - nb_ex)
if val == next:
nb_ex += 1
@ -71,9 +76,11 @@ def comp_ranks_series(notes: pd.Series):
nb_ex = 0
else:
if val == next:
rangs_int[etudid] = i + 1 - nb_ex
srang = "%d ex" % (i + 1 - nb_ex)
nb_ex = 1
else:
rangs_int[etudid] = i + 1
srang = "%d" % (i + 1)
rangs[etudid] = srang
return rangs
rangs_str[etudid] = srang
return rangs_str, rangs_int

View File

@ -25,8 +25,9 @@
#
##############################################################################
"""Fonctions de calcul des moyennes d'UE
"""Fonctions de calcul des moyennes d'UE (classiques ou BUT)
"""
from re import X
import numpy as np
import pandas as pd
@ -36,17 +37,20 @@ 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
def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.DataFrame:
"""Charge les coefs des modules de la formation pour le semestre indiqué.
"""Charge les coefs APC des modules de la formation pour le semestre indiqué.
Ces coefs lient les modules à chaque UE.
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.
@ -56,8 +60,19 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
.order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
)
modules = Module.query.filter_by(formation_id=formation_id).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
modules = (
Module.query.filter_by(formation_id=formation_id)
.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
)
)
if semestre_idx is not None:
ues = ues.filter_by(semestre_idx=semestre_idx)
@ -76,9 +91,21 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
query = query.filter(UniteEns.semestre_idx == semestre_idx)
for mod_coef in query:
module_coefs_df[mod_coef.module_id][mod_coef.ue_id] = mod_coef.coef
if mod_coef.module_id in module_coefs_df:
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
@ -86,19 +113,19 @@ def df_load_module_coefs(formation_id: int, semestre_idx: int = None) -> pd.Data
def df_load_modimpl_coefs(
formsemestre: models.FormSemestre, ues=None, modimpls=None
) -> pd.DataFrame:
"""Charge les coefs des modules du formsemestre indiqué.
"""Charge les coefs APC des modules du formsemestre indiqué.
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)
@ -109,8 +136,25 @@ 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)
try:
modimpl_coefs_df[mod2impl[mod_coef.module_id]][
mod_coef.ue_id
] = mod_coef.coef
except IndexError:
# il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation
pass
# 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
@ -124,38 +168,36 @@ 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)
def notes_sem_load_cube(formsemestre):
"""Calcule le cube des notes du semestre
(charge toutes les notes, calcule les moyenne des modules
et assemble le cube)
Resultat:
def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
"""Construit le "cube" (tenseur) des notes du semestre.
Charge toutes les notes (sql), calcule les moyennes des modules
et assemble le cube.
etuds: tous les inscrits au semestre (avec dem. et def.)
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.
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_evals_notes dict { modimpl.id : evals_notes }
modimpls_evaluations dict { modimpl.id : liste des évaluations }
modimpls_evaluations_complete: {modimpl_id : liste de booleens (complete/non)}
modimpls_results dict { modimpl.id : ModuleImplResultsAPC }
"""
modimpls_results = {}
modimpls_evals_poids = {}
modimpls_evals_notes = {}
modimpls_evaluations = {}
modimpls_evaluations_complete = {}
modimpls_notes = []
for modimpl in formsemestre.modimpls:
evals_notes, evaluations, evaluations_completes = moy_mod.df_load_modimpl_notes(
modimpl.id
)
evals_poids, ues = moy_mod.df_load_evaluations_poids(modimpl.id)
etuds_moy_module = moy_mod.compute_module_moy(
evals_notes, evals_poids, evaluations, evaluations_completes
)
modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_evals_notes[modimpl.id] = evals_notes
modimpls_evaluations[modimpl.id] = evaluations
modimpls_evaluations_complete[modimpl.id] = evaluations_completes
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)
modimpls_results[modimpl.id] = mod_results
modimpls_notes.append(etuds_moy_module)
if len(modimpls_notes):
cube = notes_sem_assemble_cube(modimpls_notes)
@ -165,53 +207,57 @@ def notes_sem_load_cube(formsemestre):
return (
cube,
modimpls_evals_poids,
modimpls_evals_notes,
modimpls_evaluations,
modimpls_evaluations_complete,
modimpls_results,
)
def compute_ue_moys(
def compute_ue_moys_apc(
sem_cube: np.array,
etuds: list,
modimpls: list,
ues: list,
modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame,
modimpl_mask: np.array,
) -> pd.DataFrame:
"""Calcul de la moyenne d'UE
"""Calcul de la moyenne d'UE en mode APC (BUT).
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
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
ERR erreur dans une formule utilisateurs (pas gérées ici).
sem_cube: notes moyennes aux modules
ndarray (etuds x modimpls x UEs)
(floats avec des NaN)
etuds : lites des étudiants (dim. 0 du cube)
modimpls : liste des modules à considérer (dim. 1 du cube)
etuds : liste des étudiants (dim. 0 du cube)
modimpls : liste des module_impl (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube)
module_inscr_df: matrice d'inscription du semestre (etud x modimpl)
module_coefs_df: matrice coefficients (UE x modimpl)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
(utilisé pour éliminer les bonus, et pourra servir à cacluler
sur des sous-ensembles de modules)
Resultat: DataFrame columns UE, rows etudid
Résultat: DataFrame columns UE (sans bonus), 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:
if nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 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)
# Met à zéro tous les coefs des modules non sélectionnés dans le masque:
modimpl_coefs = np.where(modimpl_mask, modimpl_coefs_df.values, 0.0)
# 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)
@ -225,12 +271,180 @@ def compute_ue_moys(
)
# Annule les coefs des modules NaN
modimpl_coefs_etuds_no_nan = np.where(np.isnan(sem_cube), 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)
#
# Version vectorisée
#
etud_moy_ue = np.sum(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = np.sum(
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
)
def compute_ue_moys_classic(
formsemestre: FormSemestre,
sem_matrix: np.array,
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
NI non inscrit à (au moins un) module de cette UE
NA pas de notes disponibles
ERR erreur dans une formule utilisateur. [XXX pas encore gérées ici]
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 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
- moyennes d'UE: DataFrame columns UE, rows etudid
- coefficients d'UE: DataFrame, columns UE, rows etudid
les coefficients effectifs de chaque UE pour chaque étudiant
(sommes de coefs de modules pris en compte)
"""
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
),
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
pd.DataFrame(columns=[ue.id for ue in ues], 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
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:
# 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)
# --------------------- Calcul des moyennes d'UE
ue_modules = np.array(
[[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
)
# 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)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_ue = (
np.sum(coefs * sem_matrix_inscrits, axis=2) / np.sum(coefs, axis=2)
).T
etud_moy_ue_df = pd.DataFrame(
etud_moy_ue, index=modimpl_inscr_df.index, 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
def compute_malus(
formsemestre: FormSemestre,
sem_modimpl_moys: np.array,
ues: list[UniteEns],
modimpl_inscr_df: pd.DataFrame,
) -> pd.DataFrame:
"""Calcul le malus sur les UE
Dans chaque UE, on peut avoir un ou plusieurs modules de MALUS.
Leurs notes sont positives ou négatives.
La somme des notes de malus somme est _soustraite_ à la moyenne de chaque UE.
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)
Résultat: DataFrame de float, index etudid, columns: ue.id (sans NaN)
"""
ues_idx = [ue.id for ue in ues]
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
for ue in ues:
if ue.type != UE_SPORT:
modimpl_mask = np.array(
[
(m.module.module_type == ModuleType.MALUS)
and (m.module.ue.id == ue.id)
for m in formsemestre.modimpls_sorted
]
)
if len(modimpl_mask):
malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1)
malus[ue.id] = malus_moys
malus.fillna(0.0, inplace=True)
return malus

134
app/comp/res_but.py Normal file
View File

@ -0,0 +1,134 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Résultats semestres BUT
"""
import time
import numpy as np
import pandas as pd
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.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
class ResultatsSemestreBUT(NotesTableCompat):
"""Résultats BUT: organisation des calculs"""
_cached_attrs = NotesTableCompat._cached_attrs + (
"modimpl_coefs_df",
"modimpls_evals_poids",
"sem_cube",
)
def __init__(self, formsemestre):
super().__init__(formsemestre)
if not self.load_cached():
t0 = time.time()
self.compute()
t1 = time.time()
self.store()
t2 = time.time()
log(
f"ResultatsSemestreBUT: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
)
def compute(self):
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
(
self.sem_cube,
self.modimpls_evals_poids,
self.modimpls_results,
) = 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, 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)
# Masque de tous les modules _sauf_ les bonus (sport)
modimpls_mask = [
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
]
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube,
self.etuds,
self.formsemestre.modimpls_sorted,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs_df,
modimpls_mask,
)
# 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
)
# --- Modules de MALUS sur les UEs
self.malus = moy_ue.compute_malus(
self.formsemestre, self.sem_cube, self.ues, self.modimpl_inscr_df
)
self.etud_moy_ue -= self.malus
# --- Bonus Sport & Culture
if not all(modimpls_mask): # au moins un module bonus
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 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
)
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements:
self.compute_rangs()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
En APC, il s'agit d'une moyenne indicative sans valeur.
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
"""
mod_idx = self.modimpl_coefs_df.columns.get_loc(moduleimpl_id)
etud_idx = self.etud_index[etudid]
# moyenne sur les UE:
if len(self.sem_cube[etud_idx, mod_idx]):
return np.nanmean(self.sem_cube[etud_idx, mod_idx])
return np.nan
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
moyenne générale.
En BUT, c'est simple: Coef = somme des coefs des modules vers cette UE.
(ne dépend pas des modules auxquels est inscrit l'étudiant, ).
"""
return self.modimpl_coefs_df.loc[ue.id].sum()

34
app/comp/res_cache.py Normal file
View File

@ -0,0 +1,34 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Cache pour résultats (super classe)
"""
from app.models import FormSemestre
class ResultatsCache:
_cached_attrs = () # virtual
def __init__(self, formsemestre: FormSemestre, cache_class=None):
self.formsemestre: FormSemestre = formsemestre
self.cache_class = cache_class
def load_cached(self) -> bool:
"Load cached dataframes, returns False si pas en cache"
data = self.cache_class.get(self.formsemestre.id)
if not data:
return False
for attr in self._cached_attrs:
setattr(self, attr, data[attr])
return True
def store(self):
"Cache our data"
self.cache_class.set(
self.formsemestre.id,
{attr: getattr(self, attr) for attr in self._cached_attrs},
)

248
app/comp/res_classic.py Normal file
View File

@ -0,0 +1,248 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Résultats semestres classiques (non APC)
"""
import time
import numpy as np
import pandas as pd
from sqlalchemy.sql import text
from flask import g, url_for
from app import db
from app import log
from app.comp import moy_mod, moy_ue, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import ModuleType
class ResultatsSemestreClassic(NotesTableCompat):
"""Résultats du semestre (formation classique): organisation des calculs."""
_cached_attrs = NotesTableCompat._cached_attrs + (
"modimpl_coefs",
"modimpl_idx",
"sem_matrix",
)
def __init__(self, formsemestre):
super().__init__(formsemestre)
if not self.load_cached():
t0 = time.time()
self.compute()
t1 = time.time()
self.store()
t2 = time.time()
log(
f"ResultatsSemestreClassic: cached formsemestre_id={formsemestre.id} ({(t1-t0):g}s +{(t2-t1):g}s)"
)
# recalculé (aussi rapide que de les cacher)
self.moy_min = self.etud_moy_gen.min()
self.moy_max = self.etud_moy_gen.max()
self.moy_moy = self.etud_moy_gen.mean()
def compute(self):
"Charge les notes et inscriptions et calcule les moyennes d'UE et gen."
self.sem_matrix, self.modimpls_results = notes_sem_load_matrix(
self.formsemestre
)
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_sorted]
)
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,
self.etud_coef_ue_df,
) = moy_ue.compute_ue_moys_classic(
self.formsemestre,
self.sem_matrix,
self.ues,
self.modimpl_inscr_df,
self.modimpl_coefs,
modimpl_standards_mask,
)
# --- Modules de MALUS sur les UEs et la moyenne générale
self.malus = moy_ue.compute_malus(
self.formsemestre, self.sem_matrix, self.ues, self.modimpl_inscr_df
)
self.etud_moy_ue -= self.malus
# ajuste la moyenne générale (à l'aide des coefs d'UE)
self.etud_moy_gen -= (self.etud_coef_ue_df * self.malus).sum(
axis=1
) / self.etud_coef_ue_df.sum(axis=1)
# --- 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)
# compat nt, utilisé pour l'afficher sur les bulletins:
self.bonus = bonus_mg
# --- UE capitalisées
self.apply_capitalisation()
# --- Classements:
self.compute_rangs()
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl
Result: valeur float (peut être NaN) ou chaîne "NI" (non inscrit ou DEM)
"""
return self.modimpls_results[moduleimpl_id].etuds_moy_module.get(etudid, "NI")
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl"""
notes_series: pd.Series = self.modimpls_results[moduleimpl_id].etuds_moy_module
nb_notes = len(notes_series)
if not nb_notes:
super().get_mod_stats(moduleimpl_id)
return {
# Series: Statistical methods from ndarray have been overridden to automatically
# exclude missing data (currently represented as NaN)
"moy": notes_series.mean(), # donc sans prendre en compte les NaN
"max": notes_series.max(),
"min": notes_series.min(),
"nb_notes": nb_notes,
"nb_missing": sum(notes_series.isna()),
"nb_valid_evals": sum(
self.modimpls_results[moduleimpl_id].evaluations_completes
),
}
def compute_etud_ue_coef(self, etudid: int, ue: UniteEns) -> float:
"""Détermine le coefficient de l'UE pour cet étudiant.
N'est utilisé que pour l'injection des UE capitalisées dans la
moyenne générale.
Coef = somme des coefs des modules de l'UE auxquels il est inscrit
"""
c = comp_etud_sum_coef_modules_ue(self.formsemestre.id, etudid, ue["ue_id"])
if c is not None: # inscrit à au moins un module de cette UE
return c
# arfff: aucun moyen de déterminer le coefficient de façon sûre
log(
"* oups: calcul coef UE impossible\nformsemestre_id='%s'\netudid='%s'\nue=%s"
% (self.formsemestre.id, etudid, ue)
)
etud: Identite = Identite.query.get(etudid)
raise ScoValueError(
"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée %s impossible à déterminer
pour l'étudiant <a href="%s" class="discretelink">%s</a></p>
<p>Il faut <a href="%s">saisir le coefficient de cette UE avant de continuer</a></p>
</div>
"""
% (
ue.acronyme,
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
etud.nom_disp(),
url_for(
"notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre.id,
err_ue_id=ue["ue_id"],
),
)
)
return 0.0 # ?
def notes_sem_load_matrix(formsemestre: FormSemestre) -> tuple[np.ndarray, dict]:
"""Calcule la matrice des notes du semestre
(charge toutes les notes, calcule les moyennes des modules
et assemble la matrice)
Resultat:
sem_matrix : 2d-array (etuds x modimpls)
modimpls_results dict { modimpl.id : ModuleImplResultsClassic }
"""
modimpls_results = {}
modimpls_notes = []
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
modimpls_notes.append(etuds_moy_module)
return (
notes_sem_assemble_matrix(modimpls_notes),
modimpls_results,
)
def notes_sem_assemble_matrix(modimpls_notes: list[pd.Series]) -> np.ndarray:
"""Réuni les notes moyennes des modules du semestre en une matrice
modimpls_notes : liste des moyennes de module
(Series rendus par compute_module_moy, index: etud)
Resultat: ndarray (etud x module)
"""
if not len(modimpls_notes):
return np.zeros((0, 0), dtype=float)
modimpls_notes_arr = [s.values for s in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud) à (etud x mod)
return modimpls_notes.T
def comp_etud_sum_coef_modules_ue(formsemestre_id, etudid, ue_id):
"""Somme des coefficients des modules de l'UE dans lesquels cet étudiant est inscrit
ou None s'il n'y a aucun module.
"""
# comme l'ancien notes_table.comp_etud_sum_coef_modules_ue
# mais en raw sqlalchemy et la somme en SQL
sql = text(
"""
SELECT sum(mod.coefficient)
FROM notes_modules mod, notes_moduleimpl mi, notes_moduleimpl_inscription ins
WHERE mod.id = mi.module_id
and ins.etudid = :etudid
and ins.moduleimpl_id = mi.id
and mi.formsemestre_id = :formsemestre_id
and mod.ue_id = :ue_id
"""
)
cursor = db.session.execute(
sql, {"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id}
)
r = cursor.fetchone()
if r is None:
return None
return r[0]

713
app/comp/res_common.py Normal file
View File

@ -0,0 +1,713 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
from collections import Counter
from functools import cached_property
import numpy as np
import pandas as pd
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.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
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
# ce sont les attributs listés dans `_cached_attrs`
# le stockage et l'invalidation sont gérés dans sco_cache.py
#
# - les valeurs cachées durant le temps d'une requête
# (durée de vie de l'instance de ResultatsSemestre)
# qui sont notamment les attributs décorés par `@cached_property``
#
class ResultatsSemestre(ResultatsCache):
_cached_attrs = (
"etud_moy_gen_ranks",
"etud_moy_gen",
"etud_moy_ue",
"modimpl_inscr_df",
"modimpls_results",
"etud_coef_ue_df",
)
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre, ResultatsSemestreCache)
# BUT ou standard ? (apc == "approche par compétences")
self.is_apc = formsemestre.formation.is_apc()
# Attributs "virtuels", définis dans les sous-classes
# ResultatsSemestreBUT ou ResultatsSemestreClassic
self.etud_moy_ue = {}
"etud_moy_ue: DataFrame columns UE, rows etudid"
self.etud_moy_gen = {}
self.etud_moy_gen_ranks = {}
self.etud_moy_gen_ranks_int = {}
self.modimpls_results: ModuleImplResults = None
"Résultats de chaque modimpl: dict { modimpl.id : ModuleImplResults(Classique ou BUT) }"
self.etud_coef_ue_df = None
"""coefs d'UE effectifs pour chaque étudiant (pour form. classiques)"""
self.validations = None
def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes"
# voir ce qui est chargé / calculé ici et dans les sous-classes
raise NotImplementedError()
def get_inscriptions_counts(self) -> Counter:
"""Nombre d'inscrits, défaillants, démissionnaires.
Exemple: res.get_inscriptions_counts()[scu.INSCRIT]
Result: a collections.Counter instance
"""
return Counter(ins.etat for ins in self.formsemestre.inscriptions)
@cached_property
def etuds(self) -> list[Identite]:
"Liste des inscrits au semestre, avec les démissionnaires et les défaillants"
# nb: si la liste des inscrits change, ResultatsSemestre devient invalide
return self.formsemestre.get_inscrits(include_demdef=True)
@cached_property
def etud_index(self) -> dict[int, int]:
"dict { etudid : indice dans les inscrits }"
return {e.id: idx for idx, e in enumerate(self.etuds)}
@cached_property
def etuds_dict(self) -> dict[int, Identite]:
"""dict { etudid : Identite } inscrits au semestre,
avec les démissionnaires et defs."""
return {etud.id: etud for etud in self.etuds}
@cached_property
def ues(self) -> list[UniteEns]:
"""Liste des UEs du semestre (avec les UE bonus sport)
(indices des DataFrames).
Note: un étudiant n'est pas nécessairement inscrit dans toutes ces UEs.
"""
return self.formsemestre.query_ues(with_sport=True).all()
@cached_property
def ressources(self):
"Liste des ressources du semestre, triées par numéro de module"
return [
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.formsemestre.modimpls_sorted
if m.module.module_type == scu.ModuleType.SAE
]
def get_etud_ue_validables(self, etudid: int) -> list[UniteEns]:
"""Liste des UEs du semestre qui doivent être validées
Rappel: l'étudiant est inscrit à des modimpls et non à des UEs.
- En BUT: on considère que l'étudiant va (ou non) valider toutes les UEs des modules
du parcours. XXX notion à implémenter, pour l'instant toutes les UE du semestre.
- En classique: toutes les UEs des modimpls auxquels l'étufdiant est inscrit sont
susceptibles d'être validées.
Les UE "bonus" (sport) ne sont jamais "validables".
"""
if self.is_apc:
# TODO: introduire la notion de parcours (#sco93)
return self.formsemestre.query_ues().filter(UniteEns.type != UE_SPORT).all()
else:
# restreint aux UE auxquelles l'étudiant est inscrit (dans l'un des modimpls)
ues = {
modimpl.module.ue
for modimpl in self.formsemestre.modimpls_sorted
if self.modimpl_inscr_df[modimpl.id][etudid]
}
ues = sorted(list(ues), key=lambda x: x.numero or 0)
return ues
def modimpls_in_ue(self, ue_id, etudid) -> list[ModuleImpl]:
"""Liste des modimpl de cette 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
Par exemple, pour avoir le nombre d'UE au dessus de 10 pour l'étudiant etudid
nb_ues_ok = sum(res.ue_au_dessus().loc[etudid])
"""
return self.etud_moy_ue > (seuil - scu.NOTES_TOLERANCE)
def apply_capitalisation(self):
"""Recalcule la moyenne générale pour prendre en compte d'éventuelles
UE capitalisées.
"""
# Supposant qu'il y a peu d'UE capitalisées,
# on va soustraire la moyenne d'UE et ajouter celle de l'UE capitalisée.
# return # XXX XXX XXX
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees
ue_by_code = {}
for etudid in ue_capitalisees.index:
recompute_mg = False
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])
# for ue_code in ue_codes:
# ue = ue_by_code.get(ue_code)
# if ue is None:
# ue = self.formsemestre.query_ues.filter_by(ue_code=ue_code)
# ue_by_code[ue_code] = ue
# Quand il y a une capitalisation, vérifie toutes les UEs
sum_notes_ue = 0.0
sum_coefs_ue = 0.0
for ue in self.formsemestre.query_ues():
ue_cap = self.get_etud_ue_status(etudid, ue.id)
if ue_cap and ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
if recompute_mg and sum_coefs_ue > 0.0:
# On doit prendre en compte une ou plusieurs UE capitalisées
# et donc recalculer la moyenne générale
self.etud_moy_gen[etudid] = sum_notes_ue / sum_coefs_ue
def _get_etud_ue_cap(self, etudid, ue):
""""""
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:
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
else:
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
Result: dict, ou None si l'UE n'est pas dans ce semestre.
"""
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
if ue.type == UE_SPORT:
return {
"is_capitalized": False,
"was_capitalized": False,
"is_external": False,
"coef_ue": 0.0,
"cur_moy_ue": 0.0,
"moy": 0.0,
"event_date": None,
"ue": ue.to_dict(),
"formsemestre_id": None,
"capitalized_ue_id": None,
"ects_pot": 0.0,
}
if not ue_id in self.etud_moy_ue:
return None
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
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)
)
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"])
):
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_ue = self.etud_coef_ue_df[ue_id][etudid]
return {
"is_capitalized": is_capitalized,
"was_capitalized": was_capitalized,
"is_external": ue_cap["is_external"] if is_capitalized else ue.is_external,
"coef_ue": coef_ue,
"ects_pot": ue.ects or 0.0,
"cur_moy_ue": cur_moy_ue,
"moy": moy_ue,
"event_date": ue_cap["event_date"] if is_capitalized else None,
"ue": ue.to_dict(),
"formsemestre_id": ue_cap["formsemestre_id"] if is_capitalized else None,
"capitalized_ue_id": ue_cap["ue_id"] if is_capitalized else None,
}
def get_etud_ue_cap_coef(self, etudid, ue, ue_cap):
"""Calcule le coefficient d'une UE capitalisée, pour cet étudiant,
injectée dans le semestre courant.
ue : ue du semestre courant
ue_cap = resultat de formsemestre_get_etud_capitalisation
{ 'ue_id' (dans le semestre source),
'ue_code', 'moy', 'event_date','formsemestre_id' }
"""
# 1- Coefficient explicitement déclaré dans le semestre courant pour cette UE ?
ue_coef_db = FormSemestreUECoef.query.filter_by(
formsemestre_id=self.formsemestre.id, ue_id=ue.id
).first()
if ue_coef_db is not None:
return ue_coef_db.coefficient
# En APC: somme des coefs des modules vers cette UE
# En classique: Capitalisation UE externe: quel coef appliquer ?
# En ScoDoc 7, calculait la somme des coefs dans l'UE du semestre d'origine
# ici si l'étudiant est inscrit dans le semestre courant,
# somme des coefs des modules de l'UE auxquels il est inscrit
return self.compute_etud_ue_coef(etudid, ue)
# Pour raccorder le code des anciens codes qui attendent une NoteTable
class NotesTableCompat(ResultatsSemestre):
"""Implementation partielle de NotesTable WIP TODO
Les méthodes définies dans cette classe sont
pour conserver la compatibilité abvec les codes anciens et
il n'est pas recommandé de les utiliser dans de nouveaux
développements (API malcommode et peu efficace).
"""
_cached_attrs = ResultatsSemestre._cached_attrs + (
"bonus",
"bonus_ues",
"malus",
"etud_moy_gen_ranks",
"etud_moy_gen_ranks_int",
"ue_rangs",
)
def __init__(self, formsemestre: FormSemestre):
super().__init__(formsemestre)
nb_etuds = len(self.etuds)
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 = None # sera surchargé en Classic, mais pas en APC
self.moy_min = "NA"
self.moy_max = "NA"
self.moy_moy = "NA"
self.expr_diagnostics = ""
self.parcours = self.formsemestre.formation.get_parcours()
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
"""Liste des étudiants inscrits
order_by = False|'nom'|'moy' tri sur nom ou sur moyenne générale (indicative)
Note: pour récupérer les etudids des inscrits, non triés, il est plus efficace
d'utiliser `[ ins.etudid for ins in nt.formsemestre.inscriptions ]`
"""
etuds = self.formsemestre.get_inscrits(
include_demdef=include_demdef, order=(order_by == "nom")
)
if order_by == "moy":
etuds.sort(
key=lambda e: (
self.etud_moy_gen_ranks_int.get(e.id, 100000),
e.sort_key,
)
)
return etuds
def get_etudids(self) -> list[int]:
"""(deprecated)
Liste des etudids inscrits, incluant les démissionnaires.
triée par ordre alphabetique de NOM
(à éviter: renvoie les etudids, mais est moins efficace que get_inscrits)
"""
# Note: pour avoir les inscrits non triés,
# utiliser [ ins.etudid for ins in self.formsemestre.inscriptions ]
return [x["etudid"] for x in self.inscrlist]
@cached_property
def sem(self) -> dict:
"""le formsemestre, comme un gros et gras dict (nt.sem)"""
return self.formsemestre.get_infos_dict()
@cached_property
def inscrlist(self) -> list[dict]: # utilisé par PE
"""Liste des inscrits au semestre (avec DEM et DEF),
sous forme de dict etud,
classée dans l'ordre alphabétique de noms.
"""
etuds = self.formsemestre.get_inscrits(include_demdef=True, order=True)
return [e.to_dict_scodoc7() for e in etuds]
@cached_property
def stats_moy_gen(self):
"""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()
"""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):
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
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)
"""
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 compute_rangs(self):
"""Calcule les classements
Moyenne générale: etud_moy_gen_ranks
Par UE (sauf ue bonus)
"""
(
self.etud_moy_gen_ranks,
self.etud_moy_gen_ranks_int,
) = moy_sem.comp_ranks_series(self.etud_moy_gen)
for ue in self.formsemestre.query_ues():
moy_ue = self.etud_moy_ue[ue.id]
self.ue_rangs[ue.id] = (
moy_sem.comp_ranks_series(moy_ue)[0], # juste en chaine
int(moy_ue.count()),
)
# .count() -> nb of non NaN values
def get_etud_ue_rang(self, ue_id, etudid) -> tuple[str, int]:
"""Le rang de l'étudiant dans cette ue
Result: rang:str, effectif:str
"""
rangs, effectif = self.ue_rangs[ue_id]
if rangs is not None:
rang = rangs[etudid]
else:
return "", ""
return rang, effectif
def etud_check_conditions_ues(self, etudid):
"""Vrai si les conditions sur les UE sont remplies.
Ne considère que les UE ayant des notes (moyenne calculée).
(les UE sans notes ne sont pas comptées comme sous la barre)
Prend en compte les éventuelles UE capitalisées.
Pour les parcours habituels, cela revient à vérifier que
les moyennes d'UE sont toutes > à leur barre (sauf celles sans notes)
Pour les parcours non standards (LP2014), cela peut être plus compliqué.
Return: True|False, message explicatif
"""
ue_status_list = []
for ue in self.formsemestre.query_ues():
ue_status = self.get_etud_ue_status(etudid, ue.id)
if ue_status:
ue_status_list.append(ue_status)
return self.parcours.check_barre_ues(ue_status_list)
def all_etuds_have_sem_decisions(self):
"""True si tous les étudiants du semestre ont une décision de jury.
Ne regarde pas les décisions d'UE.
"""
for ins in self.formsemestre.inscriptions:
if ins.etat != scu.INSCRIT:
continue # skip démissionnaires
if self.get_etud_decision_sem(ins.etudid) is None:
return False
return True
def etud_has_decision(self, etudid):
"""True s'il y a une décision de jury pour cet étudiant"""
return self.get_etud_decision_ues(etudid) or self.get_etud_decision_sem(etudid)
def get_etud_decision_ues(self, etudid: int) -> dict:
"""Decisions du jury pour les UE de cet etudiant, ou None s'il n'y en pas eu.
Ne tient pas compte des UE capitalisées.
{ ue_id : { 'code' : ADM|CMP|AJ, 'event_date' : }
Ne renvoie aucune decision d'UE pour les défaillants
"""
if self.get_etud_etat(etudid) == DEF:
return {}
else:
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(
self.formsemestre
)
return self.validations.decisions_jury_ues.get(etudid, None)
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.
{ 'code' : None|ATT|..., 'assidu' : 0|1, 'event_date' : , compense_formsemestre_id }
Si état défaillant, force le code a DEF
"""
if self.get_etud_etat(etudid) == DEF:
return {
"code": DEF,
"assidu": False,
"event_date": "",
"compense_formsemestre_id": None,
}
else:
if not self.validations:
self.validations = res_sem.load_formsemestre_validations(
self.formsemestre
)
return self.validations.decisions_jury.get(etudid, None)
def get_etud_etat(self, etudid: int) -> str:
"Etat de l'etudiant: 'I', 'D', DEF ou '' (si pas connu dans ce semestre)"
ins = self.formsemestre.etuds_inscriptions.get(etudid, None)
if ins is None:
return ""
return ins.etat
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
En APC, il s'agira d'une moyenne indicative sans valeur.
Result: valeur float (peut être naN) ou chaîne "NI" (non inscrit ou DEM)
"""
raise NotImplementedError() # virtual method
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne générale de cet etudiant dans ce semestre.
Prend en compte les UE capitalisées.
Si apc, moyenne indicative.
Si pas de notes: 'NA'
"""
return self.etud_moy_gen[etudid]
def get_etud_ects_pot(self, etudid: int) -> dict:
"""
Un dict avec les champs
ects_pot : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
ects_pot_fond: (float) nb d'ECTS issus d'UE fondamentales (non électives)
Ce sont les ECTS des UE au dessus de la barre (10/20 en principe), avant le jury (donc non
encore enregistrées).
"""
# was nt.get_etud_moy_infos
# XXX pour compat nt, à remplacer ultérieurement
ues = self.get_etud_ue_validables(etudid)
ects_pot = 0.0
for ue in ues:
if (
ue.id in self.etud_moy_ue
and ue.ects is not None
and self.etud_moy_ue[ue.id][etudid] > self.parcours.NOTES_BARRE_VALID_UE
):
ects_pot += ue.ects
return {
"ects_pot": ects_pot,
"ects_pot_fond": 0.0, # not implemented (anciennemment pour école ingé)
}
def get_etud_rang(self, etudid: int):
return self.etud_moy_gen_ranks.get(etudid, 99999) # XXX
def get_etud_rang_group(self, etudid: int, group_id: int):
return (None, 0) # XXX unimplemented TODO
def get_evals_in_mod(self, moduleimpl_id: int) -> list[dict]:
"""Liste d'informations (compat NotesTable) sur évaluations completes
de ce module.
Évaluation "complete" ssi toutes notes saisies ou en attente.
"""
modimpl = ModuleImpl.query.get(moduleimpl_id)
modimpl_results = self.modimpls_results.get(moduleimpl_id)
if not modimpl_results:
return [] # safeguard
evals_results = []
for e in modimpl.evaluations:
if modimpl_results.evaluations_completes_dict.get(e.id, False):
d = e.to_dict()
d["heure_debut"] = e.heure_debut # datetime.time
d["heure_fin"] = e.heure_fin
d["jour"] = e.jour # datetime
d["notes"] = {
etud.id: {
"etudid": etud.id,
"value": modimpl_results.evals_notes[e.id][etud.id],
}
for etud in self.etuds
}
d["etat"] = {
"evalattente": modimpl_results.evaluations_etat[e.id].nb_attente,
}
evals_results.append(d)
elif e.id not in modimpl_results.evaluations_completes_dict:
# ne devrait pas arriver ? XXX
log(
f"Warning: 220213 get_evals_in_mod {e.id} not in mod {moduleimpl_id} ?"
)
return evals_results
def get_evaluations_etats(self):
"""[ {...evaluation et son etat...} ]"""
# TODO: à moderniser
from app.scodoc import sco_evaluations
if not hasattr(self, "_evaluations_etats"):
self._evaluations_etats = sco_evaluations.do_evaluation_list_in_sem(
self.formsemestre.id
)
return self._evaluations_etats
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
"""Liste des états des évaluations de ce module"""
# XXX TODO à moderniser: lent, recharge des donénes que l'on a déjà...
return [
e
for e in self.get_evaluations_etats()
if e["moduleimpl_id"] == moduleimpl_id
]
def get_moduleimpls_attente(self):
"""Liste des modimpls du semestre ayant des notes en attente"""
return [
modimpl
for modimpl in self.formsemestre.modimpls_sorted
if self.modimpls_results[modimpl.id].en_attente
]
def get_mod_stats(self, moduleimpl_id: int) -> dict:
"""Stats sur les notes obtenues dans un modimpl
Vide en APC
"""
return {
"moy": "-",
"max": "-",
"min": "-",
"nb_notes": "-",
"nb_missing": "-",
"nb_valid_evals": "-",
}
def get_nom_short(self, etudid):
"formatte nom d'un etud (pour table recap)"
etud = self.identdict[etudid]
return (
(etud["nom_usuel"] or etud["nom"]).upper()
+ " "
+ etud["prenom"].capitalize()[:2]
+ "."
)
@cached_property
def T(self):
return self.get_table_moyennes_triees()
def get_table_moyennes_triees(self) -> list:
"""Result: liste de tuples
moy_gen, moy_ue_0, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
"""
table_moyennes = []
etuds_inscriptions = self.formsemestre.etuds_inscriptions
ues = self.formsemestre.query_ues(with_sport=True) # avec bonus
for etudid in etuds_inscriptions:
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.formsemestre.modimpls_sorted)
)
else:
moy_ues = []
ue_is_cap = {}
for ue in ues:
ue_status = self.get_etud_ue_status(etudid, ue.id)
if ue_status:
moy_ues.append(ue_status["moy"])
ue_is_cap[ue.id] = ue_status["is_capitalized"]
else:
moy_ues.append("?")
t = [moy_gen] + list(moy_ues)
# Moyennes modules:
for modimpl in self.formsemestre.modimpls_sorted:
if ue_is_cap.get(modimpl.module.ue.id, False):
val = "-c-"
else:
val = self.get_etud_mod_moy(modimpl.id, etudid)
t.append(val)
t.append(etudid)
table_moyennes.append(t)
# tri par moyennes décroissantes,
# en laissant les démissionnaires à la fin, par ordre alphabetique
etuds = [ins.etud for ins in etuds_inscriptions.values()]
etuds.sort(key=lambda e: e.sort_key)
self._rang_alpha = {e.id: i for i, e in enumerate(etuds)}
table_moyennes.sort(key=self._row_key)
return table_moyennes
def _row_key(self, x):
"""clé de tri par moyennes décroissantes,
en laissant les demissionnaires à la fin, par ordre alphabetique.
(moy_gen, rang_alpha)
"""
try:
moy = -float(x[0])
except (ValueError, TypeError):
moy = 1000.0
return (moy, self._rang_alpha[x[-1]])
@cached_property
def identdict(self) -> dict:
"""{ etudid : etud_dict } pour tous les inscrits au semestre"""
return {
ins.etud.id: ins.etud.to_dict_scodoc7()
for ins in self.formsemestre.inscriptions
}

56
app/comp/res_sem.py Normal file
View File

@ -0,0 +1,56 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Chargement des résultats de semestres (tous types)
"""
from flask import g
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
def load_formsemestre_results(formsemestre: FormSemestre) -> ResultatsSemestre:
"""Returns ResultatsSemestre for this formsemestre.
Suivant le type de formation, retour une instance de
ResultatsSemestreClassic ou de ResultatsSemestreBUT.
Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it.
"""
# --- Try local cache (within the same request context)
if not hasattr(g, "formsemestre_results_cache"):
g.formsemestre_results_cache = {}
else:
if formsemestre.id in g.formsemestre_results_cache:
return g.formsemestre_results_cache[formsemestre.id]
klass = (
ResultatsSemestreBUT
if formsemestre.formation.is_apc()
else ResultatsSemestreClassic
)
g.formsemestre_results_cache[formsemestre.id] = klass(formsemestre)
return g.formsemestre_results_cache[formsemestre.id]
def load_formsemestre_validations(formsemestre: FormSemestre) -> ValidationsSemestre:
"""Charge les résultats de jury de ce semestre.
Search in local cache (g.formsemestre_result_cache)
If not in cache, build it and cache it.
"""
if not hasattr(g, "formsemestre_validation_cache"):
g.formsemestre_validations_cache = {} # pylint: disable=C0237
else:
if formsemestre.id in g.formsemestre_validations_cache:
return g.formsemestre_validations_cache[formsemestre.id]
g.formsemestre_validations_cache[formsemestre.id] = ValidationsSemestre(
formsemestre
)
return g.formsemestre_validations_cache[formsemestre.id]

View File

@ -0,0 +1,29 @@
"""entreprises.__init__
"""
from flask import Blueprint
from app.scodoc import sco_etud
from app.auth.models import User
bp = Blueprint("entreprises", __name__)
LOGS_LEN = 10
@bp.app_template_filter()
def format_prenom(s):
return sco_etud.format_prenom(s)
@bp.app_template_filter()
def format_nom(s):
return sco_etud.format_nom(s)
@bp.app_template_filter()
def get_nomcomplet(s):
user = User.query.filter_by(user_name=s).first()
return user.get_nomcomplet()
from app.entreprises import routes

289
app/entreprises/forms.py Normal file
View File

@ -0,0 +1,289 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
#
##############################################################################
import re
import requests
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed, FileRequired
from markupsafe import Markup
from sqlalchemy import text
from wtforms import StringField, SubmitField, TextAreaField, SelectField, HiddenField
from wtforms.fields import EmailField, DateField
from wtforms.validators import ValidationError, DataRequired, Email
from app.entreprises.models import Entreprise, EntrepriseContact
from app.models import Identite
from app.auth.models import User
CHAMP_REQUIS = "Ce champ est requis"
class EntrepriseCreationForm(FlaskForm):
siret = StringField(
"SIRET",
validators=[DataRequired(message=CHAMP_REQUIS)],
render_kw={"placeholder": "Numéro composé de 14 chiffres", "maxlength": "14"},
)
nom_entreprise = StringField(
"Nom de l'entreprise",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
adresse = StringField(
"Adresse de l'entreprise",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
codepostal = StringField(
"Code postal de l'entreprise",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
ville = StringField(
"Ville de l'entreprise",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
pays = StringField(
"Pays de l'entreprise",
validators=[DataRequired(message=CHAMP_REQUIS)],
render_kw={"style": "margin-bottom: 50px;"},
)
nom_contact = StringField(
"Nom du contact", validators=[DataRequired(message=CHAMP_REQUIS)]
)
prenom_contact = StringField(
"Prénom du contact",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
telephone = StringField(
"Téléphone du contact",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
mail = EmailField(
"Mail du contact",
validators=[
DataRequired(message=CHAMP_REQUIS),
Email(message="Adresse e-mail invalide"),
],
)
poste = StringField("Poste du contact", validators=[])
service = StringField("Service du contact", validators=[])
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
def validate_siret(self, siret):
siret = siret.data.strip()
if re.match("^\d{14}$", siret) == None:
raise ValidationError("Format incorrect")
req = requests.get(
f"https://entreprise.data.gouv.fr/api/sirene/v1/siret/{siret}"
)
if req.status_code != 200:
raise ValidationError("SIRET inexistant")
entreprise = Entreprise.query.filter_by(siret=siret).first()
if entreprise is not None:
lien = f'<a href="/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}">ici</a>'
raise ValidationError(
Markup(f"Entreprise déjà présent, lien vers la fiche : {lien}")
)
class EntrepriseModificationForm(FlaskForm):
siret = StringField("SIRET", validators=[], render_kw={"disabled": ""})
nom = StringField(
"Nom de l'entreprise",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
adresse = StringField("Adresse", validators=[DataRequired(message=CHAMP_REQUIS)])
codepostal = StringField(
"Code postal", validators=[DataRequired(message=CHAMP_REQUIS)]
)
ville = StringField("Ville", validators=[DataRequired(message=CHAMP_REQUIS)])
pays = StringField("Pays", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
class OffreCreationForm(FlaskForm):
intitule = StringField("Intitulé", validators=[DataRequired(message=CHAMP_REQUIS)])
description = TextAreaField(
"Description", validators=[DataRequired(message=CHAMP_REQUIS)]
)
type_offre = SelectField(
"Type de l'offre",
choices=[("Stage"), ("Alternance")],
validators=[DataRequired(message=CHAMP_REQUIS)],
)
missions = TextAreaField(
"Missions", validators=[DataRequired(message=CHAMP_REQUIS)]
)
duree = StringField("Durée", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
class OffreModificationForm(FlaskForm):
intitule = StringField("Intitulé", validators=[DataRequired(message=CHAMP_REQUIS)])
description = TextAreaField(
"Description", validators=[DataRequired(message=CHAMP_REQUIS)]
)
type_offre = SelectField(
"Type de l'offre",
choices=[("Stage"), ("Alternance")],
validators=[DataRequired(message=CHAMP_REQUIS)],
)
missions = TextAreaField(
"Missions", validators=[DataRequired(message=CHAMP_REQUIS)]
)
duree = StringField("Durée", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
class ContactCreationForm(FlaskForm):
hidden_entreprise_id = HiddenField()
nom = StringField("Nom", validators=[DataRequired(message=CHAMP_REQUIS)])
prenom = StringField("Prénom", validators=[DataRequired(message=CHAMP_REQUIS)])
telephone = StringField(
"Téléphone", validators=[DataRequired(message=CHAMP_REQUIS)]
)
mail = EmailField(
"Mail",
validators=[
DataRequired(message=CHAMP_REQUIS),
Email(message="Adresse e-mail invalide"),
],
)
poste = StringField("Poste", validators=[])
service = StringField("Service", validators=[])
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
def validate(self):
rv = FlaskForm.validate(self)
if not rv:
return False
contact = EntrepriseContact.query.filter_by(
entreprise_id=self.hidden_entreprise_id.data,
nom=self.nom.data,
prenom=self.prenom.data,
).first()
if contact is not None:
self.nom.errors.append("Ce contact existe déjà (même nom et prénom)")
self.prenom.errors.append("")
return False
return True
class ContactModificationForm(FlaskForm):
nom = StringField("Nom", validators=[DataRequired(message=CHAMP_REQUIS)])
prenom = StringField("Prénom", validators=[DataRequired(message=CHAMP_REQUIS)])
telephone = StringField(
"Téléphone", validators=[DataRequired(message=CHAMP_REQUIS)]
)
mail = EmailField(
"Mail",
validators=[
DataRequired(message=CHAMP_REQUIS),
Email(message="Adresse e-mail invalide"),
],
)
poste = StringField("Poste", validators=[])
service = StringField("Service", validators=[])
submit = SubmitField("Modifier", render_kw={"style": "margin-bottom: 10px;"})
class HistoriqueCreationForm(FlaskForm):
etudiant = StringField(
"Étudiant",
validators=[DataRequired(message=CHAMP_REQUIS)],
render_kw={"placeholder": "Tapez le nom de l'étudiant puis selectionnez"},
)
type_offre = SelectField(
"Type de l'offre",
choices=[("Stage"), ("Alternance")],
validators=[DataRequired(message=CHAMP_REQUIS)],
)
date_debut = DateField(
"Date début", validators=[DataRequired(message=CHAMP_REQUIS)]
)
date_fin = DateField("Date fin", validators=[DataRequired(message=CHAMP_REQUIS)])
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
def validate(self):
rv = FlaskForm.validate(self)
if not rv:
return False
if self.date_debut.data > self.date_fin.data:
self.date_debut.errors.append("Les dates sont incompatibles")
self.date_fin.errors.append("Les dates sont incompatibles")
return False
return True
def validate_etudiant(self, etudiant):
etudiant_data = etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm).params(nom_prenom=etudiant_data).first()
)
if etudiant is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
class EnvoiOffreForm(FlaskForm):
responsable = StringField(
"Responsable de formation",
validators=[DataRequired(message=CHAMP_REQUIS)],
)
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
def validate_responsable(self, responsable):
responsable_data = responsable.data.upper().strip()
stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data"
)
responsable = (
User.query.from_statement(stm)
.params(responsable_data=responsable_data)
.first()
)
if responsable is None:
raise ValidationError("Champ incorrect (selectionnez dans la liste)")
class AjoutFichierForm(FlaskForm):
fichier = FileField(
"Fichier",
validators=[
FileRequired(message=CHAMP_REQUIS),
FileAllowed(["pdf", "docx"], "Fichier .pdf ou .docx uniquement"),
],
)
submit = SubmitField("Envoyer", render_kw={"style": "margin-bottom: 10px;"})
class SuppressionConfirmationForm(FlaskForm):
submit = SubmitField("Supprimer", render_kw={"style": "margin-bottom: 10px;"})

137
app/entreprises/models.py Normal file
View File

@ -0,0 +1,137 @@
from app import db
class Entreprise(db.Model):
__tablename__ = "entreprises"
id = db.Column(db.Integer, primary_key=True)
siret = db.Column(db.Text)
nom = db.Column(db.Text)
adresse = db.Column(db.Text)
codepostal = db.Column(db.Text)
ville = db.Column(db.Text)
pays = db.Column(db.Text)
contacts = db.relationship(
"EntrepriseContact",
backref="entreprise",
lazy="dynamic",
cascade="all, delete-orphan",
)
offres = db.relationship(
"EntrepriseOffre",
backref="entreprise",
lazy="dynamic",
cascade="all, delete-orphan",
)
def to_dict(self):
return {
"siret": self.siret,
"nom": self.nom,
"adresse": self.adresse,
"codepostal": self.codepostal,
"ville": self.ville,
"pays": self.pays,
}
class EntrepriseContact(db.Model):
__tablename__ = "entreprise_contact"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
)
nom = db.Column(db.Text)
prenom = db.Column(db.Text)
telephone = db.Column(db.Text)
mail = db.Column(db.Text)
poste = db.Column(db.Text)
service = db.Column(db.Text)
def to_dict(self):
return {
"nom": self.nom,
"prenom": self.prenom,
"telephone": self.telephone,
"mail": self.mail,
"poste": self.poste,
"service": self.service,
}
def to_dict_export(self):
entreprise = Entreprise.query.get(self.entreprise_id)
return {
"nom": self.nom,
"prenom": self.prenom,
"telephone": self.telephone,
"mail": self.mail,
"poste": self.poste,
"service": self.service,
"siret": entreprise.siret,
"nom_entreprise": entreprise.nom,
"adresse_entreprise": entreprise.adresse,
"codepostal": entreprise.codepostal,
"ville": entreprise.ville,
"pays": entreprise.pays,
}
class EntrepriseOffre(db.Model):
__tablename__ = "entreprise_offre"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(
db.Integer, db.ForeignKey("entreprises.id", ondelete="cascade")
)
date_ajout = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
intitule = db.Column(db.Text)
description = db.Column(db.Text)
type_offre = db.Column(db.Text)
missions = db.Column(db.Text)
duree = db.Column(db.Text)
def to_dict(self):
return {
"intitule": self.intitule,
"description": self.description,
"type_offre": self.type_offre,
"missions": self.missions,
"duree": self.duree,
}
class EntrepriseLog(db.Model):
__tablename__ = "entreprise_log"
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
authenticated_user = db.Column(db.Text)
object = db.Column(db.Integer)
text = db.Column(db.Text)
class EntrepriseEtudiant(db.Model):
__tablename__ = "entreprise_etudiant"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id"))
etudid = db.Column(db.Integer)
type_offre = db.Column(db.Text)
date_debut = db.Column(db.Date)
date_fin = db.Column(db.Date)
formation_text = db.Column(db.Text)
formation_scodoc = db.Column(db.Integer)
class EntrepriseEnvoiOffre(db.Model):
__tablename__ = "entreprise_envoi_offre"
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
receiver_id = db.Column(db.Integer, db.ForeignKey("user.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
class EntrepriseEnvoiOffreEtudiant(db.Model):
__tablename__ = "entreprise_envoi_offre_etudiant"
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey("user.id"))
receiver_id = db.Column(db.Integer, db.ForeignKey("identite.id"))
offre_id = db.Column(db.Integer, db.ForeignKey("entreprise_offre.id"))
date_envoi = db.Column(db.DateTime(timezone=True), server_default=db.func.now())

858
app/entreprises/routes.py Normal file
View File

@ -0,0 +1,858 @@
import os
from config import Config
from datetime import datetime, timedelta
import glob
import shutil
from flask import render_template, redirect, url_for, request, flash, send_file, abort
from flask.json import jsonify
from flask_login import current_user
from app.decorators import permission_required
from app.entreprises import LOGS_LEN
from app.entreprises.forms import (
EntrepriseCreationForm,
EntrepriseModificationForm,
SuppressionConfirmationForm,
OffreCreationForm,
OffreModificationForm,
ContactCreationForm,
ContactModificationForm,
HistoriqueCreationForm,
EnvoiOffreForm,
AjoutFichierForm,
)
from app.entreprises import bp
from app.entreprises.models import (
Entreprise,
EntrepriseOffre,
EntrepriseContact,
EntrepriseLog,
EntrepriseEtudiant,
EntrepriseEnvoiOffre,
)
from app.models import Identite
from app.auth.models import User
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_etud, sco_excel
import app.scodoc.sco_utils as scu
from app import db
from sqlalchemy import text
from werkzeug.utils import secure_filename
@bp.route("/", methods=["GET"])
def index():
"""
Permet d'afficher une page avec la liste des entreprises et une liste des dernières opérations
Retourne: template de la page (entreprises.html)
Arguments du template:
title:
titre de la page
entreprises:
liste des entreprises
logs:
liste des logs
"""
entreprises = Entreprise.query.all()
logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all()
return render_template(
"entreprises/entreprises.html",
title=("Entreprises"),
entreprises=entreprises,
logs=logs,
)
@bp.route("/contacts", methods=["GET"])
def contacts():
"""
Permet d'afficher une page la liste des contacts et une liste des dernières opérations
Retourne: template de la page (contacts.html)
Arguments du template:
title:
titre de la page
contacts:
liste des contacts
logs:
liste des logs
"""
contacts = (
db.session.query(EntrepriseContact, Entreprise)
.join(Entreprise, EntrepriseContact.entreprise_id == Entreprise.id)
.all()
)
logs = EntrepriseLog.query.order_by(EntrepriseLog.date.desc()).limit(LOGS_LEN).all()
return render_template(
"entreprises/contacts.html", title=("Contacts"), contacts=contacts, logs=logs
)
@bp.route("/fiche_entreprise/<int:id>", methods=["GET"])
def fiche_entreprise(id):
"""
Permet d'afficher la fiche entreprise d'une entreprise avec une liste des dernières opérations et
l'historique des étudiants ayant réaliser un stage ou une alternance dans cette entreprise.
La fiche entreprise comporte les informations de l'entreprise, les contacts de l'entreprise et
les offres de l'entreprise.
Arguments:
id:
l'id de l'entreprise
Retourne: template de la page (fiche_entreprise.html)
Arguments du template:
title:
titre de la page
entreprise:
un objet entreprise
contacts:
liste des contacts de l'entreprise
offres:
liste des offres de l'entreprise avec leurs fichiers
logs:
liste des logs
historique:
liste des étudiants ayant réaliser un stage ou une alternance dans l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404()
offres = entreprise.offres
offres_with_files = []
for offre in offres:
if datetime.now() - offre.date_ajout.replace(tzinfo=None) >= timedelta(
days=90
): # pour une date d'expiration ?
break
files = []
path = os.path.join(
Config.SCODOC_VAR_DIR,
"entreprises",
f"{offre.entreprise_id}",
f"{offre.id}",
)
if os.path.exists(path):
for dir in glob.glob(
f"{path}/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
):
for file in glob.glob(f"{dir}/*"):
file = [os.path.basename(dir), os.path.basename(file)]
files.append(file)
offres_with_files.append([offre, files])
contacts = entreprise.contacts
logs = (
EntrepriseLog.query.order_by(EntrepriseLog.date.desc())
.filter_by(object=id)
.limit(LOGS_LEN)
.all()
)
historique = (
db.session.query(EntrepriseEtudiant, Identite)
.order_by(EntrepriseEtudiant.date_debut.desc())
.filter(EntrepriseEtudiant.entreprise_id == id)
.join(Identite, Identite.id == EntrepriseEtudiant.etudid)
.all()
)
return render_template(
"entreprises/fiche_entreprise.html",
title=("Fiche entreprise"),
entreprise=entreprise,
contacts=contacts,
offres=offres_with_files,
logs=logs,
historique=historique,
)
@bp.route("/offres", methods=["GET"])
def offres():
"""
Permet d'afficher la page où l'on recoit les offres
Retourne: template de la page (offres.html)
Arguments du template:
title:
titre de la page
offres_recus:
liste des offres reçues
"""
offres_recues = (
db.session.query(EntrepriseEnvoiOffre, EntrepriseOffre)
.filter(EntrepriseEnvoiOffre.receiver_id == current_user.id)
.join(EntrepriseOffre, EntrepriseOffre.id == EntrepriseEnvoiOffre.offre_id)
.all()
)
return render_template(
"entreprises/offres.html", title=("Offres"), offres_recues=offres_recues
)
@bp.route("/add_entreprise", methods=["GET", "POST"])
def add_entreprise():
"""
Permet d'ajouter une entreprise dans la base avec un formulaire
"""
form = EntrepriseCreationForm()
if form.validate_on_submit():
entreprise = Entreprise(
nom=form.nom_entreprise.data.strip(),
siret=form.siret.data.strip(),
adresse=form.adresse.data.strip(),
codepostal=form.codepostal.data.strip(),
ville=form.ville.data.strip(),
pays=form.pays.data.strip(),
)
db.session.add(entreprise)
db.session.commit()
db.session.refresh(entreprise)
contact = EntrepriseContact(
entreprise_id=entreprise.id,
nom=form.nom_contact.data.strip(),
prenom=form.prenom_contact.data.strip(),
telephone=form.telephone.data.strip(),
mail=form.mail.data.strip(),
poste=form.poste.data.strip(),
service=form.service.data.strip(),
)
db.session.add(contact)
nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{entreprise.nom}</a>"
log = EntrepriseLog(
authenticated_user=current_user.user_name,
text=f"{nom_entreprise} - Création de la fiche entreprise ({entreprise.nom}) avec un contact",
)
db.session.add(log)
db.session.commit()
flash("L'entreprise a été ajouté à la liste.")
return redirect(url_for("entreprises.index"))
return render_template(
"entreprises/ajout_entreprise.html",
title=("Ajout entreprise + contact"),
form=form,
)
@bp.route("/edit_entreprise/<int:id>", methods=["GET", "POST"])
def edit_entreprise(id):
"""
Permet de modifier une entreprise de la base avec un formulaire
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = EntrepriseModificationForm()
if form.validate_on_submit():
nom_entreprise = f"<a href=/ScoDoc/entreprises/fiche_entreprise/{entreprise.id}>{form.nom.data.strip()}</a>"
if entreprise.nom != form.nom.data.strip():
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text=f"{nom_entreprise} - Modification du nom (ancien nom : {entreprise.nom})",
)
entreprise.nom = form.nom.data.strip()
db.session.add(log)
if entreprise.adresse != form.adresse.data.strip():
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text=f"{nom_entreprise} - Modification de l'adresse (ancienne adresse : {entreprise.adresse})",
)
entreprise.adresse = form.adresse.data.strip()
db.session.add(log)
if entreprise.codepostal != form.codepostal.data.strip():
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text=f"{nom_entreprise} - Modification du code postal (ancien code postal : {entreprise.codepostal})",
)
entreprise.codepostal = form.codepostal.data.strip()
db.session.add(log)
if entreprise.ville != form.ville.data.strip():
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text=f"{nom_entreprise} - Modification de la ville (ancienne ville : {entreprise.ville})",
)
entreprise.ville = form.ville.data.strip()
db.session.add(log)
if entreprise.pays != form.pays.data.strip():
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text=f"{nom_entreprise} - Modification du pays (ancien pays : {entreprise.pays})",
)
entreprise.pays = form.pays.data.strip()
db.session.add(log)
db.session.commit()
flash("L'entreprise a été modifié.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
elif request.method == "GET":
form.siret.data = entreprise.siret
form.nom.data = entreprise.nom
form.adresse.data = entreprise.adresse
form.codepostal.data = entreprise.codepostal
form.ville.data = entreprise.ville
form.pays.data = entreprise.pays
return render_template(
"entreprises/form.html", title=("Modification entreprise"), form=form
)
@bp.route("/delete_entreprise/<int:id>", methods=["GET", "POST"])
def delete_entreprise(id):
"""
Permet de supprimer une entreprise de la base avec un formulaire de confirmation
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = SuppressionConfirmationForm()
if form.validate_on_submit():
db.session.delete(entreprise)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text=f"Suppression de la fiche entreprise ({entreprise.nom})",
)
db.session.add(log)
db.session.commit()
flash("L'entreprise a été supprimé de la liste.")
return redirect(url_for("entreprises.index"))
return render_template(
"entreprises/delete_confirmation.html",
title=("Supression entreprise"),
form=form,
)
@bp.route("/add_offre/<int:id>", methods=["GET", "POST"])
def add_offre(id):
"""
Permet d'ajouter une offre a une entreprise
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = OffreCreationForm()
if form.validate_on_submit():
offre = EntrepriseOffre(
entreprise_id=entreprise.id,
intitule=form.intitule.data.strip(),
description=form.description.data.strip(),
type_offre=form.type_offre.data.strip(),
missions=form.missions.data.strip(),
duree=form.duree.data.strip(),
)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text="Création d'une offre",
)
db.session.add(offre)
db.session.add(log)
db.session.commit()
flash("L'offre a été ajouté à la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template("entreprises/form.html", title=("Ajout offre"), form=form)
@bp.route("/edit_offre/<int:id>", methods=["GET", "POST"])
def edit_offre(id):
"""
Permet de modifier une offre
Arguments:
id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=id).first_or_404()
form = OffreModificationForm()
if form.validate_on_submit():
offre.intitule = form.intitule.data.strip()
offre.description = form.description.data.strip()
offre.type_offre = form.type_offre.data.strip()
offre.missions = form.missions.data.strip()
offre.duree = form.duree.data.strip()
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=offre.entreprise_id,
text="Modification d'une offre",
)
db.session.add(log)
db.session.commit()
flash("L'offre a été modifié.")
return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise.id))
elif request.method == "GET":
form.intitule.data = offre.intitule
form.description.data = offre.description
form.type_offre.data = offre.type_offre
form.missions.data = offre.missions
form.duree.data = offre.duree
return render_template(
"entreprises/form.html", title=("Modification offre"), form=form
)
@bp.route("/delete_offre/<int:id>", methods=["GET", "POST"])
def delete_offre(id):
"""
Permet de supprimer une offre
Arguments:
id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=id).first_or_404()
entreprise_id = offre.entreprise.id
form = SuppressionConfirmationForm()
if form.validate_on_submit():
db.session.delete(offre)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=offre.entreprise_id,
text="Suppression d'une offre",
)
db.session.add(log)
db.session.commit()
flash("L'offre a été supprimé de la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id))
return render_template(
"entreprises/delete_confirmation.html", title=("Supression offre"), form=form
)
@bp.route("/add_contact/<int:id>", methods=["GET", "POST"])
def add_contact(id):
"""
Permet d'ajouter un contact a une entreprise
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = ContactCreationForm(hidden_entreprise_id=entreprise.id)
if form.validate_on_submit():
contact = EntrepriseContact(
entreprise_id=entreprise.id,
nom=form.nom.data.strip(),
prenom=form.prenom.data.strip(),
telephone=form.telephone.data.strip(),
mail=form.mail.data.strip(),
poste=form.poste.data.strip(),
service=form.service.data.strip(),
)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=entreprise.id,
text="Création d'un contact",
)
db.session.add(log)
db.session.add(contact)
db.session.commit()
flash("Le contact a été ajouté à la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template("entreprises/form.html", title=("Ajout contact"), form=form)
@bp.route("/edit_contact/<int:id>", methods=["GET", "POST"])
def edit_contact(id):
"""
Permet de modifier un contact
Arguments:
id:
l'id du contact
"""
contact = EntrepriseContact.query.filter_by(id=id).first_or_404()
form = ContactModificationForm()
if form.validate_on_submit():
contact.nom = form.nom.data.strip()
contact.prenom = form.prenom.data.strip()
contact.telephone = form.telephone.data.strip()
contact.mail = form.mail.data.strip()
contact.poste = form.poste.data.strip()
contact.service = form.service.data.strip()
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=contact.entreprise_id,
text="Modification d'un contact",
)
db.session.add(log)
db.session.commit()
flash("Le contact a été modifié.")
return redirect(
url_for("entreprises.fiche_entreprise", id=contact.entreprise.id)
)
elif request.method == "GET":
form.nom.data = contact.nom
form.prenom.data = contact.prenom
form.telephone.data = contact.telephone
form.mail.data = contact.mail
form.poste.data = contact.poste
form.service.data = contact.service
return render_template(
"entreprises/form.html", title=("Modification contact"), form=form
)
@bp.route("/delete_contact/<int:id>", methods=["GET", "POST"])
def delete_contact(id):
"""
Permet de supprimer un contact
Arguments:
id:
l'id du contact
"""
contact = EntrepriseContact.query.filter_by(id=id).first_or_404()
entreprise_id = contact.entreprise.id
form = SuppressionConfirmationForm()
if form.validate_on_submit():
contact_count = EntrepriseContact.query.filter_by(
entreprise_id=contact.entreprise.id
).count()
if contact_count == 1:
flash(
"Le contact n'a pas été supprimé de la fiche entreprise. (1 contact minimum)"
)
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id))
else:
db.session.delete(contact)
log = EntrepriseLog(
authenticated_user=current_user.user_name,
object=contact.entreprise_id,
text="Suppression d'un contact",
)
db.session.add(log)
db.session.commit()
flash("Le contact a été supprimé de la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise_id))
return render_template(
"entreprises/delete_confirmation.html", title=("Supression contact"), form=form
)
@bp.route("/add_historique/<int:id>", methods=["GET", "POST"])
def add_historique(id):
"""
Permet d'ajouter un étudiant ayant réalisé un stage ou une alternance sur la fiche entreprise de l'entreprise
Arguments:
id:
l'id de l'entreprise
"""
entreprise = Entreprise.query.filter_by(id=id).first_or_404()
form = HistoriqueCreationForm()
if form.validate_on_submit():
etudiant_nomcomplet = form.etudiant.data.upper().strip()
stm = text(
"SELECT id, CONCAT(nom, ' ', prenom) as nom_prenom FROM Identite WHERE CONCAT(nom, ' ', prenom)=:nom_prenom"
)
etudiant = (
Identite.query.from_statement(stm)
.params(nom_prenom=etudiant_nomcomplet)
.first()
)
formation = etudiant.inscription_courante_date(
form.date_debut.data, form.date_fin.data
)
historique = EntrepriseEtudiant(
entreprise_id=entreprise.id,
etudid=etudiant.id,
type_offre=form.type_offre.data.strip(),
date_debut=form.date_debut.data,
date_fin=form.date_fin.data,
formation_text=formation.formsemestre.titre if formation else None,
formation_scodoc=formation.formsemestre.formsemestre_id
if formation
else None,
)
db.session.add(historique)
db.session.commit()
flash("L'étudiant a été ajouté sur la fiche entreprise.")
return redirect(url_for("entreprises.fiche_entreprise", id=entreprise.id))
return render_template(
"entreprises/ajout_historique.html", title=("Ajout historique"), form=form
)
@bp.route("/envoyer_offre/<int:id>", methods=["GET", "POST"])
def envoyer_offre(id):
"""
Permet d'envoyer une offre à un utilisateur
Arguments:
id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=id).first_or_404()
form = EnvoiOffreForm()
if form.validate_on_submit():
responsable_data = form.responsable.data.upper().strip()
stm = text(
"SELECT id, UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')')) FROM \"user\" WHERE UPPER(CONCAT(nom, ' ', prenom, ' ', '(', user_name, ')'))=:responsable_data"
)
responsable = (
User.query.from_statement(stm)
.params(responsable_data=responsable_data)
.first()
)
envoi_offre = EntrepriseEnvoiOffre(
sender_id=current_user.id, receiver_id=responsable.id, offre_id=offre.id
)
db.session.add(envoi_offre)
db.session.commit()
flash(f"L'offre a été envoyé à {responsable.get_nomplogin()}.")
return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise_id))
return render_template(
"entreprises/envoi_offre_form.html", title=("Envoyer une offre"), form=form
)
@bp.route("/etudiants")
def json_etudiants():
"""
Permet de récuperer un JSON avec tous les étudiants
Arguments:
term:
le terme utilisé pour le filtre de l'autosuggest
Retourne:
le JSON de tous les étudiants (nom, prenom, formation actuelle?) correspondant au terme
"""
if request.args.get("term") == None:
abort(400)
term = request.args.get("term").strip()
etudiants = Identite.query.filter(Identite.nom.ilike(f"%{term}%")).all()
list = []
content = {}
for etudiant in etudiants:
value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
if etudiant.inscription_courante() is not None:
content = {
"id": f"{etudiant.id}",
"value": value,
"info": f"{etudiant.inscription_courante().formsemestre.titre}",
}
else:
content = {"id": f"{etudiant.id}", "value": value}
list.append(content)
content = {}
return jsonify(results=list)
@bp.route("/responsables")
def json_responsables():
"""
Permet de récuperer un JSON avec tous les étudiants
Arguments:
term:
le terme utilisé pour le filtre de l'autosuggest
Retourne:
le JSON de tous les utilisateurs (nom, prenom, login) correspondant au terme
"""
if request.args.get("term") == None:
abort(400)
term = request.args.get("term").strip()
responsables = User.query.filter(
User.nom.ilike(f"%{term}%"), User.nom.is_not(None), User.prenom.is_not(None)
).all()
list = []
content = {}
for responsable in responsables:
value = f"{responsable.get_nomplogin()}"
content = {"id": f"{responsable.id}", "value": value, "info": ""}
list.append(content)
content = {}
return jsonify(results=list)
@bp.route("/export_entreprises")
def export_entreprises():
"""
Permet d'exporter la liste des entreprises sous format excel (.xlsx)
"""
entreprises = Entreprise.query.all()
if entreprises:
keys = ["siret", "nom", "adresse", "ville", "codepostal", "pays"]
titles = keys[:]
L = [
[entreprise.to_dict().get(k, "") for k in keys]
for entreprise in entreprises
]
title = "entreprises"
xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title)
filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
else:
abort(404)
@bp.route("/export_contacts")
def export_contacts():
"""
Permet d'exporter la liste des contacts sous format excel (.xlsx)
"""
contacts = EntrepriseContact.query.all()
if contacts:
keys = ["nom", "prenom", "telephone", "mail", "poste", "service"]
titles = keys[:]
L = [[contact.to_dict().get(k, "") for k in keys] for contact in contacts]
title = "contacts"
xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title)
filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
else:
abort(404)
@bp.route("/export_contacts_bis")
def export_contacts_bis():
"""
Permet d'exporter la liste des contacts avec leur entreprise sous format excel (.xlsx)
"""
contacts = EntrepriseContact.query.all()
if contacts:
keys = [
"nom",
"prenom",
"telephone",
"mail",
"poste",
"service",
"nom_entreprise",
"siret",
"adresse_entreprise",
"ville",
"codepostal",
"pays",
]
titles = keys[:]
L = [
[contact.to_dict_export().get(k, "") for k in keys] for contact in contacts
]
title = "contacts"
xlsx = sco_excel.excel_simple_table(titles=titles, lines=L, sheet_name=title)
filename = title
return scu.send_file(xlsx, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
else:
abort(404)
@bp.route(
"/get_offre_file/<int:entreprise_id>/<int:offre_id>/<string:filedir>/<string:filename>"
)
def get_offre_file(entreprise_id, offre_id, filedir, filename):
"""
Permet de télécharger un fichier d'une offre
Arguments:
entreprise_id:
l'id de l'entreprise
offre_id:
l'id de l'offre
filedir:
le répertoire du fichier
filename:
le nom du fichier
"""
if os.path.isfile(
os.path.join(
Config.SCODOC_VAR_DIR,
"entreprises",
f"{entreprise_id}",
f"{offre_id}",
f"{filedir}",
f"{filename}",
)
):
return send_file(
os.path.join(
Config.SCODOC_VAR_DIR,
"entreprises",
f"{entreprise_id}",
f"{offre_id}",
f"{filedir}",
f"{filename}",
),
as_attachment=True,
)
else:
abort(404)
@bp.route("/add_offre_file/<int:offre_id>", methods=["GET", "POST"])
def add_offre_file(offre_id):
"""
Permet d'ajouter un fichier à une offre
Arguments:
offre_id:
l'id de l'offre
"""
offre = EntrepriseOffre.query.filter_by(id=offre_id).first_or_404()
form = AjoutFichierForm()
if form.validate_on_submit():
date = f"{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}"
path = os.path.join(
Config.SCODOC_VAR_DIR,
"entreprises",
f"{offre.entreprise_id}",
f"{offre.id}",
f"{date}",
)
os.makedirs(path)
file = form.fichier.data
filename = secure_filename(file.filename)
file.save(os.path.join(path, filename))
flash("Le fichier a été ajouté a l'offre.")
return redirect(url_for("entreprises.fiche_entreprise", id=offre.entreprise_id))
return render_template(
"entreprises/form.html", title=("Ajout fichier à une offre"), form=form
)
@bp.route("/delete_offre_file/<int:offre_id>/<string:filedir>", methods=["GET", "POST"])
def delete_offre_file(offre_id, filedir):
"""
Permet de supprimer un fichier d'une offre
Arguments:
offre_id:
l'id de l'offre
filedir:
le répertoire du fichier
"""
offre = EntrepriseOffre.query.filter_by(id=offre_id).first_or_404()
form = SuppressionConfirmationForm()
if form.validate_on_submit():
path = os.path.join(
Config.SCODOC_VAR_DIR,
"entreprises",
f"{offre.entreprise_id}",
f"{offre_id}",
f"{filedir}",
)
if os.path.isdir(path):
shutil.rmtree(path)
flash("Le fichier relié à l'offre a été supprimé.")
return redirect(
url_for("entreprises.fiche_entreprise", id=offre.entreprise_id)
)
return render_template(
"entreprises/delete_confirmation.html",
title=("Suppression fichier d'une offre"),
form=form,
)

View File

@ -28,7 +28,6 @@
"""
Formulaires configuration Exports Apogée (codes)
"""
import re
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
@ -71,7 +70,7 @@ class CodesDecisionsForm(FlaskForm):
ATT = _build_code_field("ATT")
CMP = _build_code_field("CMP")
DEF = _build_code_field("DEF")
DEM = _build_code_field("DEF")
DEM = _build_code_field("DEM")
NAR = _build_code_field("NAR")
RAT = _build_code_field("RAT")
submit = SubmitField("Valider")

View File

@ -47,7 +47,6 @@ from app.scodoc.sco_config_actions import (
LogoDelete,
LogoUpdate,
LogoInsert,
BonusSportUpdate,
)
from flask_login import current_user
@ -120,9 +119,9 @@ class AddLogoForm(FlaskForm):
label="Nom",
validators=[
validators.regexp(
r"^[a-zA-Z0-9-]*$",
r"^[a-zA-Z0-9-_]*$",
re.IGNORECASE,
"Ne doit comporter que lettres, chiffres ou -",
"Ne doit comporter que lettres, chiffres, _ ou -",
),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
@ -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

@ -12,14 +12,7 @@ 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.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.entreprises import (
Entreprise,
EntrepriseCorrespondant,
EntrepriseContact,
)
from app.models.etudiants import (
Identite,
Adresse,
@ -56,13 +49,15 @@ from app.models.evaluations import (
)
from app.models.groups import Partition, GroupDescr, group_membership
from app.models.notes import (
ScolarEvent,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
BulAppreciations,
NotesNotes,
NotesNotesLog,
)
from app.models.validations import (
ScolarEvent,
ScolarFormSemestreValidation,
ScolarAutorisationInscription,
)
from app.models.preferences import ScoPreference
from app.models.but_refcomp import (
@ -71,3 +66,4 @@ from app.models.but_refcomp import (
ApcSituationPro,
ApcAppCritique,
)
from app.models.config import ScoDocSiteConfig

View File

@ -9,11 +9,24 @@ from datetime import datetime
from enum import unique
from typing import Any
from sqlalchemy.orm import class_mapper
import sqlalchemy
from app import db
from app.scodoc.sco_utils import ModuleType
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
def attribute_names(cls):
"liste ids (noms de colonnes) d'un modèle"
return [
prop.key
for prop in class_mapper(cls).iterate_properties
if isinstance(prop, sqlalchemy.orm.ColumnProperty)
]
class XMLModel:
_xml_attribs = {} # to be overloaded
id = "_"
@ -24,21 +37,31 @@ class XMLModel:
and renamed for our models.
The mapping is specified by the _xml_attribs
attribute in each model class.
Keep only attributes corresponding to columns in our model:
other XML attributes are simply ignored.
"""
return {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
columns = attribute_names(cls)
renamed_attributes = {cls._xml_attribs.get(k, k): v for (k, v) in args.items()}
return {k: renamed_attributes[k] for k in renamed_attributes if k in columns}
def __repr__(self):
return f'<{self.__class__.__name__} {self.id} "{self.titre if hasattr(self, "titre") else ""}">'
class ApcReferentielCompetences(db.Model, XMLModel):
"Référentiel de compétence d'une spécialité"
id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
annexe = db.Column(db.Text())
specialite = db.Column(db.Text())
specialite_long = db.Column(db.Text())
type_titre = db.Column(db.Text())
type_structure = db.Column(db.Text())
type_departement = db.Column(db.Text()) # "secondaire", "tertiaire"
version_orebut = db.Column(db.Text())
_xml_attribs = { # Orébut xml attrib : attribute
"type": "type_titre",
"version": "version_orebut",
}
# ScoDoc specific fields:
scodoc_date_loaded = db.Column(db.DateTime, default=datetime.utcnow)
@ -58,15 +81,22 @@ class ApcReferentielCompetences(db.Model, XMLModel):
)
formations = db.relationship("Formation", backref="referentiel_competence")
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite}>"
def to_dict(self):
"""Représentation complète du ref. de comp.
comme un dict.
"""
return {
"dept_id": self.dept_id,
"annexe": self.annexe,
"specialite": self.specialite,
"specialite_long": self.specialite_long,
"type_structure": self.type_structure,
"type_departement": self.type_departement,
"type_titre": self.type_titre,
"version_orebut": self.version_orebut,
"scodoc_date_loaded": self.scodoc_date_loaded.isoformat() + "Z"
if self.scodoc_date_loaded
else "",
@ -77,23 +107,21 @@ class ApcReferentielCompetences(db.Model, XMLModel):
class ApcCompetence(db.Model, XMLModel):
__table_args__ = (
# les compétences dans Orébut sont identifiées par leur "titre"
# unique au sein d'un référentiel:
db.UniqueConstraint(
"referentiel_id", "titre", name="apc_competence_referentiel_id_titre_key"
),
)
"Compétence"
id = db.Column(db.Integer, primary_key=True)
referentiel_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
# les compétences dans Orébut sont identifiées par leur id unique
# (mais id_orebut n'est pas unique car le même ref. pourra être chargé dans plusieurs depts)
id_orebut = db.Column(db.Text(), nullable=True, index=True)
titre = db.Column(db.Text(), nullable=False, index=True)
titre_long = db.Column(db.Text())
couleur = db.Column(db.Text())
numero = db.Column(db.Integer) # ordre de présentation
_xml_attribs = { # xml_attrib : attribute
"name": "titre",
"id": "id_orebut",
"nom_court": "titre", # was name
"libelle_long": "titre_long",
}
situations = db.relationship(
@ -115,8 +143,12 @@ class ApcCompetence(db.Model, XMLModel):
cascade="all, delete-orphan",
)
def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre}>"
def to_dict(self):
return {
"id_orebut": self.id_orebut,
"titre": self.titre,
"titre_long": self.titre_long,
"couleur": self.couleur,
@ -246,7 +278,10 @@ class ApcAnneeParcours(db.Model, XMLModel):
return {
"ordre": self.ordre,
"competences": {
x.competence.titre: {"niveau": x.niveau}
x.competence.titre: {
"niveau": x.niveau,
"id_orebut": x.competence.id_orebut,
}
for x in self.niveaux_competences
},
}

View File

@ -3,10 +3,10 @@
"""Model : site config WORK IN PROGRESS #WIP
"""
from flask import flash
from app import db, log
from app.scodoc import bonus_sport
from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
import functools
from app.scodoc.sco_codes_parcours import (
ADC,
@ -87,70 +87,101 @@ class ScoDocSiteConfig(db.Model):
}
@classmethod
def set_bonus_sport_func(cls, func_name):
def set_bonus_sport_class(cls, class_name):
"""Record bonus_sport config.
If func_name not defined, raise NameError
If class_name not defined, raise NameError
"""
if func_name not in cls.get_bonus_sport_func_names():
raise NameError("invalid function name for bonus_sport")
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 " + func_name)
c.value = func_name
log("setting to " + class_name)
c.value = class_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name)
c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_func_name(cls):
def get_bonus_sport_class_name(cls):
"""Get configured bonus function name, or None if None."""
f = cls.get_bonus_sport_func_from_name()
if f is None:
klass = cls.get_bonus_sport_class_from_name()
if klass is None:
return ""
else:
return f.__name__
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):
"""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):
"""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.
"""
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
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}.
f"""Fonction de calcul de l'UE bonus 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_")
]
)
@classmethod
def get_code_apo(cls, code: str) -> str:
"""La représentation d'un code pour les exports Apogée.

View File

@ -21,7 +21,7 @@ class Departement(db.Model):
db.Boolean(), nullable=False, default=True, server_default="true"
) # sur page d'accueil
entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement")
# entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement")
etudiants = db.relationship("Identite", lazy="dynamic", backref="departement")
formations = db.relationship("Formation", lazy="dynamic", backref="departement")
formsemestres = db.relationship(

View File

@ -1,66 +0,0 @@
# -*- coding: UTF-8 -*
"""Gestion des absences
"""
from app import db
class Entreprise(db.Model):
"""une entreprise"""
__tablename__ = "entreprises"
id = db.Column(db.Integer, primary_key=True)
entreprise_id = db.synonym("id")
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
nom = db.Column(db.Text)
adresse = db.Column(db.Text)
ville = db.Column(db.Text)
codepostal = db.Column(db.Text)
pays = db.Column(db.Text)
contact_origine = db.Column(db.Text)
secteur = db.Column(db.Text)
note = db.Column(db.Text)
privee = db.Column(db.Text)
localisation = db.Column(db.Text)
# -1 inconnue, 0, 25, 50, 75, 100:
qualite_relation = db.Column(db.Integer)
plus10salaries = db.Column(db.Boolean())
date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
class EntrepriseCorrespondant(db.Model):
"""Personne contact en entreprise"""
__tablename__ = "entreprise_correspondant"
id = db.Column(db.Integer, primary_key=True)
entreprise_corresp_id = db.synonym("id")
entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id"))
nom = db.Column(db.Text)
prenom = db.Column(db.Text)
civilite = db.Column(db.Text)
fonction = db.Column(db.Text)
phone1 = db.Column(db.Text)
phone2 = db.Column(db.Text)
mobile = db.Column(db.Text)
mail1 = db.Column(db.Text)
mail2 = db.Column(db.Text)
fax = db.Column(db.Text)
note = db.Column(db.Text)
class EntrepriseContact(db.Model):
"""Evènement (contact) avec une entreprise"""
__tablename__ = "entreprise_contact"
id = db.Column(db.Integer, primary_key=True)
entreprise_contact_id = db.synonym("id")
date = db.Column(db.DateTime(timezone=True))
type_contact = db.Column(db.Text)
entreprise_id = db.Column(db.Integer, db.ForeignKey("entreprises.id"))
entreprise_corresp_id = db.Column(
db.Integer, db.ForeignKey("entreprise_correspondant.id")
)
etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression
description = db.Column(db.Text)
enseignant = db.Column(db.Text)

View File

@ -4,11 +4,18 @@
et données rattachées (adresses, annotations, ...)
"""
from flask import g, url_for
from functools import cached_property
from flask import abort, url_for
from flask import g, request
import sqlalchemy
from app import db
from app import models
from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat
import app.scodoc.sco_utils as scu
class Identite(db.Model):
"""étudiant"""
@ -46,18 +53,37 @@ class Identite(db.Model):
#
adresses = db.relationship("Adresse", lazy="dynamic", backref="etud")
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
# one-to-one relation:
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
def __repr__(self):
return f"<Etud {self.id} {self.nom} {self.prenom}>"
@classmethod
def from_request(cls, etudid=None, code_nip=None):
"""Etudiant à partir de l'etudid ou du code_nip, soit
passés en argument soit retrouvés directement dans la requête web.
Erreur 404 si inexistant.
"""
args = make_etud_args(etudid=etudid, code_nip=code_nip)
return Identite.query.filter_by(**args).first_or_404()
@property
def civilite_str(self):
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
personnes ne souhaitant pas d'affichage).
"""
return {"M": "M.", "F": "Mme", "X": ""}[self.civilite]
def nom_disp(self):
"nom à afficher"
def sex_nom(self, no_accents=False) -> str:
"'M. DUPONTÉ', ou si no_accents, 'M. DUPONTE'"
s = f"{self.civilite_str} {(self.nom_usuel or self.nom).upper()}"
if no_accents:
return scu.suppress_accents(s)
return s
def nom_disp(self) -> str:
"Nom à afficher"
if self.nom_usuel:
return (
(self.nom_usuel + " (" + self.nom + ")") if self.nom else self.nom_usuel
@ -65,10 +91,52 @@ class Identite(db.Model):
else:
return self.nom
@cached_property
def nomprenom(self, reverse=False) -> str:
"""Civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
"""
nom = self.nom_usuel or self.nom
prenom = self.prenom_str
if reverse:
fields = (nom, prenom)
else:
fields = (self.civilite_str, prenom, nom)
return " ".join([x for x in fields if x])
@property
def prenom_str(self):
"""Prénom à afficher. Par exemple: "Jean-Christophe" """
if not self.prenom:
return ""
frags = self.prenom.split()
r = []
for frag in frags:
fields = frag.split("-")
r.append("-".join([x.lower().capitalize() for x in fields]))
return " ".join(r)
@cached_property
def sort_key(self) -> tuple:
"clé pour tris par ordre alphabétique"
return (self.nom_usuel or self.nom).lower(), self.prenom.lower()
def get_first_email(self, field="email") -> str:
"le mail associé à la première adrese de l'étudiant, ou None"
"Le mail associé à la première adrese de l'étudiant, ou None"
return self.adresses[0].email or None if self.adresses.count() > 0 else None
def to_dict_scodoc7(self):
"""Représentation dictionnaire,
compatible ScoDoc7 mais sans infos admission
"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
e["etudid"] = self.id
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)")
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
def to_dict_bul(self, include_urls=True):
"""Infos exportées dans les bulletins"""
from app.scodoc import sco_photos
@ -104,6 +172,17 @@ class Identite(db.Model):
]
return r[0] if r else None
def inscription_courante_date(self, date_debut, date_fin):
"""La première inscription à un formsemestre incluant la
période [date_debut, date_fin]
"""
r = [
ins
for ins in self.formsemestre_inscriptions
if ins.formsemestre.contient_periode(date_debut, date_fin)
]
return r[0] if r else None
def etat_inscription(self, formsemestre_id):
"""etat de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
@ -117,6 +196,42 @@ class Identite(db.Model):
return False
def make_etud_args(
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True
) -> dict:
"""forme args dict pour requete recherche etudiant
On peut specifier etudid
ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine
(dans cet ordre).
Résultat: dict avec soit "etudid", soit "code_nip", soit "code_ine"
"""
args = None
if etudid:
args = {"etudid": etudid}
elif code_nip:
args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global)
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
if "etudid" in vals:
args = {"etudid": int(vals["etudid"])}
elif "code_nip" in vals:
args = {"code_nip": str(vals["code_nip"])}
elif "code_ine" in vals:
args = {"code_ine": str(vals["code_ine"])}
if not args:
if abort_404:
abort(404, "pas d'étudiant sélectionné")
elif raise_exc:
raise ValueError("make_etud_args: pas d'étudiant sélectionné !")
return args
class Adresse(db.Model):
"""Adresse d'un étudiant
(le modèle permet plusieurs adresses, mais l'UI n'en gère qu'une seule)
@ -193,6 +308,28 @@ class Admission(db.Model):
# classement (1..Ngr) par le jury dans le groupe APB
apb_classement_gr = db.Column(db.Integer)
def get_bac(self) -> Baccalaureat:
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
return Baccalaureat(self.bac, specialite=self.specialite)
def to_dict(self, no_nulls=False):
"""Représentation dictionnaire,"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
if no_nulls:
for k in e:
if e[k] is None:
col_type = getattr(
sqlalchemy.inspect(models.Admission).columns, "apb_groupe"
).expression.type
if isinstance(col_type, sqlalchemy.Text):
e[k] = ""
elif isinstance(col_type, sqlalchemy.Integer):
e[k] = 0
elif isinstance(col_type, sqlalchemy.Boolean):
e[k] = False
return e
# Suivi scolarité / débouchés
class ItemSuivi(db.Model):

View File

@ -2,12 +2,16 @@
"""ScoDoc models: evaluations
"""
import datetime
from app import db
from app.models import UniteEns
from app.models import formsemestre
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.notesdb as ndb
from app.scodoc import sco_evaluation_db
class Evaluation(db.Model):
@ -51,11 +55,11 @@ class Evaluation(db.Model):
e["evaluation_id"] = self.id
e["jour"] = ndb.DateISOtoDMY(e["jour"])
e["numero"] = ndb.int_null_is_zero(e["numero"])
return sco_evaluation_db.evaluation_enrich_dict(e)
return evaluation_enrich_dict(e)
def from_dict(self, data):
"""Set evaluation attributes from given dict values."""
sco_evaluation_db._check_evaluation_args(data)
check_evaluation_args(data)
for k in self.__dict__.keys():
if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k])
@ -145,3 +149,89 @@ class EvaluationUEPoids(db.Model):
def __repr__(self):
return f"<EvaluationUEPoids {self.evaluation} {self.ue} poids={self.poids}>"
# Fonction héritée de ScoDoc7 à refactorer
def evaluation_enrich_dict(e):
"""add or convert some fileds in an evaluation dict"""
# For ScoDoc7 compat
heure_debut_dt = e["heure_debut"] or datetime.time(
8, 00
) # au cas ou pas d'heure (note externe?)
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None:
m = d % 60
e["duree"] = "%dh" % (d / 60)
if m != 0:
e["duree"] += "%02d" % m
else:
e["duree"] = ""
if heure_debut and (not heure_fin or heure_fin == heure_debut):
e["descrheure"] = " à " + heure_debut
elif heure_debut and heure_fin:
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
else:
e["descrheure"] = ""
# matin, apresmidi: utile pour se referer aux absences:
if heure_debut_dt < datetime.time(12, 00):
e["matin"] = 1
else:
e["matin"] = 0
if heure_fin_dt > datetime.time(12, 00):
e["apresmidi"] = 1
else:
e["apresmidi"] = 0
return e
def check_evaluation_args(args):
"Check coefficient, dates and duration, raises exception if invalid"
moduleimpl_id = args["moduleimpl_id"]
# check bareme
note_max = args.get("note_max", None)
if note_max is None:
raise ScoValueError("missing note_max")
try:
note_max = float(note_max)
except ValueError:
raise ScoValueError("Invalid note_max value")
if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)")
# check coefficient
coef = args.get("coefficient", None)
if coef is None:
raise ScoValueError("missing coefficient")
try:
coef = float(coef)
except ValueError:
raise ScoValueError("Invalid coefficient value")
if coef < 0:
raise ScoValueError("Invalid coefficient value (must be positive or null)")
# check date
jour = args.get("jour", None)
args["jour"] = jour
if jour:
modimpl = ModuleImpl.query.get(moduleimpl_id)
formsemestre = modimpl.formsemestre
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
jour = datetime.date(y, m, d)
if (jour > formsemestre.date_fin) or (jour < formsemestre.date_debut):
raise ScoValueError(
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
% (d, m, y),
dest_url="javascript:history.back();",
)
heure_debut = args.get("heure_debut", None)
args["heure_debut"] = heure_debut
heure_fin = args.get("heure_fin", None)
args["heure_fin"] = heure_fin
if jour and ((not heure_debut) or (not heure_fin)):
raise ScoValueError("Les heures doivent être précisées")
d = ndb.TimeDuration(heure_debut, heure_fin)
if d and ((d < 0) or (d > 60 * 12)):
raise ScoValueError("Heures de l'évaluation incohérentes !")

View File

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

@ -3,10 +3,12 @@
"""ScoDoc models: formsemestre
"""
import datetime
from functools import cached_property
import flask_sqlalchemy
from app import db
from app import log
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
@ -20,6 +22,7 @@ from app.models.etudiants import Identite
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_permissions import Permission
class FormSemestre(db.Model):
@ -47,7 +50,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"
)
@ -84,7 +87,11 @@ class FormSemestre(db.Model):
etapes = db.relationship(
"FormSemestreEtape", cascade="all,delete", backref="formsemestre"
)
modimpls = db.relationship("ModuleImpl", backref="formsemestre", lazy="dynamic")
modimpls = db.relationship(
"ModuleImpl",
backref="formsemestre",
lazy="dynamic",
)
etuds = db.relationship(
"Identite",
secondary="notes_formsemestre_inscription",
@ -97,6 +104,11 @@ class FormSemestre(db.Model):
lazy=True,
backref=db.backref("formsemestres", lazy=True),
)
partitions = db.relationship(
"Partition",
backref=db.backref("formsemestre", lazy=True),
lazy="dynamic",
)
# Ancien id ScoDoc7 pour les migrations de bases anciennes
# ne pas utiliser après migrate_scodoc7_dept_archives
scodoc7_id = db.Column(db.Text(), nullable=True)
@ -106,18 +118,64 @@ 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)
# ScoDoc7 output_formators: (backward compat)
d["formsemestre_id"] = self.id
d["date_debut"] = (
self.date_debut.strftime("%d/%m/%Y") if self.date_debut else ""
)
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y") if self.date_fin else ""
d["titre_num"] = self.titre_num()
if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
d["date_debut_iso"] = self.date_debut.isoformat()
else:
d["date_debut"] = d["date_debut_iso"] = ""
if self.date_fin:
d["date_fin"] = self.date_fin.strftime("%d/%m/%Y")
d["date_fin_iso"] = self.date_fin.isoformat()
else:
d["date_fin"] = d["date_fin_iso"] = ""
d["responsables"] = [u.id for u in self.responsables]
return d
def get_infos_dict(self) -> dict:
"""Un dict avec des informations sur le semestre
pour les bulletins et autres templates
(contenu compatible scodoc7 / anciens templates)
"""
d = self.to_dict()
d["anneescolaire"] = self.annee_scolaire_str()
d["annee_debut"] = str(self.date_debut.year)
d["annee"] = d["annee_debut"]
d["annee_fin"] = str(self.date_fin.year)
if d["annee_fin"] != d["annee_debut"]:
d["annee"] += "-" + str(d["annee_fin"])
d["mois_debut_ord"] = self.date_debut.month
d["mois_fin_ord"] = self.date_fin.month
# La période: considère comme "S1" (ou S3) les débuts en aout-sept-octobre
# devrait sans doute pouvoir etre changé...
if self.date_debut.month >= 8 and self.date_debut.month <= 10:
d["periode"] = 1 # typiquement, début en septembre: S1, S3...
else:
d["periode"] = 2 # typiquement, début en février: S2, S4...
d["titre_num"] = self.titre_num()
d["titreannee"] = self.titre_annee()
d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}"
d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}"
d["titremois"] = "%s %s (%s - %s)" % (
d["titre_num"],
self.modalite or "",
d["mois_debut"],
d["mois_fin"],
)
d["session_id"] = self.session_id()
d["etapes"] = self.etapes_apo_vdi()
d["etapes_apo_str"] = self.etapes_apo_str()
d["responsables"] = [u.id for u in self.responsables] # liste des ids
return d
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
"""UE des modules de ce semestre, triées par numéro.
- Formations classiques: les UEs auxquelles appartiennent
@ -139,6 +197,38 @@ 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 (y compris bonus)
- 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 or 0,
m.module.matiere.numero or 0,
m.module.numero or 0,
m.module.code or "",
)
)
return modimpls
def can_be_edited_by(self, user):
"""Vrai si user peut modifier ce semestre"""
if not user.has_permission(Permission.ScoImplement): # pas chef
if not self.resp_can_edit or user.id not in [
resp.id for resp in self.responsables
]:
return False
return True
def est_courant(self) -> bool:
"""Vrai si la date actuelle (now) est dans le semestre
(les dates de début et fin sont incluses)
@ -146,6 +236,31 @@ class FormSemestre(db.Model):
today = datetime.date.today()
return (self.date_debut <= today) and (today <= self.date_fin)
def contient_periode(self, date_debut, date_fin) -> bool:
"""Vrai si l'intervalle [date_debut, date_fin] est
inclus dans le semestre.
(les dates de début et fin sont incluses)
"""
return (self.date_debut <= date_debut) and (date_fin <= self.date_fin)
def est_sur_une_annee(self):
"""Test si sem est entièrement sur la même année scolaire.
(ce n'est pas obligatoire mais si ce n'est pas le
cas les exports Apogée risquent de mal fonctionner)
Pivot au 1er août.
"""
if self.date_debut > self.date_fin:
log(f"Warning: semestre {self.id} begins after ending !")
annee_debut = self.date_debut.year
if self.date_debut.month < 8: # août
# considere que debut sur l'anne scolaire precedente
annee_debut -= 1
annee_fin = self.date_fin.year
if self.date_fin.month < 9:
# 9 (sept) pour autoriser un début en sept et une fin en aout
annee_fin -= 1
return annee_debut == annee_fin
def est_decale(self):
"""Vrai si semestre "décalé"
c'est à dire semestres impairs commençant entre janvier et juin
@ -157,6 +272,11 @@ class FormSemestre(db.Model):
not self.semestre_id % 2 and self.date_debut.month > 6
)
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
"Liste des vdis"
# was read_formsemestre_etapes
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
def etapes_apo_str(self) -> str:
"""Chaine décrivant les étapes de ce semestre
ex: "V1RT, V1RT3, V1RT4"
@ -213,6 +333,15 @@ class FormSemestre(db.Model):
"-".join((imputation_dept, parcours_name, modalite, semestre_id, annee_sco))
)
def titre_annee(self) -> str:
""" """
titre_annee = (
f"{self.titre_num()} {self.modalite or ''} {self.date_debut.year}"
)
if self.date_fin.year != self.date_debut.year:
titre_annee += "-" + str(self.date_fin.year)
return titre_annee
def titre_mois(self) -> str:
"""Le titre et les dates du semestre, pour affichage dans des listes
Ex: "BUT QLIO (PN 2022) semestre 1 FI (Sept 2022 - Jan 2023)"
@ -240,14 +369,29 @@ class FormSemestre(db.Model):
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
)
def get_inscrits(self, include_dem=False) -> list:
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
"""Liste des étudiants inscrits à ce semestre
Si all, tous les étudiants, avec les démissionnaires.
Si include_demdef, tous les étudiants, avec les démissionnaires
et défaillants.
Si order, tri par clé sort_key
"""
if include_dem:
return [ins.etud for ins in self.inscriptions]
if include_demdef:
etuds = [ins.etud for ins in self.inscriptions]
else:
return [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
etuds = [ins.etud for ins in self.inscriptions if ins.etat == scu.INSCRIT]
if order:
etuds.sort(key=lambda e: e.sort_key)
return etuds
@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)"""
return {ins.etud.id: ins for ins in self.inscriptions}
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
@ -342,16 +486,18 @@ class FormSemestreUECoef(db.Model):
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
index=True,
)
coefficient = db.Column(db.Float, nullable=False)
class FormSemestreUEComputationExpr(db.Model):
"""Formules utilisateurs pour calcul moyenne UE"""
"""Formules utilisateurs pour calcul moyenne UE (désactivées en 9.2+)."""
__tablename__ = "notes_formsemestre_ue_computation_expr"
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id"),)

View File

@ -31,6 +31,11 @@ class Partition(db.Model):
show_in_lists = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
)
groups = db.relationship(
"GroupDescr",
backref=db.backref("partition", lazy=True),
lazy="dynamic",
)
def __init__(self, **kwargs):
super(Partition, self).__init__(**kwargs)
@ -42,6 +47,9 @@ class Partition(db.Model):
else:
self.numero = 1
def __repr__(self):
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">"""
class GroupDescr(db.Model):
"""Description d'un groupe d'une partition"""
@ -55,6 +63,11 @@ class GroupDescr(db.Model):
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
def __repr__(self):
return (
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
)
group_membership = db.Table(
"group_membership",

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
@ -53,7 +53,7 @@ class ModuleImpl(db.Model):
if evaluations_poids is None:
from app.comp import moy_mod
evaluations_poids, _ = moy_mod.df_load_evaluations_poids(self.id)
evaluations_poids, _ = moy_mod.load_evaluations_poids(self.id)
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
return evaluations_poids
@ -72,14 +72,14 @@ class ModuleImpl(db.Model):
return True
from app.comp import moy_mod
return moy_mod.check_moduleimpl_conformity(
return moy_mod.moduleimpl_is_conforme(
self,
self.get_evaluations_poids(),
self.module.formation.get_module_coefs(self.module.semestre_id),
)
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
"""as a dict, with the same conversions as in ScoDoc7, including module"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat)
@ -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
@ -33,7 +34,7 @@ class Module(db.Model):
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
# Type: ModuleType: DEFAULT, MALUS, RESSOURCE, MODULE_SAE (enum)
module_type = db.Column(db.Integer)
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations:
modimpls = db.relationship("ModuleImpl", backref="module", lazy="dynamic")
ues_apc = db.relationship("UniteEns", secondary="module_ue_coef", viewonly=True)
@ -62,6 +63,7 @@ class Module(db.Model):
e["numero"] = 0 if self.numero is None else self.numero
e["coefficient"] = 0.0 if self.coefficient is None else self.coefficient
e["module_type"] = 0 if self.module_type is None else self.module_type
e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e
def is_apc(self):
@ -129,9 +131,29 @@ class Module(db.Model):
# à redéfinir les relationships...
return sorted(self.ue_coefs, key=lambda x: x.ue.numero)
def ue_coefs_descr(self):
"""List of tuples [ (ue_acronyme, coef) ]"""
return [(c.ue.acronyme, c.coef) for c in self.get_ue_coefs_sorted()]
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,
sauf UE bonus sport.
Result: List of tuples [ (ue, coef) ]
"""
if not self.is_apc():
return []
if include_zeros:
# 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()
)
coefs_dict = self.get_ue_coef_dict()
coefs_list = []
for ue in ues_semestre:
coefs_list.append((ue, coefs_dict.get(ue.id, 0.0)))
return coefs_list
# Liste seulement les coefs définis:
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
class ModuleUECoef(db.Model):

View File

@ -4,102 +4,9 @@
"""
from app import db
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
class ScolarEvent(db.Model):
"""Evenement dans le parcours scolaire d'un étudiant"""
__tablename__ = "scolar_events"
id = db.Column(db.Integer, primary_key=True)
event_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
)
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
# 'ECHEC_SEM'
# 'UTIL_COMPENSATION'
event_type = db.Column(db.String(SHORT_STR_LEN))
# Semestre compensé par formsemestre_id:
comp_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
class ScolarFormSemestreValidation(db.Model):
"""Décisions de jury"""
__tablename__ = "scolar_formsemestre_validation"
# Assure unicité de la décision:
__table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),)
id = db.Column(db.Integer, primary_key=True)
formsemestre_validation_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
index=True,
)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
index=True,
)
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
# NULL pour les UE, True|False pour les semestres:
assidu = db.Column(db.Boolean)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# NULL sauf si compense un semestre:
compense_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
moy_ue = db.Column(db.Float)
# (normalement NULL) indice du semestre, utile seulement pour
# UE "antérieures" et si la formation définit des UE utilisées
# dans plusieurs semestres (cas R&T IUTV v2)
semestre_id = db.Column(db.Integer)
# Si UE validée dans le cursus d'un autre etablissement
is_external = db.Column(db.Boolean, default=False, server_default="false")
class ScolarAutorisationInscription(db.Model):
"""Autorisation d'inscription dans un semestre"""
__tablename__ = "scolar_autorisation_inscription"
id = db.Column(db.Integer, primary_key=True)
autorisation_inscription_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
)
formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
# semestre ou on peut s'inscrire:
semestre_id = db.Column(db.Integer)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
origin_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
class BulAppreciations(db.Model):
@ -161,3 +68,32 @@ class NotesNotesLog(db.Model):
comment = db.Column(db.Text) # texte libre
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
uid = db.Column(db.Integer, db.ForeignKey("user.id"))
def etud_has_notes_attente(etudid, formsemestre_id):
"""Vrai si cet etudiant a au moins une note en attente dans ce semestre.
(ne compte que les notes en attente dans des évaluation avec coef. non nul).
"""
# XXX ancienne méthode de notes_table à ré-écrire
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT n.*
FROM notes_notes n, notes_evaluation e, notes_moduleimpl m,
notes_moduleimpl_inscription i
WHERE n.etudid = %(etudid)s
and n.value = %(code_attente)s
and n.evaluation_id = e.id
and e.moduleimpl_id = m.id
and m.formsemestre_id = %(formsemestre_id)s
and e.coefficient != 0
and m.id = i.moduleimpl_id
and i.etudid=%(etudid)s
""",
{
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"code_attente": scu.NOTES_ATTENTE,
},
)
return len(cursor.fetchall()) > 0

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")
@ -60,6 +62,7 @@ class UniteEns(db.Model):
e["numero"] = e["numero"] if e["numero"] else 0
e["ects"] = e["ects"] if e["ects"] else 0.0
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e
def is_locked(self):

109
app/models/validations.py Normal file
View File

@ -0,0 +1,109 @@
# -*- coding: UTF-8 -*
"""Notes, décisions de jury, évènements scolaires
"""
from app import db
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
class ScolarFormSemestreValidation(db.Model):
"""Décisions de jury"""
__tablename__ = "scolar_formsemestre_validation"
# Assure unicité de la décision:
__table_args__ = (db.UniqueConstraint("etudid", "formsemestre_id", "ue_id"),)
id = db.Column(db.Integer, primary_key=True)
formsemestre_validation_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
index=True,
)
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
index=True,
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
index=True,
)
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
# NULL pour les UE, True|False pour les semestres:
assidu = db.Column(db.Boolean)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
# NULL sauf si compense un semestre:
compense_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
moy_ue = db.Column(db.Float)
# (normalement NULL) indice du semestre, utile seulement pour
# UE "antérieures" et si la formation définit des UE utilisées
# dans plusieurs semestres (cas R&T IUTV v2)
semestre_id = db.Column(db.Integer)
# Si UE validée dans le cursus d'un autre etablissement
is_external = db.Column(
db.Boolean, default=False, server_default="false", index=True
)
ue = db.relationship("UniteEns", lazy="select", uselist=False)
def __repr__(self):
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})"
class ScolarAutorisationInscription(db.Model):
"""Autorisation d'inscription dans un semestre"""
__tablename__ = "scolar_autorisation_inscription"
id = db.Column(db.Integer, primary_key=True)
autorisation_inscription_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
)
formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
# semestre ou on peut s'inscrire:
semestre_id = db.Column(db.Integer)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
origin_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
class ScolarEvent(db.Model):
"""Evenement dans le parcours scolaire d'un étudiant"""
__tablename__ = "scolar_events"
id = db.Column(db.Integer, primary_key=True)
event_id = db.synonym("id")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id"),
)
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)
ue_id = db.Column(
db.Integer,
db.ForeignKey("notes_ue.id"),
)
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
# 'ECHEC_SEM'
# 'UTIL_COMPENSATION'
event_type = db.Column(db.String(SHORT_STR_LEN))
# Semestre compensé par formsemestre_id:
comp_formsemestre_id = db.Column(
db.Integer,
db.ForeignKey("notes_formsemestre.id"),
)

View File

@ -87,7 +87,7 @@ def get_tags_latex(code_latex):
"""
if code_latex:
# changé par EV: était r"([\*]{2}[a-zA-Z0-9:éèàâêëïôöù]+[\*]{2})"
res = re.findall(r"([\*]{2}[^ \t\n\r\f\v\*]+[\*]{2})", code_latex)
res = re.findall(r"([\*]{2}[^\t\n\r\f\v\*]+[\*]{2})", code_latex)
return [tag[2:-2] for tag in res]
else:
return []

View File

@ -46,9 +46,12 @@ import io
import os
from zipfile import ZipFile
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable, SeqGenTable
import app.scodoc.sco_utils as scu
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours # sco_codes_parcours.NEXT -> sem suivant
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
@ -174,6 +177,8 @@ class JuryPE(object):
self.PARCOURSINFO_DICT = {} # Les parcours des étudiants
self.syntheseJury = {} # Le jury de synthèse
self.semestresDeScoDoc = sco_formsemestre.do_formsemestre_list()
# Calcul du jury PE
self.exe_calculs_juryPE(semBase)
self.synthetise_juryPE()
@ -317,12 +322,10 @@ class JuryPE(object):
etudiants = []
for sem in semsListe: # pour chacun des semestres de la liste
# nt = self.get_notes_d_un_semestre( sem['formsemestre_id'] )
nt = self.get_cache_notes_d_un_semestre(sem["formsemestre_id"])
# sco_cache.NotesTableCache.get( sem['formsemestre_id'])
etudiantsDuSemestre = (
nt.get_etudids()
) # nt.identdict.keys() # identification des etudiants du semestre
) # identification des etudiants du semestre
if pe_tools.PE_DEBUG:
pe_tools.pe_print(
@ -486,14 +489,14 @@ class JuryPE(object):
lastdate = max(sesdates) # date de fin de l'inscription la plus récente
# if PETable.AFFICHAGE_DEBUG_PE == True : pe_tools.pe_print(" derniere inscription = ", lastDateSem)
semestresDeScoDoc = sco_formsemestre.do_formsemestre_list()
if sonDernierSidValide is None:
# si l'étudiant n'a validé aucun semestre, les prend tous ? (à vérifier)
semestresSuperieurs = semestresDeScoDoc
semestresSuperieurs = self.semestresDeScoDoc
else:
semestresSuperieurs = [
sem
for sem in semestresDeScoDoc
for sem in self.semestresDeScoDoc
if sem["semestre_id"] > sonDernierSidValide
] # Semestre de rang plus élevé que son dernier sem valide
datesDesSemestresSuperieurs = [
@ -1127,9 +1130,10 @@ class JuryPE(object):
# ------------------------------------------------------------------------------------------------------------------
# ------------------------------------------------------------------------------------------------------------------
def get_cache_notes_d_un_semestre(self, formsemestre_id): # inutile en realité !
def get_cache_notes_d_un_semestre(self, formsemestre_id: int) -> NotesTableCompat:
"""Charge la table des notes d'un formsemestre"""
return sco_cache.NotesTableCache.get(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
return res_sem.load_formsemestre_results(formsemestre)
# ------------------------------------------------------------------------------------------------------------------

View File

@ -37,8 +37,13 @@ Created on Fri Sep 9 09:15:05 2016
"""
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.moduleimpls import ModuleImpl
from app.models.ues import UniteEns
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_tag_module
from app.pe import pe_tagtable
@ -51,7 +56,7 @@ class SemestreTag(pe_tagtable.TableTag):
- nt: le tableau de notes du semestre considéré
- nt.inscrlist: étudiants inscrits à ce semestre, par ordre alphabétique (avec demissions)
- nt.identdict: { etudid : ident }
- nt._modimpls : liste des moduleimpl { ... 'module_id', ...}
- liste des moduleimpl { ... 'module_id', ...}
Attributs supplémentaires :
- inscrlist/identdict: étudiants inscrits hors démissionnaires ou défaillants
@ -96,7 +101,11 @@ class SemestreTag(pe_tagtable.TableTag):
self.nt = notetable
# Les attributs hérités : la liste des étudiants
self.inscrlist = [etud for etud in self.nt.inscrlist if etud["etat"] == "I"]
self.inscrlist = [
etud
for etud in self.nt.inscrlist
if self.nt.get_etud_etat(etud["etudid"]) == "I"
]
self.identdict = {
etudid: ident
for (etudid, ident) in self.nt.identdict.items()
@ -106,12 +115,15 @@ class SemestreTag(pe_tagtable.TableTag):
# Les modules pris en compte dans le calcul des moyennes par tag => ceux des UE standards
self.modimpls = [
modimpl
for modimpl in self.nt._modimpls
if modimpl["ue"]["type"] == sco_codes_parcours.UE_STANDARD
for modimpl in self.nt.formsemestre.modimpls_sorted
if modimpl.module.ue.type == sco_codes_parcours.UE_STANDARD
] # la liste des modules (objet modimpl)
# self._modimpl_ids = [modimpl['moduleimpl_id'] for modimpl in self._modimpls] # la liste de id des modules (modimpl_id)
self.somme_coeffs = sum(
[modimpl["module"]["coefficient"] for modimpl in self.modimpls]
[
modimpl.module.coefficient
for modimpl in self.modimpls
if modimpl.module.coefficient is not None
]
)
# -----------------------------------------------------------------------------
@ -155,9 +167,9 @@ class SemestreTag(pe_tagtable.TableTag):
tagdict = {}
for modimpl in self.modimpls:
modimpl_id = modimpl["moduleimpl_id"]
modimpl_id = modimpl.id
# liste des tags pour le modimpl concerné:
tags = sco_tag_module.module_tag_list(modimpl["module_id"])
tags = sco_tag_module.module_tag_list(modimpl.module.id)
for (
tag
@ -171,17 +183,13 @@ class SemestreTag(pe_tagtable.TableTag):
# Ajout du modimpl au tagname considéré
tagdict[tagname][modimpl_id] = {
"module_id": modimpl["module_id"], # les données sur le module
"coeff": modimpl["module"][
"coefficient"
], # le coeff du module dans le semestre
"module_id": modimpl.module.id, # les données sur le module
"coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
"ponderation": ponderation, # la pondération demandée pour le tag sur le module
"module_code": modimpl["module"][
"code"
], # le code qui doit se retrouver à l'identique dans des ue capitalisee
"ue_id": modimpl["ue"]["ue_id"], # les données sur l'ue
"ue_code": modimpl["ue"]["ue_code"],
"ue_acronyme": modimpl["ue"]["acronyme"],
"module_code": modimpl.module.code, # le code qui doit se retrouver à l'identique dans des ue capitalisee
"ue_id": modimpl.module.ue.id, # les données sur l'ue
"ue_code": modimpl.module.ue.ue_code,
"ue_acronyme": modimpl.module.ue.acronyme,
}
return tagdict
@ -217,7 +225,9 @@ class SemestreTag(pe_tagtable.TableTag):
def get_moyennes_DUT(self):
"""Lit les moyennes DUT du semestre pour tous les étudiants
et les renvoie au même format que comp_MoyennesTag"""
return [(self.nt.moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()]
return [
(self.nt.etud_moy_gen[etudid], 1.0, etudid) for etudid in self.get_etudids()
]
# -----------------------------------------------------------------------------
def get_noteEtCoeff_modimpl(self, modimpl_id, etudid, profondeur=2):
@ -229,7 +239,7 @@ class SemestreTag(pe_tagtable.TableTag):
"""
(note, coeff_norm) = (None, None)
modimpl = get_moduleimpl(self.nt, modimpl_id) # Le module considéré
modimpl = get_moduleimpl(modimpl_id) # Le module considéré
if modimpl == None or profondeur < 0:
return (None, None)
@ -237,14 +247,14 @@ class SemestreTag(pe_tagtable.TableTag):
ue_capitalisees = self.get_ue_capitalisees(
etudid
) # les ue capitalisées des étudiants
ue_capitalisees_id = [
ue["ue_id"] for ue in ue_capitalisees
] # les id des ue capitalisées
ue_capitalisees_id = {
ue_cap["ue_id"] for ue_cap in ue_capitalisees
} # les id des ue capitalisées
# Si le module ne fait pas partie des UE capitalisées
if modimpl["module"]["ue_id"] not in ue_capitalisees_id:
if modimpl.module.ue.id not in ue_capitalisees_id:
note = self.nt.get_etud_mod_moy(modimpl_id, etudid) # lecture de la note
coeff = modimpl["module"]["coefficient"] # le coeff
coeff = modimpl.module.coefficient # le coeff
coeff_norm = (
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
) # le coeff normalisé
@ -255,29 +265,30 @@ class SemestreTag(pe_tagtable.TableTag):
self.nt, etudid, modimpl_id
) # la moyenne actuelle
# A quel semestre correspond l'ue capitalisée et quelles sont ses notes ?
# fid_prec = [ ue['formsemestre_id'] for ue in ue_capitalisees if ue['ue_id'] == modimpl['module']['ue_id'] ][0]
# semestre_id = modimpl['module']['semestre_id']
fids_prec = [
ue["formsemestre_id"]
for ue in ue_capitalisees
if ue["ue_code"] == modimpl["ue"]["ue_code"]
ue_cap["formsemestre_id"]
for ue_cap in ue_capitalisees
if ue_cap["ue_code"] == modimpl.module.ue.ue_code
] # and ue['semestre_id'] == semestre_id]
if len(fids_prec) > 0:
# => le formsemestre_id du semestre dont vient la capitalisation
fid_prec = fids_prec[0]
# Lecture des notes de ce semestre
nt_prec = sco_cache.NotesTableCache.get(
fid_prec
) # le tableau de note du semestre considéré
# le tableau de note du semestre considéré:
formsemestre_prec = FormSemestre.query.get_or_404(fid_prec)
nt_prec: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre_prec
)
# Y-a-t-il un module équivalent c'est à dire correspondant au même code (le module_id n'étant pas significatif en cas de changement de PPN)
modimpl_prec = [
module
for module in nt_prec._modimpls
if module["module"]["code"] == modimpl["module"]["code"]
modi
for modi in nt_prec.formsemestre.modimpls_sorted
if modi.module.code == modimpl.module.code
]
if len(modimpl_prec) > 0: # si une correspondance est trouvée
modprec_id = modimpl_prec[0]["moduleimpl_id"]
modprec_id = modimpl_prec[0].id
moy_ue_capitalisee = get_moy_ue_from_nt(nt_prec, etudid, modprec_id)
if (
moy_ue_capitalisee is None
@ -285,7 +296,7 @@ class SemestreTag(pe_tagtable.TableTag):
note = self.nt.get_etud_mod_moy(
modimpl_id, etudid
) # lecture de la note
coeff = modimpl["module"]["coefficient"] # le coeff
coeff = modimpl.module.coefficient # le coeff
coeff_norm = (
coeff / self.somme_coeffs if self.somme_coeffs != 0 else 0
) # le coeff normalisé
@ -299,10 +310,11 @@ class SemestreTag(pe_tagtable.TableTag):
return (note, coeff_norm)
# -----------------------------------------------------------------------------
def get_ue_capitalisees(self, etudid):
"""Renvoie la liste des ue_id effectivement capitalisées par un étudiant"""
# return [ ue for ue in self.nt.ue_capitalisees[etudid] if self.nt.get_etud_ue_status(etudid,ue['ue_id'])['is_capitalized'] ]
return self.nt.ue_capitalisees[etudid]
def get_ue_capitalisees(self, etudid) -> list[dict]:
"""Renvoie la liste des capitalisation effectivement capitalisées par un étudiant"""
if etudid in self.nt.validations.ue_capitalisees.index:
return self.nt.validations.ue_capitalisees.loc[[etudid]].to_dict("records")
return []
# -----------------------------------------------------------------------------
def get_listesNotesEtCoeffsTagEtudiant(self, tag, etudid):
@ -468,37 +480,27 @@ def comp_coeff_pond(coeffs, ponderations):
# -----------------------------------------------------------------------------
def get_moduleimpl(nt, modimpl_id):
"""Renvoie l'objet modimpl dont l'id est modimpl_id fourni dans la note table nt,
en utilisant l'attribut nt._modimpls"""
modimplids = [
modimpl["moduleimpl_id"] for modimpl in nt._modimpls
] # la liste de id des modules (modimpl_id)
if modimpl_id not in modimplids:
if SemestreTag.DEBUG:
log(
"SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
% (modimpl_id)
)
return None
return nt._modimpls[modimplids.index(modimpl_id)]
def get_moduleimpl(modimpl_id) -> dict:
"""Renvoie l'objet modimpl dont l'id est modimpl_id"""
modimpl = ModuleImpl.query.get(modimpl_id)
if modimpl:
return modimpl
if SemestreTag.DEBUG:
log(
"SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
% (modimpl_id)
)
return None
# **********************************************
def get_moy_ue_from_nt(nt, etudid, modimpl_id):
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve le module de modimpl_id
en partant du note table nt"""
mod = get_moduleimpl(nt, modimpl_id) # le module
indice = 0
while indice < len(nt._ues):
if (
nt._ues[indice]["ue_id"] == mod["module"]["ue_id"]
): # si les ue_id correspond
data = [
ligne for ligne in nt.T if ligne[-1] == etudid
] # les notes de l'étudiant
if data:
return data[0][indice + 1] # la moyenne à l'ue
else:
indice += 1
return None # si non trouvé
def get_moy_ue_from_nt(nt, etudid, modimpl_id) -> float:
"""Renvoie la moyenne de l'UE d'un etudid dans laquelle se trouve
le module de modimpl_id
"""
# ré-écrit
modimpl = get_moduleimpl(modimpl_id) # le module
ue_status = nt.get_etud_ue_status(etudid, modimpl.module.ue.id)
if ue_status is None:
return None
return ue_status["moy"]

View File

@ -117,15 +117,15 @@ class TableTag(object):
# -----------------------------------------------------------------------------------------------------------
def get_moy_from_stats(self, tag):
""" Renvoie la moyenne des notes calculées pour d'un tag donné"""
"""Renvoie la moyenne des notes calculées pour d'un tag donné"""
return self.statistiques[tag][0] if tag in self.statistiques else None
def get_min_from_stats(self, tag):
""" Renvoie la plus basse des notes calculées pour d'un tag donné"""
"""Renvoie la plus basse des notes calculées pour d'un tag donné"""
return self.statistiques[tag][1] if tag in self.statistiques else None
def get_max_from_stats(self, tag):
""" Renvoie la plus haute des notes calculées pour d'un tag donné"""
"""Renvoie la plus haute des notes calculées pour d'un tag donné"""
return self.statistiques[tag][2] if tag in self.statistiques else None
# -----------------------------------------------------------------------------------------------------------
@ -236,7 +236,7 @@ class TableTag(object):
return moystr
def str_res_d_un_etudiant(self, etudid, delim=";"):
"""Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique). """
"""Renvoie sur une ligne les résultats d'un étudiant à tous les tags (par ordre alphabétique)."""
return delim.join(
[self.str_resTag_d_un_etudiant(tag, etudid) for tag in self.get_all_tags()]
)
@ -256,7 +256,7 @@ class TableTag(object):
# -----------------------------------------------------------------------
def str_tagtable(self, delim=";", decimal_sep=","):
"""Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags. """
"""Renvoie une chaine de caractère listant toutes les moyennes, les rangs des étudiants pour tous les tags."""
entete = ["etudid", "nom", "prenom"]
for tag in self.get_all_tags():
entete += [titre + "_" + tag for titre in ["note", "rang", "nb_inscrit"]]

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
@ -253,13 +254,13 @@ class TF(object):
continue # allowed empty field, skip
# type
typ = descr.get("type", "string")
if val != "" and val != None:
if val != "" and val is not None:
# check only non-null values
if typ[:3] == "int":
try:
val = int(val)
self.values[field] = val
except:
except ValueError:
msg.append(
"La valeur du champ '%s' doit être un nombre entier" % field
)
@ -269,20 +270,24 @@ class TF(object):
try:
val = float(val.replace(",", ".")) # allow ,
self.values[field] = val
except:
except ValueError:
msg.append(
"La valeur du champ '%s' doit être un nombre" % field
)
ok = 0
if typ[:3] == "int" or typ == "float" or typ == "real":
if "min_value" in descr and val < descr["min_value"]:
if (
ok
and (typ[:3] == "int" or typ == "float" or typ == "real")
and val != ""
and val != None
):
if "min_value" in descr and self.values[field] < descr["min_value"]:
msg.append(
"La valeur (%d) du champ '%s' est trop petite (min=%s)"
% (val, field, descr["min_value"])
)
ok = 0
if "max_value" in descr and val > descr["max_value"]:
if "max_value" in descr and self.values[field] > descr["max_value"]:
msg.append(
"La valeur (%s) du champ '%s' est trop grande (max=%s)"
% (val, field, descr["max_value"])
@ -594,6 +599,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 +722,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,6 +28,46 @@
from operator import mul
import pprint
""" ANCIENS BONUS SPORT pour ScoDoc < 9.2 NON UTILISES A PARTIR DE 9.2 (voir comp/bonus_spo.py)
La fonction bonus_sport reçoit:
- notes_sport: la liste des notes des modules de sport et culture (une note par module
de l'UE de type sport/culture, toujours dans remise sur 20);
- coefs: un coef (float) pondérant chaque note (la plupart des bonus les ignorent);
- infos: dictionnaire avec des données pouvant être utilisées pour les calculs.
Ces données dépendent du type de formation.
infos = {
"moy" : la moyenne générale (float). 0. en BUT.
"sem" : {
"date_debut_iso" : "2010-08-01", # date de début de semestre
}
"moy_ues": {
ue_id : { # ue_status
"is_capitalized" : True|False,
"moy" : float, # moyenne d'UE prise en compte (peut-être capitalisée)
"sum_coefs": float, # > 0 si UE avec la moyenne calculée
"cur_moy_ue": float, # moyenne de l'UE (sans capitalisation))
}
}
}
Les notes passées sont:
- pour les formations classiques, la moyenne dans le module, calculée comme d'habitude
(moyenne pondérée des notes d'évaluations);
- pour le BUT: pareil, *en ignorant* les éventuels poids des évaluations. Le coefficient
de l'évaluation est pris en compte, mais pas les poids vers les UE.
Pour modifier les moyennes d'UE:
- modifier infos["moy_ues"][ue_id][["cur_moy_ue"]
et, seulement si l'UE n'est pas capitalisée, infos["moy_ues"][ue_id][["moy"]/
La valeur retournée est:
- formations classiques: ajoutée à la moyenne générale
- BUT: valeur multipliée par la somme des coefs modules sport ajoutée à chaque UE.
"""
def bonus_iutv(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT Villetaneuse
@ -52,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
@ -335,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
@ -416,6 +456,11 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None):
return bonus
def bonus_iutlemans(notes_sport, coefs, infos=None):
"fake: formule inutilisée en ScoDoc 9.2 mais doiut être présente"
return 0.0
def bonus_iutlr(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point

View File

@ -313,7 +313,7 @@ def sco_footer():
def html_sem_header(
title, sem=None, with_page_header=True, with_h2=True, page_title=None, **args
title, with_page_header=True, with_h2=True, page_title=None, **args
):
"Titre d'une page semestre avec lien vers tableau de bord"
# sem now unused and thus optional...

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

@ -25,7 +25,9 @@
#
##############################################################################
"""Calculs sur les notes et cache des resultats
"""Calculs sur les notes et cache des résultats
Ancien code ScoDoc 7 en cours de rénovation
"""
from operator import itemgetter
@ -102,7 +104,7 @@ def comp_ranks(T):
def get_sem_ues_modimpls(formsemestre_id, modimpls=None):
"""Get liste des UE du semestre (à partir des moduleimpls)
(utilisé quand on ne peut pas construire nt et faire nt.get_ues())
(utilisé quand on ne peut pas construire nt et faire nt.get_ues_stat_dict())
"""
if modimpls is None:
modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
@ -168,7 +170,8 @@ 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)
self.formsemestre_id = formsemestre_id
@ -200,7 +203,7 @@ class NotesTable:
self.inscrlist.sort(key=itemgetter("nomp"))
# { etudid : rang dans l'ordre alphabetique }
self.rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
self._rang_alpha = {e["etudid"]: i for i, e in enumerate(self.inscrlist)}
self.bonus = scu.DictDefault(defaultvalue=0)
# Notes dans les modules { moduleimpl_id : { etudid: note_moyenne_dans_ce_module } }
@ -294,7 +297,7 @@ class NotesTable:
for ue in self._ues:
is_cap[ue["ue_id"]] = ue_status[ue["ue_id"]]["is_capitalized"]
for modimpl in self.get_modimpls():
for modimpl in self.get_modimpls_dict():
val = self.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if is_cap[modimpl["module"]["ue_id"]]:
t.append("-c-")
@ -316,7 +319,7 @@ class NotesTable:
self.moy_min = self.moy_max = "NA"
# calcul rangs (/ moyenne generale)
self.rangs = comp_ranks(T)
self.etud_moy_gen_ranks = comp_ranks(T)
self.rangs_groupes = (
{}
@ -364,7 +367,7 @@ class NotesTable:
moy = -float(x[0])
except (ValueError, TypeError):
moy = 1000.0
return (moy, self.rang_alpha[x[-1]])
return (moy, self._rang_alpha[x[-1]])
def get_etudids(self, sorted=False):
if sorted:
@ -417,46 +420,17 @@ class NotesTable:
else:
return ' <font color="red">(%s)</font> ' % etat
def get_ues(self, filter_sport=False, filter_non_inscrit=False, etudid=None):
"""liste des ue, ordonnée par numero.
Si filter_non_inscrit, retire les UE dans lesquelles l'etudiant n'est
inscrit à aucun module.
def get_ues_stat_dict(self, filter_sport=False): # was get_ues()
"""Liste des UEs, ordonnée par numero.
Si filter_sport, retire les UE de type SPORT
"""
if not filter_sport and not filter_non_inscrit:
if not filter_sport:
return self._ues
if filter_sport:
ues_src = [ue for ue in self._ues if ue["type"] != UE_SPORT]
else:
ues_src = self._ues
if not filter_non_inscrit:
return ues_src
ues = []
for ue in ues_src:
if self.get_etud_ue_status(etudid, ue["ue_id"])["is_capitalized"]:
# garde toujours les UE capitalisees
has_note = True
else:
has_note = False
# verifie que l'etud. est inscrit a au moins un module de l'UE
# (en fait verifie qu'il a une note)
modimpls = self.get_modimpls(ue["ue_id"])
return [ue for ue in self._ues if ue["type"] != UE_SPORT]
for modi in modimpls:
moy = self.get_etud_mod_moy(modi["moduleimpl_id"], etudid)
try:
float(moy)
has_note = True
break
except:
pass
if has_note:
ues.append(ue)
return ues
def get_modimpls(self, ue_id=None):
"liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
def get_modimpls_dict(self, ue_id=None):
"Liste des modules pour une UE (ou toutes si ue_id==None), triés par matières."
if ue_id is None:
r = self._modimpls
else:
@ -522,7 +496,7 @@ class NotesTable:
Les moyennes d'UE ne tiennent pas compte des capitalisations.
"""
ues = self.get_ues()
ues = self.get_ues_stat_dict()
sum_moy = 0 # la somme des moyennes générales valides
nb_moy = 0 # le nombre de moyennes générales valides
for ue in ues:
@ -561,9 +535,9 @@ class NotesTable:
i = 0
for ue in ues:
i += 1
ue["nb_moy"] = len(ue["_notes"])
if ue["nb_moy"] > 0:
ue["moy"] = sum(ue["_notes"]) / ue["nb_moy"]
ue["nb_vals"] = len(ue["_notes"])
if ue["nb_vals"] > 0:
ue["moy"] = sum(ue["_notes"]) / ue["nb_vals"]
ue["max"] = max(ue["_notes"])
ue["min"] = min(ue["_notes"])
else:
@ -591,7 +565,7 @@ class NotesTable:
Si non inscrit, moy == 'NI' et sum_coefs==0
"""
assert ue_id
modimpls = self.get_modimpls(ue_id)
modimpls = self.get_modimpls_dict(ue_id)
nb_notes = 0 # dans cette UE
sum_notes = 0.0
sum_coefs = 0.0
@ -729,11 +703,12 @@ class NotesTable:
ue_status = {
'est_inscrit' : True si étudiant inscrit à au moins un module de cette UE
'moy' : moyenne, avec capitalisation eventuelle
'capitalized_ue_id' : id de l'UE capitalisée
'coef_ue' : coef de l'UE utilisé pour le calcul de la moyenne générale
(la somme des coefs des modules, ou le coef d'UE capitalisée,
ou encore le coef d'UE si l'option use_ue_coefs est active)
'cur_moy_ue' : moyenne de l'UE en cours (sans considérer de capitalisation)
'cur_coef_ue': coefficient de l'UE courante
'cur_coef_ue': coefficient de l'UE courante (inutilisé ?)
'is_capitalized' : True|False,
'ects_pot' : (float) nb de crédits ECTS qui seraient validés (sous réserve de validation par le jury),
'ects_pot_fond': 0. si UE non fondamentale, = ects_pot sinon,
@ -767,7 +742,7 @@ class NotesTable:
sem_ects_pot_fond = 0.0
sem_ects_pot_pro = 0.0
for ue in self.get_ues():
for ue in self.get_ues_stat_dict():
# - On calcule la moyenne d'UE courante:
if not block_computation:
mu = self.comp_etud_moy_ue(etudid, ue_id=ue["ue_id"], cnx=cnx)
@ -940,6 +915,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(
@ -953,14 +929,14 @@ class NotesTable:
return infos
def get_etud_moy_gen(self, etudid):
def get_etud_moy_gen(self, etudid): # -> float | str
"""Moyenne generale de cet etudiant dans ce semestre.
Prend en compte les UE capitalisées.
Si pas de notes: 'NA'
"""
return self.moy_gen[etudid]
def get_etud_moy_infos(self, etudid):
def get_etud_moy_infos(self, etudid): # XXX OBSOLETE
"""Infos sur moyennes"""
return self.etud_moy_infos[etudid]
@ -978,15 +954,18 @@ class NotesTable:
Return: True|False, message explicatif
"""
return self.parcours.check_barre_ues(
[self.get_etud_ue_status(etudid, ue["ue_id"]) for ue in self._ues]
)
ue_status_list = []
for ue in self._ues:
ue_status = self.get_etud_ue_status(etudid, ue["ue_id"])
if ue_status:
ue_status_list.append(ue_status)
return self.parcours.check_barre_ues(ue_status_list)
def get_table_moyennes_triees(self):
return self.T
def get_etud_rang(self, etudid) -> str:
return self.rangs.get(etudid, "999")
return self.etud_moy_gen_ranks.get(etudid, "999")
def get_etud_rang_group(self, etudid, group_id):
"""Returns rank of etud in this group and number of etuds in group.
@ -1036,7 +1015,10 @@ class NotesTable:
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"select etudid, code, assidu, compense_formsemestre_id, event_date from scolar_formsemestre_validation where formsemestre_id=%(formsemestre_id)s and ue_id is NULL;",
"""SELECT etudid, code, assidu, compense_formsemestre_id, event_date
FROM scolar_formsemestre_validation
WHERE formsemestre_id=%(formsemestre_id)s AND ue_id is NULL;
""",
{"formsemestre_id": self.formsemestre_id},
)
decisions_jury = {}
@ -1162,8 +1144,14 @@ class NotesTable:
"""
self.ue_capitalisees = scu.DictDefault(defaultvalue=[])
cnx = None
semestre_id = self.sem["semestre_id"]
for etudid in self.get_etudids():
capital = formsemestre_get_etud_capitalisation(self.sem, etudid)
capital = formsemestre_get_etud_capitalisation(
self.formation["id"],
semestre_id,
ndb.DateDMYtoISO(self.sem["date_debut"]),
etudid,
)
for ue_cap in capital:
# Si la moyenne d'UE n'avait pas été stockée (anciennes versions de ScoDoc)
# il faut la calculer ici et l'enregistrer
@ -1175,16 +1163,18 @@ class NotesTable:
nt_cap = sco_cache.NotesTableCache.get(
ue_cap["formsemestre_id"]
) # > UE capitalisees par un etud
moy_ue_cap = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])[
"moy"
]
ue_cap_status = nt_cap.get_etud_ue_status(etudid, ue_cap["ue_id"])
if ue_cap_status:
moy_ue_cap = ue_cap_status["moy"]
else:
moy_ue_cap = ""
ue_cap["moy_ue"] = moy_ue_cap
if (
isinstance(moy_ue_cap, float)
and moy_ue_cap >= self.parcours.NOTES_BARRE_VALID_UE
):
if not cnx:
cnx = ndb.GetDBConnexion(autocommit=False)
cnx = ndb.GetDBConnexion()
sco_parcours_dut.do_formsemestre_validate_ue(
cnx,
nt_cap,
@ -1329,11 +1319,7 @@ class NotesTable:
return self._evaluations_etats
def get_sem_evaluation_etat_list(self):
"""Liste des evaluations de ce semestre, avec leur etat"""
return self.get_evaluations_etats()
def get_mod_evaluation_etat_list(self, moduleimpl_id):
def get_mod_evaluation_etat_list(self, moduleimpl_id) -> list[dict]:
"""Liste des évaluations de ce module"""
return [
e
@ -1352,7 +1338,7 @@ class NotesTable:
# Rappel des épisodes précédents: T est une liste de liste
# Colonnes: 0 moy_gen, moy_ue1, ..., moy_ue_n, moy_mod1, ..., moy_mod_n, etudid
ues = self.get_ues() # incluant le(s) UE de sport
ues = self.get_ues_stat_dict() # incluant le(s) UE de sport
for t in self.T:
etudid = t[-1]
if etudid in results.etud_moy_gen: # evite les démissionnaires
@ -1367,4 +1353,4 @@ class NotesTable:
# re-trie selon la nouvelle moyenne générale:
self.T.sort(key=self._row_key)
# Remplace aussi le rang:
self.rangs = results.etud_moy_gen_ranks
self.etud_moy_gen_ranks = results.etud_moy_gen_ranks

View File

@ -479,7 +479,7 @@ def _get_abs_description(a, cursor=None):
)
if Mlist:
M = Mlist[0]
module += "%s " % M["module"]["code"]
module += "%s " % (M["module"]["code"] or "(module sans code)")
if desc:
return "(%s) %s" % (desc, module)

View File

@ -32,6 +32,10 @@ import datetime
from flask import url_for, g, request, abort
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import Identite, FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc import notesdb as ndb
from app.scodoc.scolog import logdb
@ -46,7 +50,6 @@ from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app import log
from app.scodoc.sco_exceptions import ScoValueError
@ -71,8 +74,8 @@ def doSignaleAbsence(
etudid: etudiant concerné. Si non spécifié, cherche dans
les paramètres de la requête courante.
"""
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
etudid = etud["etudid"]
etud = Identite.from_request(etudid)
if not moduleimpl_id:
moduleimpl_id = None
description_abs = description
@ -82,7 +85,7 @@ def doSignaleAbsence(
for jour in dates:
if demijournee == 2:
sco_abs.add_absence(
etudid,
etud.id,
jour,
False,
estjust,
@ -90,7 +93,7 @@ def doSignaleAbsence(
moduleimpl_id,
)
sco_abs.add_absence(
etudid,
etud.id,
jour,
True,
estjust,
@ -100,7 +103,7 @@ def doSignaleAbsence(
nbadded += 2
else:
sco_abs.add_absence(
etudid,
etud.id,
jour,
demijournee,
estjust,
@ -113,27 +116,30 @@ def doSignaleAbsence(
J = ""
else:
J = "NON "
M = ""
indication_module = ""
if moduleimpl_id and moduleimpl_id != "NULL":
mod = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
formsemestre_id = mod["formsemestre_id"]
nt = sco_cache.NotesTableCache.get(formsemestre_id)
ues = nt.get_ues(etudid=etudid)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = nt.get_ues_stat_dict()
for ue in ues:
modimpls = nt.get_modimpls(ue_id=ue["ue_id"])
modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
for modimpl in modimpls:
if modimpl["moduleimpl_id"] == moduleimpl_id:
M = "dans le module %s" % modimpl["module"]["code"]
indication_module = "dans le module %s" % (
modimpl["module"]["code"] or "(pas de code)"
)
H = [
html_sco_header.sco_header(
page_title="Signalement d'une absence pour %(nomprenom)s" % etud,
page_title=f"Signalement d'une absence pour {etud.nomprenom}",
),
"""<h2>Signalement d'absences</h2>""",
]
if dates:
H.append(
"""<p>Ajout de %d absences <b>%sjustifiées</b> du %s au %s %s</p>"""
% (nbadded, J, datedebut, datefin, M)
% (nbadded, J, datedebut, datefin, indication_module)
)
else:
H.append(
@ -142,11 +148,18 @@ def doSignaleAbsence(
)
H.append(
"""<ul><li><a href="SignaleAbsenceEtud?etudid=%(etudid)s">Autre absence pour <b>%(nomprenom)s</b></a></li>
<li><a href="CalAbs?etudid=%(etudid)s">Calendrier de ses absences</a></li>
</ul>
<hr>"""
% etud
f"""<ul>
<li><a href="{url_for("absences.SignaleAbsenceEtud",
scodoc_dept=g.scodoc_dept, etudid=etud.id
)}">Autre absence pour <b>{etud.nomprenom}</b></a>
</li>
<li><a href="{url_for("absences.CalAbs",
scodoc_dept=g.scodoc_dept, etudid=etud.id
)}">Calendrier de ses absences</a>
</li>
</ul>
<hr>
"""
)
H.append(sco_find_etud.form_search_etud())
H.append(html_sco_header.sco_footer())
@ -171,11 +184,12 @@ def SignaleAbsenceEtud(): # etudid implied
menu_module = ""
else:
formsemestre_id = etud["cursem"]["formsemestre_id"]
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = nt.get_ues_stat_dict()
require_module = sco_preferences.get_preference(
"abs_require_module", formsemestre_id
)
nt = sco_cache.NotesTableCache.get(formsemestre_id)
ues = nt.get_ues(etudid=etudid)
if require_module:
menu_module = """
<script type="text/javascript">
@ -200,13 +214,13 @@ def SignaleAbsenceEtud(): # etudid implied
menu_module += """<option value="" selected>(Module)</option>"""
for ue in ues:
modimpls = nt.get_modimpls(ue_id=ue["ue_id"])
modimpls = nt.get_modimpls_dict(ue_id=ue["ue_id"])
for modimpl in modimpls:
menu_module += (
"""<option value="%(modimpl_id)s">%(modname)s</option>\n"""
% {
"modimpl_id": modimpl["moduleimpl_id"],
"modname": modimpl["module"]["code"],
"modname": modimpl["module"]["code"] or "",
}
)
menu_module += """</select></p>"""
@ -952,10 +966,10 @@ def _tables_abs_etud(
ex.append(
f"""<a href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"]}</a>"""
">{mod["module"]["code"] or "(module sans code)"}</a>"""
)
else:
ex.append(mod["module"]["code"])
ex.append(mod["module"]["code"] or "(module sans code)")
if ex:
return ", ".join(ex)
return ""
@ -970,10 +984,10 @@ def _tables_abs_etud(
ex.append(
f"""<a href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=mod["moduleimpl_id"])}
">{mod["module"]["code"]}</a>"""
">{mod["module"]["code"] or '(module sans code)'}</a>"""
)
else:
ex.append(mod["module"]["code"])
ex.append(mod["module"]["code"] or "(module sans code)")
if ex:
return ", ".join(ex)
return ""

View File

@ -95,9 +95,12 @@ 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 import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
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
@ -125,8 +128,8 @@ APO_NEWLINE = "\r\n"
def _apo_fmt_note(note):
"Formatte une note pour Apogée (séparateur décimal: ',')"
if not note and isinstance(note, float):
return ""
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
# return ""
try:
val = float(note)
except ValueError:
@ -370,7 +373,9 @@ class ApoEtud(dict):
dict: with N, B, J, R keys, ou None si elt non trouvé
"""
etudid = self.etud["etudid"]
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if etudid not in nt.identdict:
return None # etudiant non inscrit dans ce semestre
@ -412,14 +417,14 @@ class ApoEtud(dict):
# Elements UE
decisions_ue = nt.get_etud_decision_ues(etudid)
for ue in nt.get_ues():
if code in ue["code_apogee"].split(","):
for ue in nt.get_ues_stat_dict():
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
if self.export_res_ues:
if decisions_ue and ue["ue_id"] in decisions_ue:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
return dict(
N=_apo_fmt_note(ue_status["moy"]),
N=_apo_fmt_note(ue_status["moy"] if ue_status else ""),
B=20,
J="",
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
@ -431,10 +436,12 @@ class ApoEtud(dict):
return VOID_APO_RES
# Elements Modules
modimpls = nt.get_modimpls()
modimpls = nt.get_modimpls_dict()
module_code_found = False
for modimpl in modimpls:
if code in modimpl["module"]["code_apogee"].split(","):
if modimpl["module"]["code_apogee"] and code in modimpl["module"][
"code_apogee"
].split(","):
n = nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if n != "NI" and self.export_res_modules:
return dict(N=_apo_fmt_note(n), B=20, J="", R="")
@ -474,7 +481,8 @@ class ApoEtud(dict):
# l'étudiant n'a pas de semestre courant ?!
log("comp_elt_annuel: etudid %s has no cur_sem" % etudid)
return VOID_APO_RES
cur_nt = sco_cache.NotesTableCache.get(cur_sem["formsemestre_id"])
cur_formsemestre = FormSemestre.query.get_or_404(cur_sem["formsemestre_id"])
cur_nt: NotesTableCompat = res_sem.load_formsemestre_results(cur_formsemestre)
cur_decision = cur_nt.get_etud_decision_sem(etudid)
if not cur_decision:
# pas de decision => pas de résultat annuel
@ -491,7 +499,10 @@ class ApoEtud(dict):
decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"])
autre_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"])
autre_formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
autre_nt: NotesTableCompat = res_sem.load_formsemestre_results(
autre_formsemestre
)
autre_decision = autre_nt.get_etud_decision_sem(etudid)
if not autre_decision:
# pas de decision dans l'autre => pas de résultat annuel
@ -552,7 +563,8 @@ class ApoEtud(dict):
# prend le plus recent avec decision
cur_sem = None
for sem in cur_sems:
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
decision = nt.get_etud_decision_sem(self.etud["etudid"])
if decision:
cur_sem = sem
@ -612,7 +624,8 @@ class ApoEtud(dict):
else:
autre_sem = None
for sem in autres_sems:
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
decision = nt.get_etud_decision_sem(self.etud["etudid"])
if decision:
autre_sem = sem
@ -945,15 +958,18 @@ class ApoData(object):
s.add(code)
continue
# associé à une UE:
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
for ue in nt.get_ues():
if code in ue["code_apogee"].split(","):
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
for ue in nt.get_ues_stat_dict():
if ue["code_apogee"] and code in ue["code_apogee"].split(","):
s.add(code)
continue
# associé à un module:
modimpls = nt.get_modimpls()
modimpls = nt.get_modimpls_dict()
for modimpl in modimpls:
if code in modimpl["module"]["code_apogee"].split(","):
if modimpl["module"]["code_apogee"] and code in modimpl["module"][
"code_apogee"
].split(","):
s.add(code)
continue
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))

View File

@ -405,7 +405,6 @@ def formsemestre_archive(formsemestre_id, group_ids=[]):
H = [
html_sco_header.html_sem_header(
"Archiver les PV et résultats du semestre",
sem=sem,
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
@ -524,7 +523,7 @@ def formsemestre_list_archives(formsemestre_id):
}
L.append(a)
H = [html_sco_header.html_sem_header("Archive des PV et résultats ", sem)]
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
if not L:
H.append("<p>aucune archive enregistrée</p>")
else:

View File

@ -130,21 +130,31 @@ BACS_SSP = {(t[0], t[1]): t[2:] for t in _BACS}
BACS_S = {t[0]: t[2:] for t in _BACS}
class Baccalaureat(object):
class Baccalaureat:
def __init__(self, bac, specialite=""):
self.bac = bac
self.specialite = specialite
self._abbrev, self._type = BACS_SSP.get((bac, specialite), (None, None))
self.bac = bac or ""
self.specialite = specialite or ""
self._abbrev, self._type = BACS_SSP.get(
(self.bac, self.specialite), (None, None)
)
# Parfois, la specialite commence par la serie: essaye
if self._type is None and specialite and specialite.startswith(bac):
specialite = specialite[len(bac) :].strip(" -")
self._abbrev, self._type = BACS_SSP.get((bac, specialite), (None, None))
if (
self._type is None
and self.specialite
and self.specialite.startswith(self.bac)
):
specialite = self.specialite[len(self.bac) :].strip(" -")
self._abbrev, self._type = BACS_SSP.get(
(self.bac, specialite), (None, None)
)
# Cherche la forme serie specialite
if self._type is None and specialite:
self._abbrev, self._type = BACS_S.get(bac + " " + specialite, (None, None))
self._abbrev, self._type = BACS_S.get(
self.bac + " " + specialite, (None, None)
)
# Cherche avec juste le bac, sans specialite
if self._type is None:
self._abbrev, self._type = BACS_S.get(bac, (None, None))
self._abbrev, self._type = BACS_S.get(self.bac, (None, None))
def abbrev(self):
"abbreviation for this bac"

View File

@ -43,11 +43,15 @@ 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
import app.scodoc.notesdb as ndb
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import html_sco_header
@ -136,13 +140,14 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
raise ValueError("invalid version code !")
prefs = sco_preferences.SemPreferences(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if not nt.get_etud_etat(etudid):
raise ScoValueError("Etudiant non inscrit à ce semestre")
I = scu.DictDefault(defaultvalue="")
I["etudid"] = etudid
I["formsemestre_id"] = formsemestre_id
I["sem"] = nt.sem
I["sem"] = formsemestre.get_infos_dict()
I["server_name"] = request.url_root
# Formation et parcours
@ -191,7 +196,9 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["decision_sem"] = ""
I.update(infos)
I["etud_etat_html"] = nt.get_etud_etat_html(etudid)
I["etud_etat_html"] = _get_etud_etat_html(
formsemestre.etuds_inscriptions[etudid].etat
)
I["etud_etat"] = nt.get_etud_etat(etudid)
I["filigranne"] = ""
I["demission"] = ""
@ -218,10 +225,10 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
] # deprecated / keep it for backward compat in templates
# --- Notes
ues = nt.get_ues()
modimpls = nt.get_modimpls()
ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
moy_gen = nt.get_etud_moy_gen(etudid)
I["nb_inscrits"] = len(nt.rangs)
I["nb_inscrits"] = len(nt.etud_moy_gen_ranks)
I["moy_gen"] = scu.fmt_note(moy_gen)
I["moy_min"] = scu.fmt_note(nt.moy_min)
I["moy_max"] = scu.fmt_note(nt.moy_max)
@ -261,37 +268,51 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
# notes en attente dans ce semestre
rang = scu.RANG_ATTENTE_STR
rang_gr = scu.DictDefault(defaultvalue=scu.RANG_ATTENTE_STR)
inscriptions_counts = nt.get_inscriptions_counts()
I["rang"] = rang
I["rang_gr"] = rang_gr
I["gr_name"] = gr_name
I["ninscrits_gr"] = ninscrits_gr
I["nbetuds"] = len(nt.rangs)
I["nb_demissions"] = nt.nb_demissions
I["nb_defaillants"] = nt.nb_defaillants
I["nbetuds"] = len(nt.etud_moy_gen_ranks)
I["nb_demissions"] = inscriptions_counts[scu.DEMISSION]
I["nb_defaillants"] = inscriptions_counts[scu.DEF]
if prefs["bul_show_rangs"]:
I["rang_nt"] = "%s / %d" % (
rang,
I["nbetuds"] - nt.nb_demissions - nt.nb_defaillants,
inscriptions_counts[scu.INSCRIT],
)
I["rang_txt"] = "Rang " + I["rang_nt"]
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"])
@ -330,29 +351,30 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
] = [] # modules de l'UE capitalisée (liste vide si pas capitalisée)
if ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
u["ue_descr_txt"] = "Capitalisée le %s" % ndb.DateISOtoDMY(
u["ue_descr_txt"] = "capitalisée le %s" % ndb.DateISOtoDMY(
ue_status["event_date"]
)
u[
"ue_descr_html"
] = '<a href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s" class="bull_link">%s</a>' % (
sem_origin["formsemestre_id"],
etudid,
sem_origin["titreannee"],
u["ue_descr_txt"],
)
# log('cap details %s' % ue_status['moy'])
] = f"""<a href="{ url_for( 'notes.formsemestre_bulletinetud',
scodoc_dept=g.scodoc_dept, formsemestre_id=sem_origin['formsemestre_id'], etudid=etudid)}"
title="{sem_origin['titreannee']}" class="bull_link"
>{u["ue_descr_txt"]} pouet</a>
"""
if ue_status["moy"] != "NA" and ue_status["formsemestre_id"]:
# detail des modules de l'UE capitalisee
nt_cap = sco_cache.NotesTableCache.get(
# détail des modules de l'UE capitalisée
formsemestre_cap = FormSemestre.query.get_or_404(
ue_status["formsemestre_id"]
) # > toutes notes
)
nt_cap: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre_cap
)
u["modules_capitalized"], _ = _ue_mod_bulletin(
etudid,
formsemestre_id,
ue_status["capitalized_ue_id"],
nt_cap.get_modimpls(),
nt_cap.get_modimpls_dict(),
nt_cap,
version,
)
@ -361,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],
@ -379,6 +401,7 @@ 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
# En #sco92, pas d'information
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
#
@ -389,6 +412,18 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
return C
def _get_etud_etat_html(etat: str) -> str:
"""chaine html représentant l'état (backward compat sco7)"""
if etat == scu.INSCRIT: # "I"
return ""
elif etat == scu.DEMISSION: # "D"
return ' <font color="red">(DEMISSIONNAIRE)</font> '
elif etat == scu.DEF: # "DEF"
return ' <font color="red">(DEFAILLANT)</font> '
else:
return ' <font color="red">(%s)</font> ' % etat
def _sort_mod_by_matiere(modlist, nt, etudid):
matmod = {} # { matiere_id : [] }
for mod in modlist:
@ -479,7 +514,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
)
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
mod["code"] = modimpl["module"]["code"]
mod["code_html"] = link_mod + mod["code"] + "</a>"
mod["code_html"] = link_mod + (mod["code"] or "") + "</a>"
else:
mod["code"] = mod["code_html"] = ""
mod["name"] = (
@ -497,7 +532,7 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
% (modimpl["moduleimpl_id"], mod_descr)
)
if sco_preferences.get_preference("bul_show_codemodules", formsemestre_id):
mod["code_txt"] = modimpl["module"]["code"]
mod["code_txt"] = modimpl["module"]["code"] or ""
mod["code_html"] = link_mod + mod["code_txt"] + "</a>"
else:
mod["code_txt"] = ""
@ -582,14 +617,22 @@ def _ue_mod_bulletin(etudid, formsemestre_id, ue_id, modimpls, nt, version):
e["note_txt"] = e["note_html"] = ""
e["coef_txt"] = scu.fmt_coef(e["coefficient"])
# Classement
if bul_show_mod_rangs and mod["mod_moy_txt"] != "-" and not is_malus:
if (
bul_show_mod_rangs
and (nt.mod_rangs is not None)
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

@ -32,13 +32,14 @@ import datetime
import json
from app.but import bulletin_but
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models.formsemestre import FormSemestre
from app.models.etudiants import Identite
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formsemestre
@ -90,17 +91,13 @@ def formsemestre_bulletinetud_published_dict(
etud = Identite.query.get(etudid)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if formsemestre.formation.is_apc():
nt = bulletin_but.APCNotesTableCompat(formsemestre)
else:
nt = sco_cache.NotesTableCache.get(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
d = {}
if (not sem["bul_hide_xml"]) or force_publishing:
published = 1
published = True
else:
published = 0
published = False
if xml_nodate:
docdate = ""
else:
@ -153,9 +150,9 @@ def formsemestre_bulletinetud_published_dict(
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
ues = nt.get_ues()
modimpls = nt.get_modimpls()
nbetuds = len(nt.rangs)
ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks)
mg = scu.fmt_note(nt.get_etud_moy_gen(etudid))
if (
nt.get_moduleimpls_attente()
@ -192,7 +189,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"] = []
@ -203,21 +202,22 @@ def formsemestre_bulletinetud_published_dict(
ects_txt = ""
else:
ects_txt = f"{ue['ects']:2.3g}"
rang, effectif = nt.get_etud_ue_rang(ue["ue_id"], etudid)
u = dict(
id=ue["ue_id"],
numero=scu.quote_xml_attr(ue["numero"]),
acronyme=scu.quote_xml_attr(ue["acronyme"]),
titre=scu.quote_xml_attr(ue["titre"]),
note=dict(
value=scu.fmt_note(ue_status["cur_moy_ue"]),
value=scu.fmt_note(ue_status["cur_moy_ue"] if ue_status else ""),
min=scu.fmt_note(ue["min"]),
max=scu.fmt_note(ue["max"]),
moy=scu.fmt_note(
ue["moy"]
), # CM : ajout pour faire apparaitre la moyenne des UE
),
rang=str(nt.ue_rangs[ue["ue_id"]][0][etudid]),
effectif=str(nt.ue_rangs[ue["ue_id"]][1]),
rang=rang,
effectif=effectif,
ects=ects_txt,
code_apogee=scu.quote_xml_attr(ue["code_apogee"]),
)
@ -254,7 +254,10 @@ def formsemestre_bulletinetud_published_dict(
m["note"][k] = scu.fmt_note(m["note"][k])
u["module"].append(m)
if sco_preferences.get_preference("bul_show_mod_rangs", formsemestre_id):
if (
sco_preferences.get_preference("bul_show_mod_rangs", formsemestre_id)
and nt.mod_rangs is not None
):
m["rang"] = dict(
value=nt.mod_rangs[modimpl["moduleimpl_id"]][0][etudid]
)

View File

@ -169,7 +169,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
)
)
# Contenu table: UE apres UE
# Contenu table: UE après UE
for ue in I["ues"]:
ue_descr = ue["ue_descr_html"]
coef_ue = ue["coef_ue_txt"]
@ -476,8 +476,8 @@ def _bulletin_pdf_table_legacy(I, version="long"):
else:
rang_minmax = mod["mod_rang_txt"] # vide si pas option rang
t = [
mod["code"],
mod["name"],
mod["code"] or "",
mod["name"] or "",
rang_minmax,
mod["mod_moy_txt"],
mod["mod_coef_txt"],

View File

@ -56,19 +56,24 @@ import time
import traceback
from pydoc import html
from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate
from reportlab.platypus.doctemplate import BaseDocTemplate
from flask import g, request
import app.scodoc.sco_utils as scu
from app import log, ScoValueError
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
from app.scodoc import sco_etud
import sco_version
from app.scodoc.sco_logos import find_logo
import app.scodoc.sco_utils as scu
import sco_version
def pdfassemblebulletins(
@ -141,7 +146,7 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
except:
log("process_field: invalid format=%s" % field)
text = (
"<para><i>format invalide !<i></para><para>"
"<para><i>format invalide !</i></para><para>"
+ traceback.format_exc()
+ "</para>"
)
@ -178,22 +183,21 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
if cached:
return cached[1], cached[0]
fragments = []
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# Make each bulletin
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etudids, get_sexnom
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
bookmarks = {}
filigrannes = {}
i = 1
for etudid in nt.get_etudids():
for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre_id,
etudid,
etud.id,
format="pdfpart",
version=version,
)
fragments += frag
filigrannes[i] = filigranne
bookmarks[i] = scu.suppress_accents(nt.get_sexnom(etudid))
bookmarks[i] = etud.sex_nom(no_accents=True)
i = i + 1
#
infos = {"DeptName": sco_preferences.get_preference("DeptName", formsemestre_id)}
@ -206,7 +210,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
pdfdoc = pdfassemblebulletins(
formsemestre_id,
fragments,
sem["titremois"],
formsemestre.titre_mois(),
infos,
bookmarks,
filigranne=filigrannes,
@ -216,7 +220,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
sco_pdf.PDFLOCK.release()
#
dt = time.strftime("%Y-%m-%d")
filename = "bul-%s-%s.pdf" % (sem["titre_num"], dt)
filename = "bul-%s-%s.pdf" % (formsemestre.titre_num(), dt)
filename = scu.unescape_html(filename).replace(" ", "_").replace("&", "")
# fill cache
sco_cache.SemBulletinsPDFCache.set(

View File

@ -430,7 +430,13 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
t = {
"titre": ue["acronyme"] + " " + ue["titre"],
"_titre_html": plusminus + ue["acronyme"] + " " + ue["titre"],
"_titre_html": plusminus
+ ue["acronyme"]
+ " "
+ ue["titre"]
+ ' <span class="bul_ue_descr">'
+ ue["ue_descr_txt"]
+ "</span>",
"_titre_help": ue["ue_descr_txt"],
"_titre_colspan": 2,
"module": ue_descr,
@ -465,9 +471,10 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
ects_txt = str(int(ue["ects"]))
except:
ects_txt = "-"
titre = f"{ue['acronyme'] or ''} {ue['titre'] or ''}"
t = {
"titre": ue["acronyme"] + " " + ue["titre"],
"_titre_html": minuslink + ue["acronyme"] + " " + ue["titre"],
"titre": titre,
"_titre_html": minuslink + titre,
"_titre_colspan": 2,
"module": ue["titre"],
"rang": ue_descr,
@ -619,7 +626,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
prefs=prefs,
)
if nbeval: # boite autour des evaluations (en pdf)
if nbeval: # boite autour des évaluations (en pdf)
P[-1]["_pdf_style"].append(
("BOX", (1, 1 - nbeval), (-1, 0), 0.2, self.PDF_LIGHT_GRAY)
)

View File

@ -44,6 +44,8 @@ import datetime
from xml.etree import ElementTree
from xml.etree.ElementTree import Element
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
@ -152,10 +154,11 @@ def make_xml_formsemestre_bulletinetud(
pid = partition["partition_id"]
partitions_etud_groups[pid] = sco_groups.get_etud_groups_in_partition(pid)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
ues = nt.get_ues()
modimpls = nt.get_modimpls()
nbetuds = len(nt.rangs)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = nt.get_ues_stat_dict()
modimpls = nt.get_modimpls_dict()
nbetuds = len(nt.etud_moy_gen_ranks)
mg = scu.fmt_note(nt.get_etud_moy_gen(etudid))
if (
nt.get_moduleimpls_attente()
@ -195,7 +198,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"])
@ -209,9 +217,9 @@ def make_xml_formsemestre_bulletinetud(
)
doc.append(x_ue)
if ue["type"] != sco_codes_parcours.UE_SPORT:
v = ue_status["cur_moy_ue"]
v = ue_status["cur_moy_ue"] if ue_status else ""
else:
v = nt.bonus[etudid]
v = nt.bonus[etudid] if nt.bonus is not None else 0.0
x_ue.append(
Element(
"note",
@ -225,8 +233,9 @@ def make_xml_formsemestre_bulletinetud(
except (ValueError, TypeError):
ects_txt = ""
x_ue.append(Element("ects", value=ects_txt))
x_ue.append(Element("rang", value=str(nt.ue_rangs[ue["ue_id"]][0][etudid])))
x_ue.append(Element("effectif", value=str(nt.ue_rangs[ue["ue_id"]][1])))
rang, effectif = nt.get_etud_ue_rang(ue["ue_id"], etudid)
x_ue.append(Element("rang", value=str(rang)))
x_ue.append(Element("effectif", value=str(effectif)))
# Liste les modules de l'UE
ue_modimpls = [mod for mod in modimpls if mod["module"]["ue_id"] == ue["ue_id"]]
for modimpl in ue_modimpls:
@ -243,7 +252,7 @@ def make_xml_formsemestre_bulletinetud(
x_mod = Element(
"module",
id=str(modimpl["moduleimpl_id"]),
code=str(mod["code"]),
code=str(mod["code"] or ""),
coefficient=str(mod["coefficient"]),
numero=str(mod["numero"]),
titre=scu.quote_xml_attr(mod["titre"]),
@ -262,7 +271,10 @@ def make_xml_formsemestre_bulletinetud(
moy=scu.fmt_note(modstat["moy"]),
)
)
if sco_preferences.get_preference("bul_show_mod_rangs", formsemestre_id):
if (
sco_preferences.get_preference("bul_show_mod_rangs", formsemestre_id)
and nt.mod_rangs is not None
):
x_mod.append(
Element(
"rang",

View File

@ -59,9 +59,9 @@ import traceback
from flask import g
from app import log
from app.scodoc import notesdb as ndb
from app.scodoc import sco_utils as scu
from app import log
CACHE = None # set in app.__init__.py
@ -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
@ -155,16 +156,6 @@ class EvaluationCache(ScoDocCache):
cls.delete_many(evaluation_ids)
class ResultatsSemestreBUTCache(ScoDocCache):
"""Cache pour les résultats ResultatsSemestreBUT.
Clé: formsemestre_id
Valeur: { un paquet de dataframes }
"""
prefix = "RBUT"
timeout = 1 * 60 # ttl 1 minutes (en phase de mise au point)
class AbsSemEtudCache(ScoDocCache):
"""Cache pour les comptes d'absences d'un étudiant dans un semestre.
Ce cache étant indépendant des semestres, le compte peut être faux lorsqu'on
@ -224,7 +215,9 @@ class NotesTableCache(ScoDocCache):
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 and compute is True, build it and cache it.
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"):
@ -299,7 +292,8 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
SemInscriptionsCache.delete_many(formsemestre_ids)
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
ResultatsSemestreBUTCache.delete_many(formsemestre_ids)
ResultatsSemestreCache.delete_many(formsemestre_ids)
ValidationsSemestreCache.delete_many(formsemestre_ids)
class DefferedSemCacheManager:
@ -322,3 +316,24 @@ class DefferedSemCacheManager:
while g.sem_to_invalidate:
formsemestre_id = g.sem_to_invalidate.pop()
invalidate_formsemestre(formsemestre_id)
# ---- Nouvelles classes ScoDoc 9.2
class ResultatsSemestreCache(ScoDocCache):
"""Cache pour les résultats ResultatsSemestre (notes et moyennes)
Clé: formsemestre_id
Valeur: { un paquet de dataframes }
"""
prefix = "RSEM"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
class ValidationsSemestreCache(ScoDocCache):
"""Cache pour les résultats de jury d'un semestre
Clé: formsemestre_id
Valeur: un paquet de DataFrames
"""
prefix = "VSC"
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)

View File

@ -141,7 +141,7 @@ BUG = "BUG"
ALL = "ALL"
# Explication des codes (de demestre ou d'UE)
# Explication des codes (de semestre ou d'UE)
CODES_EXPL = {
ADC: "Validé par compensation",
ADJ: "Validé par le Jury",
@ -154,6 +154,7 @@ CODES_EXPL = {
DEF: "Défaillant",
NAR: "Échec, non autorisé à redoubler",
RAT: "En attente d'un rattrapage",
DEM: "Démission",
}
# Nota: ces explications sont personnalisables via le fichier
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
@ -169,18 +170,18 @@ CODES_SEM_REO = {NAR: 1} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée
def code_semestre_validant(code):
def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre"
return CODES_SEM_VALIDES.get(code, False)
def code_semestre_attente(code):
def code_semestre_attente(code: str) -> bool:
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)"
return CODES_SEM_ATTENTES.get(code, False)
def code_ue_validant(code):
"Vrai si ce code entraine la validation de l'UE"
def code_ue_validant(code: str) -> bool:
"Vrai si ce code entraine la validation des UEs du semestre."
return CODES_UE_VALIDES.get(code, False)
@ -258,6 +259,7 @@ class TypeParcours(object):
) # par defaut, autorise tous les types d'UE
APC_SAE = False # Approche par compétences avec ressources et SAÉs
USE_REFERENTIEL_COMPETENCES = False # Lien avec ref. comp.
ECTS_FONDAMENTAUX_PER_YEAR = 0.0 # pour ISCID, deprecated
def check(self, formation=None):
return True, "" # status, diagnostic_message

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

@ -65,7 +65,7 @@ def formsemestre_table_estim_cost(
Mod = M["module"]
T.append(
{
"code": Mod["code"],
"code": Mod["code"] or "",
"titre": Mod["titre"],
"heures_cours": Mod["heures_cours"],
"heures_td": Mod["heures_td"] * n_group_td,

View File

@ -31,15 +31,17 @@ Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudi
import http
from flask import url_for, g, request
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.scolog import logdb
from app.scodoc.gen_tables import GenTable
from app.scodoc import safehtml
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
@ -115,7 +117,7 @@ def get_etudids_with_debouche(start_year):
def table_debouche_etudids(etudids, keep_numeric=True):
"""Rapport pour ces etudiants"""
"""Rapport pour ces étudiants"""
L = []
for etudid in etudids:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
@ -124,7 +126,8 @@ def table_debouche_etudids(etudids, keep_numeric=True):
es = [(s["date_fin_iso"], i) for i, s in enumerate(sems)]
imax = max(es)[1]
last_sem = sems[imax]
nt = sco_cache.NotesTableCache.get(last_sem["formsemestre_id"])
formsemestre = FormSemestre.query.get_or_404(last_sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
row = {
"etudid": etudid,
"civilite": etud["civilite"],

View File

@ -32,7 +32,8 @@ 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.models.validations 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

@ -36,6 +36,7 @@ from app import log
from app.models import SHORT_STR_LEN
from app.models.formations import Formation
from app.models.modules import Module
from app.models.ues import UniteEns
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
@ -340,6 +341,7 @@ def module_move(module_id, after=0, redirect=True):
db.session.add(module)
db.session.add(neigh)
db.session.commit()
module.formation.invalidate_cached_sems()
# redirect to ue_list page:
if redirect:
return flask.redirect(
@ -354,16 +356,18 @@ def module_move(module_id, after=0, redirect=True):
def ue_move(ue_id, after=0, redirect=1):
"""Move UE before/after previous one (decrement/increment numero)"""
o = sco_edit_ue.ue_list({"ue_id": ue_id})[0]
# log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after))
ue = UniteEns.query.get_or_404(ue_id)
redirect = int(redirect)
after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1):
raise ValueError('invalid value for "after"')
formation_id = o["formation_id"]
others = sco_edit_ue.ue_list({"formation_id": formation_id})
others = ue.formation.ues.order_by(UniteEns.numero).all()
if len({o.numero for o in others}) != len(others):
# il y a des numeros identiques !
scu.objects_renumber(db, others)
ue.formation.invalidate_cached_sems()
if len(others) > 1:
idx = [p["ue_id"] for p in others].index(ue_id)
idx = [u.id for u in others].index(ue.id)
neigh = None # object to swap with
if after == 0 and idx > 0:
neigh = others[idx - 1]
@ -371,20 +375,19 @@ def ue_move(ue_id, after=0, redirect=1):
neigh = others[idx + 1]
if neigh: #
# swap numero between partition and its neighbor
# log('moving ue %s (neigh #%s)' % (ue_id, neigh['numero']))
cnx = ndb.GetDBConnexion()
o["numero"], neigh["numero"] = neigh["numero"], o["numero"]
if o["numero"] == neigh["numero"]:
neigh["numero"] -= 2 * after - 1
sco_edit_ue._ueEditor.edit(cnx, o)
sco_edit_ue._ueEditor.edit(cnx, neigh)
ue.numero, neigh.numero = neigh.numero, ue.numero
db.session.add(ue)
db.session.add(neigh)
db.session.commit()
ue.formation.invalidate_cached_sems()
# redirect to ue_list page
if redirect:
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=o["formation_id"],
semestre_idx=o["semestre_idx"],
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
)
)

View File

@ -88,13 +88,14 @@ def do_matiere_create(args):
r = _matiereEditor.create(cnx, args)
# news
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
formation = Formation.query.get(ue["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=ue["formation_id"],
text="Modification de la formation %(acronyme)s" % F,
text="Modification de la formation {formation.acronyme}",
max_frequency=3,
)
formation.invalidate_cached_sems()
return r
@ -195,13 +196,14 @@ def do_matiere_delete(oid):
_matiereEditor.delete(cnx, oid)
# news
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
formation = Formation.query.get(ue["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=ue["formation_id"],
text="Modification de la formation %(acronyme)s" % F,
text="Modification de la formation {formation.acronyme}",
max_frequency=3,
)
formation.invalidate_cached_sems()
def matiere_delete(matiere_id=None):

View File

@ -41,7 +41,6 @@ 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.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
@ -105,32 +104,38 @@ def do_module_create(args) -> int:
r = _moduleEditor.create(cnx, args)
# news
F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0]
formation = Formation.query.get(args["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=args["formation_id"],
text="Modification de la formation %(acronyme)s" % F,
object=formation.id,
text=f"Modification de la formation {formation.acronyme}",
max_frequency=3,
)
formation.invalidate_cached_sems()
return r
def module_create(matiere_id=None, module_type=None, semestre_id=None):
"""Création d'un module"""
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
matiere = Matiere.query.get_or_404(matiere_id)
if matiere is None:
raise ScoValueError("invalid matiere !")
ue = matiere.ue
parcours = ue.formation.get_parcours()
def module_create(
matiere_id=None, module_type=None, semestre_id=None, formation_id=None
):
"""Formulaire de création d'un module
Si matiere_id est spécifié, le module sera créé dans cette matière (cas normal).
Sinon, donne le choix de l'UE de rattachement et utilise la première
matière de cette UE (si elle n'existe pas, la crée).
"""
if matiere_id:
matiere = Matiere.query.get_or_404(matiere_id)
ue = matiere.ue
formation = ue.formation
else:
formation = Formation.query.get_or_404(formation_id)
parcours = formation.get_parcours()
is_apc = parcours.APC_SAE
ues = ue.formation.ues.order_by(
ues = formation.ues.order_by(
UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme
).all()
# cherche le numero adéquat (pour placer le module en fin de liste)
modules = matiere.ue.formation.modules.all()
modules = formation.modules.all()
if modules:
default_num = max([m.numero or 0 for m in modules]) + 10
else:
@ -143,14 +148,16 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
H = [
html_sco_header.sco_header(page_title=f"Création {object_name}"),
]
if is_apc:
if not matiere_id:
H += [
f"""<h2>Création {object_name} dans la formation {ue.formation.acronyme}</h2>"""
f"""<h2>Création {object_name} dans la formation {formation.acronyme}
</h2>
"""
]
else:
H += [
f"""<h2>Création {object_name} dans la matière {matiere.titre},
(UE {ue.acronyme})</h2>
(UE {ue.acronyme}), semestre {ue.semestre_idx}</h2>
"""
]
@ -158,7 +165,6 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
ue=ue,
semestre_id=semestre_id,
)
]
@ -170,7 +176,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"size": 10,
"explanation": "code du module, ressource ou SAÉ. Exemple M1203, R2.01, ou SAÉ 3.4. Ce code doit être unique dans la formation.",
"allow_null": False,
"validator": lambda val, field, formation_id=ue.formation_id: check_module_code_unicity(
"validator": lambda val, field, formation_id=formation_id: check_module_code_unicity(
val, field, formation_id
),
},
@ -190,36 +196,16 @@ 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],
},
),
]
if is_apc:
module_types = scu.ModuleType # tous les types
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,
},
),
]
# ne propose pas SAE et Ressources:
module_types = set(scu.ModuleType) - {
scu.ModuleType.RESSOURCE,
scu.ModuleType.SAE,
}
descr += [
(
"module_type",
@ -227,8 +213,8 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
},
),
(
@ -284,11 +270,33 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
),
]
if matiere_id:
descr += [
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere_id, "input_type": "hidden"}),
]
else:
# choix de l'UE de rattachement
descr += [
(
"ue_id",
{
"input_type": "menu",
"type": "int",
"title": "UE de rattachement",
"explanation": "utilisée notamment pour les malus",
"labels": [
f"S{u.semestre_idx if u.semestre_idx is not None else '.'} / {u.acronyme} {u.titre}"
for u in ues
],
"allowed_values": [u.id for u in ues],
},
),
]
descr += [
# ('ects', { 'size' : 4, 'type' : 'float', 'title' : 'ECTS', 'explanation' : 'nombre de crédits ECTS (inutilisés: les crédits sont associés aux UE)' }),
("formation_id", {"default": ue.formation_id, "input_type": "hidden"}),
("ue_id", {"default": ue.id, "input_type": "hidden"}),
("matiere_id", {"default": matiere.id, "input_type": "hidden"}),
("formation_id", {"default": formation.id, "input_type": "hidden"}),
(
"code_apogee",
{
@ -318,12 +326,21 @@ 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:
if not matiere_id:
# formulaire avec choix UE de rattachement
ue = UniteEns.query.get(tf[2]["ue_id"])
if ue is None:
raise ValueError("UE invalide")
tf[2]["semestre_id"] = selected_ue.semestre_idx
matiere = ue.matieres.first()
if matiere:
tf[2]["matiere_id"] = matiere.id
else:
matiere_id = sco_edit_matiere.do_matiere_create(
{"ue_id": ue.id, "titre": ue.titre, "numero": 1},
)
tf[2]["matiere_id"] = matiere_id
tf[2]["semestre_id"] = ue.semestre_idx
_ = do_module_create(tf[2])
@ -331,7 +348,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
formation_id=formation.id,
semestre_idx=tf[2]["semestre_id"],
)
)
@ -378,13 +395,14 @@ def do_module_delete(oid):
_moduleEditor.delete(cnx, oid)
# news
F = sco_formations.formation_list(args={"formation_id": mod["formation_id"]})[0]
formation = module.formation
sco_news.add(
typ=sco_news.NEWS_FORM,
object=mod["formation_id"],
text="Modification de la formation %(acronyme)s" % F,
text=f"Modification de la formation {formation.acronyme}",
max_frequency=3,
)
formation.invalidate_cached_sems()
def module_delete(module_id=None):
@ -433,8 +451,6 @@ def module_delete(module_id=None):
def do_module_edit(vals: dict) -> None:
"edit a module"
from app.scodoc import sco_edit_formation
# check
mod = module_list({"module_id": vals["module_id"]})[0]
if module_is_locked(mod["module_id"]):
@ -476,31 +492,21 @@ def module_edit(module_id=None):
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
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:
# matières du même semestre seulement
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
AND ue.semestre_idx = %(semestre_idx)s
ORDER BY ue.numero, mat.numero
""",
{"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx},
)
# 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:
# matières de la formation
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]
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))
@ -523,19 +529,22 @@ def module_edit(module_id=None):
formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
).all(),
)
.order_by(FormSemestre.date_debut)
.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>"""
)
if is_apc:
module_types = scu.ModuleType # tous les types
else:
# 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}
descr = [
(
@ -557,18 +566,24 @@ def module_edit(module_id=None):
"input_type": "menu",
"title": "Type",
"explanation": "",
"labels": [x.name.capitalize() for x in scu.ModuleType],
"allowed_values": [str(int(x)) for x in scu.ModuleType],
"labels": [x.name.capitalize() for x in module_types],
"allowed_values": [str(int(x)) for x in module_types],
"enabled": unlocked,
},
),
(
"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",
@ -577,6 +592,7 @@ def module_edit(module_id=None):
(
"heures_tp",
{
"title": "Heures TP :",
"size": 4,
"type": "float",
"explanation": "nombre d'heures de Travaux Pratiques",
@ -584,9 +600,11 @@ def module_edit(module_id=None):
),
]
if is_apc:
coefs_descr = a_module.ue_coefs_descr()
if coefs_descr:
coefs_descr_txt = ", ".join(["%s: %s" % x for x in coefs_descr])
coefs_lst = a_module.ue_coefs_list()
if coefs_lst:
coefs_descr_txt = ", ".join(
[f"{ue.acronyme}: {c}" for (ue, c) in coefs_lst]
)
else:
coefs_descr_txt = """<span class="missing_value">non définis</span>"""
descr += [
@ -594,9 +612,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)",
},
)
]
@ -622,7 +640,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,
@ -694,6 +719,7 @@ def module_edit(module_id=None):
initvalues=module,
submitlabel="Modifier ce module",
)
#
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
@ -706,15 +732,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:
# pas changer de semestre un module utilisé !
raise ScoValueError(
"Module utilisé: il ne peut pas être changé de semestre !"
)
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"])
@ -815,7 +843,7 @@ def formation_add_malus_modules(formation_id, titre=None, redirect=True):
[
mod
for mod in module_list(args={"ue_id": ue["ue_id"]})
if mod["module_type"] == ModuleType.MALUS
if mod["module_type"] == scu.ModuleType.MALUS
]
)
if nb_mod_malus == 0:
@ -867,7 +895,7 @@ def ue_add_malus_module(ue_id, titre=None, code=None):
"matiere_id": matiere_id,
"formation_id": ue["formation_id"],
"semestre_id": semestre_id,
"module_type": ModuleType.MALUS,
"module_type": scu.ModuleType.MALUS,
},
)

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={
@ -125,13 +128,14 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs()
# news
F = sco_formations.formation_list(args={"formation_id": args["formation_id"]})[0]
formation = Formation.query.get(args["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=args["formation_id"],
text="Modification de la formation %(acronyme)s" % F,
text="Modification de la formation {formation.acronyme}",
max_frequency=3,
)
formation.invalidate_cached_sems()
return ue_id
@ -228,27 +232,35 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
return None
def ue_create(formation_id=None):
"""Creation d'une UE"""
return ue_edit(create=True, formation_id=formation_id)
def ue_create(formation_id=None, default_semestre_idx=None):
"""Formulaire création d'une UE"""
return ue_edit(
create=True,
formation_id=formation_id,
default_semestre_idx=default_semestre_idx,
)
def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou création d'une UE"""
def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=None):
"""Formulaire modification ou création d'une UE"""
create = int(create)
if not create:
U = ue_list(args={"ue_id": ue_id})
if not U:
raise ScoValueError("UE inexistante !")
U = U[0]
formation_id = U["formation_id"]
title = "Modification de l'UE %(titre)s" % U
initvalues = U
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
ue_dict = ue.to_dict()
formation_id = ue.formation_id
title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
initvalues = ue_dict
submitlabel = "Modifier les valeurs"
can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None)
else:
ue = None
title = "Création d'une UE"
initvalues = {}
initvalues = {
"semestre_idx": default_semestre_idx,
"color": ue_guess_color_default(formation_id, default_semestre_idx),
}
submitlabel = "Créer cette UE"
can_change_semestre_id = True
formation = Formation.query.get(formation_id)
if not formation:
raise ScoValueError(f"Formation inexistante ! (id={formation_id})")
@ -275,7 +287,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
ue_types_names = [sco_codes_parcours.UE_TYPE_NAME[k] for k in ue_types]
ue_types = [str(x) for x in ue_types]
fw = [
form_descr = [
("ue_id", {"input_type": "hidden"}),
("create", {"input_type": "hidden", "default": create}),
("formation_id", {"input_type": "hidden", "default": formation_id}),
@ -289,18 +301,28 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
"type": "int",
},
),
(
"semestre_idx",
{
"input_type": "menu",
"type": "int",
"allow_null": False,
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de l'UE dans la formation" % parcours.SESSION_NAME,
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
"allowed_values": [""] + semestres_indices,
},
),
]
if can_change_semestre_id:
form_descr += [
(
"semestre_idx",
{
"input_type": "menu",
"type": "int",
"allow_null": False,
"title": parcours.SESSION_NAME.capitalize(),
"explanation": "%s de l'UE dans la formation"
% parcours.SESSION_NAME,
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
"allowed_values": [""] + semestres_indices,
},
),
]
else:
form_descr += [
("semestre_idx", {"default": ue.semestre_idx, "input_type": "hidden"}),
]
form_descr += [
(
"type",
{
@ -330,7 +352,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,
},
),
(
@ -338,7 +362,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
{
"size": 12,
"title": "Code UE",
"explanation": "code interne (optionnel). Toutes les UE partageant le même code (et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE). Voir liste ci-dessous.",
"explanation": "code interne (non vide). Toutes les UE partageant le même code (et le même code de formation) sont compatibles (compensation de semestres, capitalisation d'UE). Voir liste ci-dessous.",
},
),
(
@ -358,9 +382,17 @@ 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(
form_descr.append(
(
"create_matiere",
{
@ -374,14 +406,33 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
fw,
form_descr,
initvalues=initvalues,
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()
if ue and ue.modules.count() and ue.semestre_idx is not None:
modules_div = f"""<div id="ue_list_modules">
<div><b>{ue.modules.count()} modules sont rattachés
à cette UE</b> du semestre S{ue.semestre_idx},
elle ne peut donc pas être changée de semestre.</div>
<ul>"""
for m in ue.modules:
modules_div += f"""<li><a class="stdlink" href="{url_for(
"notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre}</a></li>"""
modules_div += """</ul></div>"""
else:
modules_div = ""
bonus_div = """<div id="bonus_description"></div>"""
ue_div = """<div id="ue_list_code"></div>"""
return (
"\n".join(H)
+ tf[1]
+ modules_div
+ bonus_div
+ ue_div
+ html_sco_header.sco_footer()
)
else:
if create:
if not tf[2]["ue_code"]:
@ -525,14 +576,23 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
semestre_ids = range(1, parcours.NB_SEM + 1)
# transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
# basées sur des dicts
ues_obj = UniteEns.query.filter_by(formation_id=formation_id, is_external=False)
ues_obj = UniteEns.query.filter_by(
formation_id=formation_id, is_external=False
).order_by(UniteEns.semestre_idx, UniteEns.numero)
ues_externes_obj = UniteEns.query.filter_by(
formation_id=formation_id, is_external=True
)
if is_apc:
# pour faciliter la transition des anciens programmes non APC
# 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
if ue.matieres.count() < 1:
mat = Matiere(ue_id=ue.id)
db.session.add(mat)
# donne des couleurs aux UEs crées avant
colorie_anciennes_ues(ues_obj)
db.session.commit()
ues = [ue.to_dict() for ue in ues_obj]
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
@ -635,12 +695,16 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
descr_refcomp = ""
msg_refcomp = "associer à un référentiel de compétences"
else:
descr_refcomp = f"{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}"
descr_refcomp = f"""Référentiel de compétences:
<a href="{url_for('notes.refcomp_show',
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
</a> """
msg_refcomp = "changer"
H.append(
f"""
<ul>
<li>{descr_refcomp} <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
<li>{descr_refcomp}&nbsp; <a class="stdlink" href="{url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation_id)
}">{msg_refcomp}</a>
</li>
@ -975,6 +1039,7 @@ def _ue_table_matieres(
H.append(
_ue_table_modules(
parcours,
ue,
mat,
modules,
editable,
@ -1008,6 +1073,7 @@ def _ue_table_matieres(
def _ue_table_modules(
parcours,
ue,
mat,
modules,
editable,
@ -1098,8 +1164,12 @@ def _ue_table_modules(
tag_cls,
",".join(sco_tag_module.module_tag_list(mod["module_id"])),
)
if ue["semestre_idx"] is not None and mod["semestre_id"] != ue["semestre_idx"]:
warning_semestre = ' <span class="red">incohérent ?</span>'
else:
warning_semestre = ""
H.append(
" %s %s" % (parcours.SESSION_NAME, mod["semestre_id"])
" %s %s%s" % (parcours.SESSION_NAME, mod["semestre_id"], warning_semestre)
+ " (%s)" % heurescoef
+ tag_edit
)
@ -1337,7 +1407,7 @@ def formation_table_recap(formation_id, format="html"):
return tab.make_page(format=format)
def ue_list_semestre_ids(ue):
def ue_list_semestre_ids(ue: dict):
"""Liste triée des numeros de semestres des modules dans cette UE
Il est recommandable que tous les modules d'une UE aient le même indice de semestre.
Mais cela n'a pas toujours été le cas dans les programmes pédagogiques officiels,
@ -1345,3 +1415,45 @@ def ue_list_semestre_ids(ue):
"""
modules = sco_edit_module.module_list(args={"ue_id": ue["ue_id"]})
return sorted(list(set([mod["semestre_id"] for mod in modules])))
UE_PALETTE = [
"#B80004", # rouge
"#F97B3D", # Orange Crayola
"#FEB40B", # Honey Yellow
"#80CB3F", # Yellow Green
"#05162E", # Oxford Blue
"#548687", # Steel Teal
"#444054", # Independence
"#889696", # Spanish Gray
"#0CA4A5", # Viridian Green
]
def colorie_anciennes_ues(ues: list[UniteEns]) -> None:
"""Avant ScoDoc 9.2, les ue n'avaient pas de couleurs
Met des défauts raisonnables
"""
nb_colors = len(UE_PALETTE)
index = 0
last_sem_idx = 0
for ue in ues:
if ue.semestre_idx != last_sem_idx:
index = 0
last_sem_idx = ue.semestre_idx
if ue.color is None:
ue.color = UE_PALETTE[index % nb_colors]
index += 1
db.session.add(ue)
def ue_guess_color_default(formation_id: int, default_semestre_idx: int) -> str:
"""Un code couleur pour une nouvelle UE dans ce semestre"""
nb_colors = len(UE_PALETTE)
# UE existantes dans ce semestre:
nb_ues = UniteEns.query.filter(
UniteEns.formation_id == formation_id,
UniteEns.semestre_idx == default_semestre_idx,
).count()
index = nb_ues
return UE_PALETTE[index % nb_colors]

View File

@ -384,8 +384,8 @@ print apo_csv_list_stored_archives()
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
nt = sco_cache.NotesTableCache.get( formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
#
s = SemSet('NSS29902')
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)

View File

@ -38,7 +38,8 @@ from flask_mail import Message
from app import email
from app import log
from app.models import Admission
from app.models.etudiants import make_etud_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
@ -87,6 +88,8 @@ def force_uppercase(s):
def format_nomprenom(etud, reverse=False):
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
Si reverse, "Dupont Pierre", sans civilité.
DEPRECATED: utiliser Identite.nomprenom
"""
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
prenom = format_prenom(etud["prenom"])
@ -99,7 +102,9 @@ def format_nomprenom(etud, reverse=False):
def format_prenom(s):
"Formatte prenom etudiant pour affichage"
"""Formatte prenom etudiant pour affichage
DEPRECATED: utiliser Identite.prenom_str
"""
if not s:
return ""
frags = s.split()
@ -590,35 +595,6 @@ etudident_edit = _etudidentEditor.edit
etudident_create = _etudidentEditor.create
def make_etud_args(etudid=None, code_nip=None, use_request=True, raise_exc=True):
"""forme args dict pour requete recherche etudiant
On peut specifier etudid
ou bien (si use_request) cherche dans la requete http: etudid, code_nip, code_ine
(dans cet ordre).
"""
args = None
if etudid:
args = {"etudid": etudid}
elif code_nip:
args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global)
if request.method == "POST":
vals = request.form
elif request.method == "GET":
vals = request.args
else:
vals = {}
if "etudid" in vals:
args = {"etudid": int(vals["etudid"])}
elif "code_nip" in vals:
args = {"code_nip": str(vals["code_nip"])}
elif "code_ine" in vals:
args = {"code_ine": str(vals["code_ine"])}
if not args and raise_exc:
raise ValueError("getEtudInfo: no parameter !")
return args
def log_unknown_etud():
"""Log request: cas ou getEtudInfo n'a pas ramene de resultat"""
etud_args = make_etud_args(raise_exc=False)
@ -884,19 +860,24 @@ def list_scolog(etudid):
return cursor.dictfetchall()
def fill_etuds_info(etuds):
def fill_etuds_info(etuds, add_admission=True):
"""etuds est une liste d'etudiants (mappings)
Pour chaque etudiant, ajoute ou formatte les champs
-> informations pour fiche etudiant ou listes diverses
"""
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
Si add_admission: ajoute au dict le schamps "admission" s'il n'y sont pas déjà.
"""
cnx = ndb.GetDBConnexion()
# open('/tmp/t','w').write( str(etuds) )
for etud in etuds:
etudid = etud["etudid"]
etud["dept"] = g.scodoc_dept
# Admission
if add_admission and "nomlycee" not in etud:
admission = (
Admission.query.filter_by(etudid=etudid).first().to_dict(no_nulls=True)
)
etud.update(admission)
#
adrs = adresse_list(cnx, {"etudid": etudid})
if not adrs:
# certains "vieux" etudiants n'ont pas d'adresse
@ -909,79 +890,37 @@ def fill_etuds_info(etuds):
etud.update(adr)
format_etud_ident(etud)
# Semestres dans lesquel il est inscrit
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
{"etudid": etudid}
)
etud["ins"] = ins
sems = []
cursem = None # semestre "courant" ou il est inscrit
for i in ins:
sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"])
if sco_formsemestre.sem_est_courant(sem):
cursem = sem
curi = i
sem["ins"] = i
sems.append(sem)
# trie les semestres par date de debut, le plus recent d'abord
# (important, ne pas changer (suivi cohortes))
sems.sort(key=itemgetter("dateord"), reverse=True)
etud["sems"] = sems
etud["cursem"] = cursem
if cursem:
etud["inscription"] = cursem["titremois"]
etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"]
etud["inscription_formsemestre_id"] = cursem["formsemestre_id"]
etud["etatincursem"] = curi["etat"]
etud["situation"] = descr_situation_etud(etudid, etud["ne"])
# XXX est-ce utile ? sco_groups.etud_add_group_infos( etud, cursem)
else:
if etud["sems"]:
if etud["sems"][0]["dateord"] > time.strftime(
"%Y-%m-%d", time.localtime()
):
etud["inscription"] = "futur"
etud["situation"] = "futur élève"
else:
etud["inscription"] = "ancien"
etud["situation"] = "ancien élève"
else:
etud["inscription"] = "non inscrit"
etud["situation"] = etud["inscription"]
etud["inscriptionstr"] = etud["inscription"]
etud["inscription_formsemestre_id"] = None
# XXXetud['partitions'] = {} # ne va pas chercher les groupes des anciens semestres
etud["etatincursem"] = "?"
etud.update(etud_inscriptions_infos(etudid, etud["ne"]))
# nettoyage champs souvents vides
if etud["nomlycee"]:
# nettoyage champs souvent vides
if etud.get("nomlycee"):
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
if etud["villelycee"]:
etud["ilycee"] += " (%s)" % etud["villelycee"]
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
etud["ilycee"] += "<br/>"
else:
if etud["codelycee"]:
if etud.get("codelycee"):
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
else:
etud["ilycee"] = ""
rap = ""
if etud["rapporteur"] or etud["commentaire"]:
if etud.get("rapporteur") or etud.get("commentaire"):
rap = "Note du rapporteur"
if etud["rapporteur"]:
if etud.get("rapporteur"):
rap += " (%s)" % etud["rapporteur"]
rap += ": "
if etud["commentaire"]:
if etud.get("commentaire"):
rap += "<em>%s</em>" % etud["commentaire"]
etud["rap"] = rap
# if etud['boursier_prec']:
# pass
if etud["telephone"]:
if etud.get("telephone"):
etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
else:
etud["telephonestr"] = ""
if etud["telephonemobile"]:
if etud.get("telephonemobile"):
etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
etud["telephonemobile"]
)
@ -989,8 +928,56 @@ def fill_etuds_info(etuds):
etud["telephonemobilestr"] = ""
def descr_situation_etud(etudid, ne=""):
"""chaine decrivant la situation actuelle de l'etudiant"""
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
"""Dict avec les informations sur les semestres passés et courant"""
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
etud = {}
# Semestres dans lesquel il est inscrit
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
{"etudid": etudid}
)
etud["ins"] = ins
sems = []
cursem = None # semestre "courant" ou il est inscrit
for i in ins:
sem = sco_formsemestre.get_formsemestre(i["formsemestre_id"])
if sco_formsemestre.sem_est_courant(sem):
cursem = sem
curi = i
sem["ins"] = i
sems.append(sem)
# trie les semestres par date de debut, le plus recent d'abord
# (important, ne pas changer (suivi cohortes))
sems.sort(key=itemgetter("dateord"), reverse=True)
etud["sems"] = sems
etud["cursem"] = cursem
if cursem:
etud["inscription"] = cursem["titremois"]
etud["inscriptionstr"] = "Inscrit en " + cursem["titremois"]
etud["inscription_formsemestre_id"] = cursem["formsemestre_id"]
etud["etatincursem"] = curi["etat"]
etud["situation"] = descr_situation_etud(etudid, ne)
else:
if etud["sems"]:
if etud["sems"][0]["dateord"] > time.strftime("%Y-%m-%d", time.localtime()):
etud["inscription"] = "futur"
etud["situation"] = "futur élève"
else:
etud["inscription"] = "ancien"
etud["situation"] = "ancien élève"
else:
etud["inscription"] = "non inscrit"
etud["situation"] = etud["inscription"]
etud["inscriptionstr"] = etud["inscription"]
etud["inscription_formsemestre_id"] = None
etud["etatincursem"] = "?"
return etud
def descr_situation_etud(etudid: int, ne="") -> str:
"""chaîne décrivant la situation actuelle de l'étudiant"""
from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()
@ -1007,7 +994,7 @@ def descr_situation_etud(etudid, ne=""):
)
r = cursor.dictfetchone()
if not r:
situation = "non inscrit"
situation = "non inscrit" + ne
else:
sem = sco_formsemestre.get_formsemestre(r["formsemestre_id"])
if r["etat"] == "I":

View File

@ -222,7 +222,6 @@ def formsemestre_check_absences_html(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Vérification absences aux évaluations de ce semestre",
sem,
),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.
Sont listés tous les modules avec des évaluations.<br/>Aucune action n'est effectuée:
@ -238,7 +237,11 @@ def formsemestre_check_absences_html(formsemestre_id):
if evals:
H.append(
'<div class="module_check_absences"><h2><a href="moduleimpl_status?moduleimpl_id=%s">%s: %s</a></h2>'
% (M["moduleimpl_id"], M["module"]["code"], M["module"]["abbrev"])
% (
M["moduleimpl_id"],
M["module"]["code"] or "",
M["module"]["abbrev"] or "",
)
)
for E in evals:
H.append(

View File

@ -36,6 +36,8 @@ from flask import url_for, g
from flask_login import current_user
from app import log
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -81,43 +83,6 @@ _evaluationEditor = ndb.EditableTable(
)
def evaluation_enrich_dict(e):
"""add or convert some fileds in an evaluation dict"""
# For ScoDoc7 compat
heure_debut_dt = e["heure_debut"] or datetime.time(
8, 00
) # au cas ou pas d'heure (note externe?)
heure_fin_dt = e["heure_fin"] or datetime.time(8, 00)
e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"])
e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"])
e["jouriso"] = ndb.DateDMYtoISO(e["jour"])
heure_debut, heure_fin = e["heure_debut"], e["heure_fin"]
d = ndb.TimeDuration(heure_debut, heure_fin)
if d is not None:
m = d % 60
e["duree"] = "%dh" % (d / 60)
if m != 0:
e["duree"] += "%02d" % m
else:
e["duree"] = ""
if heure_debut and (not heure_fin or heure_fin == heure_debut):
e["descrheure"] = " à " + heure_debut
elif heure_debut and heure_fin:
e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin)
else:
e["descrheure"] = ""
# matin, apresmidi: utile pour se referer aux absences:
if heure_debut_dt < datetime.time(12, 00):
e["matin"] = 1
else:
e["matin"] = 0
if heure_fin_dt > datetime.time(12, 00):
e["apresmidi"] = 1
else:
e["apresmidi"] = 0
return e
def do_evaluation_list(args, sortkey=None):
"""List evaluations, sorted by numero (or most recent date first).
@ -127,7 +92,7 @@ def do_evaluation_list(args, sortkey=None):
'apresmidi' : 1 (termine après 12:00) ou 0
'descrheure' : ' de 15h00 à 16h30'
"""
# Attention: transformation fonction ScoDc7 en SQLAlchemy
# Attention: transformation fonction ScoDoc7 en SQLAlchemy
cnx = ndb.GetDBConnexion()
evals = _evaluationEditor.list(cnx, args, sortkey=sortkey)
# calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi
@ -146,58 +111,6 @@ def do_evaluation_list_in_formsemestre(formsemestre_id):
return evals
def _check_evaluation_args(args):
"Check coefficient, dates and duration, raises exception if invalid"
moduleimpl_id = args["moduleimpl_id"]
# check bareme
note_max = args.get("note_max", None)
if note_max is None:
raise ScoValueError("missing note_max")
try:
note_max = float(note_max)
except ValueError:
raise ScoValueError("Invalid note_max value")
if note_max < 0:
raise ScoValueError("Invalid note_max value (must be positive or null)")
# check coefficient
coef = args.get("coefficient", None)
if coef is None:
raise ScoValueError("missing coefficient")
try:
coef = float(coef)
except ValueError:
raise ScoValueError("Invalid coefficient value")
if coef < 0:
raise ScoValueError("Invalid coefficient value (must be positive or null)")
# check date
jour = args.get("jour", None)
args["jour"] = jour
if jour:
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
d, m, y = [int(x) for x in sem["date_debut"].split("/")]
date_debut = datetime.date(y, m, d)
d, m, y = [int(x) for x in sem["date_fin"].split("/")]
date_fin = datetime.date(y, m, d)
# passe par ndb.DateDMYtoISO pour avoir date pivot
y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")]
jour = datetime.date(y, m, d)
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)
)
heure_debut = args.get("heure_debut", None)
args["heure_debut"] = heure_debut
heure_fin = args.get("heure_fin", None)
args["heure_fin"] = heure_fin
if jour and ((not heure_debut) or (not heure_fin)):
raise ScoValueError("Les heures doivent être précisées")
d = ndb.TimeDuration(heure_debut, heure_fin)
if d and ((d < 0) or (d > 60 * 12)):
raise ScoValueError("Heures de l'évaluation incohérentes !")
def do_evaluation_create(
moduleimpl_id=None,
jour=None,
@ -219,7 +132,7 @@ def do_evaluation_create(
)
args = locals()
log("do_evaluation_create: args=" + str(args))
_check_evaluation_args(args)
check_evaluation_args(args)
# Check numeros
module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
if not "numero" in args or args["numero"] is None:
@ -262,6 +175,7 @@ def do_evaluation_create(
# news
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"])
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
@ -287,7 +201,7 @@ def do_evaluation_edit(args):
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
)
args["moduleimpl_id"] = moduleimpl_id
_check_evaluation_args(args)
check_evaluation_args(args)
cnx = ndb.GetDBConnexion()
_evaluationEditor.edit(cnx, args)

View File

@ -122,7 +122,7 @@ def evaluation_create_form(
#
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> %s' % (
moduleimpl_id,
mod["code"],
mod["code"] or "module sans code",
mod["titre"],
link,
)
@ -139,6 +139,7 @@ def evaluation_create_form(
initvalues["visibulletinlist"] = ["X"]
else:
initvalues["visibulletinlist"] = []
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
vals = scu.get_request_args()
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = []
@ -158,7 +159,7 @@ def evaluation_create_form(
else:
coef_ue = ue_coef_dict.get(ue.id, 0.0) or 0.0
if coef_ue > 0:
poids = 1.0 # par defaut au départ
poids = 1.0 # par défaut au départ
else:
poids = 0.0
initvalues[f"poids_{ue.id}"] = poids
@ -207,7 +208,7 @@ def evaluation_create_form(
{
"size": 6,
"type": "float",
"explanation": "coef. dans le module (choisi librement par l'enseignant)",
"explanation": "coef. dans le module (choisi librement par l'enseignant, non utilisé pour rattrapage et 2ème session)",
"allow_null": False,
},
)
@ -284,6 +285,7 @@ def evaluation_create_form(
]
# Liste des UE utilisées dans des modules de ce semestre:
for ue in sem_ues:
coef_ue = ue_coef_dict.get(ue.id, 0.0)
form.append(
(
f"poids_{ue.id}",
@ -292,10 +294,14 @@ def evaluation_create_form(
"size": 2,
"type": "float",
"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" title="coef. du module dans cette UE">({"coef. mod.:" +str(coef_ue) if coef_ue else "ce module n'a pas de coef. dans cette UE"})</span>
<span class="eval_coef_ue_titre">{ue.titre}</span>
""",
"allow_null": False,
# ok si poids nul ou coef vers l'UE nul:
"validator": lambda val, field: (not val)
or ue_coef_dict.get(int(field[len("poids_") :]), 0.0) != 0,
"enabled": coef_ue != 0 or initvalues[f"poids_{ue.id}"] != 0.0,
},
),
)
@ -331,7 +337,7 @@ def evaluation_create_form(
if edit:
sco_evaluation_db.do_evaluation_edit(tf[2])
else:
# creation d'une evaluation
# création d'une evaluation
evaluation_id = sco_evaluation_db.do_evaluation_create(**tf[2])
if is_apc:
# Set poids

View File

@ -31,13 +31,17 @@ import datetime
import operator
import time
import flask
from flask import url_for
from flask import g
from flask_login import current_user
from flask import request
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
@ -379,10 +383,9 @@ def _eval_etat(evals):
def do_evaluation_etat_in_sem(formsemestre_id):
"""-> nb_eval_completes, nb_evals_en_cours, nb_evals_vides,
date derniere modif, attente"""
nt = sco_cache.NotesTableCache.get(
formsemestre_id
) # > liste evaluations et moduleimpl en attente
evals = nt.get_sem_evaluation_etat_list()
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
evals = nt.get_evaluations_etats()
etat = _eval_etat(evals)
# Ajoute information sur notes en attente
etat["attente"] = len(nt.get_moduleimpls_attente()) > 0
@ -393,18 +396,18 @@ def do_evaluation_etat_in_mod(nt, moduleimpl_id):
""""""
evals = nt.get_mod_evaluation_etat_list(moduleimpl_id)
etat = _eval_etat(evals)
etat["attente"] = moduleimpl_id in [
m["moduleimpl_id"] for m in nt.get_moduleimpls_attente()
] # > liste moduleimpl en attente
# Il y a-t-il des notes en attente dans ce module ?
etat["attente"] = nt.modimpls_results[moduleimpl_id].en_attente
return etat
def formsemestre_evaluations_cal(formsemestre_id):
"""Page avec calendrier de toutes les evaluations de ce semestre"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = formsemestre.to_dict()
evals = nt.get_sem_evaluation_etat_list()
evals = nt.get_evaluations_etats()
nb_evals = len(evals)
color_incomplete = "#FF6060"
@ -467,7 +470,6 @@ def formsemestre_evaluations_cal(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Evaluations du semestre",
sem,
cssstyles=["css/calabs.css"],
),
'<div class="cal_evaluations">',
@ -537,10 +539,11 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
N'indique pas les évaluations de ratrapage ni celles des modules de bonus/malus.
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = formsemestre.to_dict()
evals = nt.get_sem_evaluation_etat_list()
evals = nt.get_evaluations_etats()
T = []
for e in evals:
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=e["moduleimpl_id"])[0]
@ -634,7 +637,14 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
)
mod_descr = (
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
% (moduleimpl_id, Mod["code"], Mod["titre"], nomcomplet, resp, link)
% (
moduleimpl_id,
Mod["code"] or "",
Mod["titre"] or "?",
nomcomplet,
resp,
link,
)
)
etit = E["description"] or ""

View File

@ -40,7 +40,7 @@ class InvalidNoteValue(ScoException):
pass
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message
# Exception qui stoque dest_url
class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None):
super().__init__(msg)

View File

@ -29,6 +29,9 @@
"""
from flask import url_for, g, request
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
@ -77,7 +80,8 @@ def _build_results_table(start_date=None, end_date=None, types_parcours=[]):
{}
) # etudid : { formsemestre_id d'inscription le plus recent dans les dates considérées, etud }
for formsemestre_id in formsemestre_ids_parcours:
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etudids
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = nt.get_etudids()
for etudid in etudids:
if etudid not in etuds_infos: # pas encore traité ?

View File

@ -342,7 +342,7 @@ def do_formsemestre_uecoef_delete(cnx, formsemestre_id, ue_id):
formsemestre_uecoef_delete(cnx, coefs[0]["formsemestre_uecoef_id"])
def read_formsemestre_etapes(formsemestre_id):
def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
"""recupere liste des codes etapes associés à ce semestre
:returns: liste d'instance de ApoEtapeVDI
"""
@ -451,7 +451,7 @@ def sem_in_annee_scolaire(sem, year=False):
)
def sem_une_annee(sem):
def sem_une_annee(sem): # XXX deprecated: use FormSemestre.est_sur_une_annee()
"""Test si sem est entièrement sur la même année scolaire.
(ce n'est pas obligatoire mais si ce n'est pas le cas les exports Apogée ne vont pas fonctionner)
pivot au 1er août.

View File

@ -84,7 +84,7 @@ def formsemestre_custommenu_edit(formsemestre_id):
scu.NotesURL() + "/formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
H = [
html_sco_header.html_sem_header("Modification du menu du semestre ", sem),
html_sco_header.html_sem_header("Modification du menu du semestre "),
"""<p class="help">Ce menu, spécifique à chaque semestre, peut être utilisé pour placer des liens vers vos applications préférées.</p>
<p class="help">Procédez en plusieurs fois si vous voulez ajouter plusieurs items.</p>""",
]

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():
@ -96,7 +99,6 @@ def formsemestre_editwithmodules(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Modification du semestre",
sem,
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
@ -116,10 +118,16 @@ def formsemestre_editwithmodules(formsemestre_id):
vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
H.append(
"""<p class="help">Seuls les modules cochés font partie de ce semestre. Pour les retirer, les décocher et appuyer sur le bouton "modifier".
</p>
<p class="help">Attention : s'il y a déjà des évaluations dans un module, il ne peut pas être supprimé !</p>
<p class="help">Les modules ont toujours un responsable. Par défaut, c'est le directeur des études.</p>"""
"""<p class="help">Seuls les modules cochés font partie de ce semestre.
Pour les retirer, les décocher et appuyer sur le bouton "modifier".
</p>
<p class="help">Attention : s'il y a déjà des évaluations dans un module,
il ne peut pas être supprimé !</p>
<p class="help">Les modules ont toujours un responsable.
Par défaut, c'est le directeur des études.</p>
<p class="help">Un semestre ne peut comporter qu'une seule UE "bonus
sport/culture"</p>
"""
)
return "\n".join(H) + html_sco_header.sco_footer()
@ -141,6 +149,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
@ -162,26 +171,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(
@ -193,49 +201,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:
@ -320,7 +317,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),
},
),
(
@ -341,6 +338,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 "",
},
),
)
@ -550,7 +550,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 peut 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"]
@ -561,7 +566,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 = ""
@ -596,7 +601,7 @@ def do_formsemestre_createwithmodules(edit=False):
"input_type": "text_suggest",
"size": 50,
"withcheckbox": True,
"title": "%s %s" % (mod["code"], mod["titre"]),
"title": "%s %s" % (mod["code"] or "", mod["titre"] or ""),
"allowed_values": allowed_user_names,
"template": itemtemplate,
"text_suggest_options": {
@ -685,12 +690,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:
@ -736,42 +742,59 @@ 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"]]
_formsemestre_check_ue_bonus_unicity(module_ids_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,
@ -779,7 +802,9 @@ def do_formsemestre_createwithmodules(edit=False):
}
moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
mod = sco_edit_module.module_list({"module_id": module_id})[0]
msg += ["création de %s (%s)" % (mod["code"], mod["titre"])]
msg += [
"création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?")
]
# INSCRIPTIONS DES ETUDIANTS
log(
'inscription module: %s = "%s"'
@ -801,7 +826,7 @@ def do_formsemestre_createwithmodules(edit=False):
)
msg += [
"inscription de %d étudiants au module %s"
% (len(etudids), mod["code"])
% (len(etudids), mod["code"] or "(module sans code)")
]
else:
log(
@ -809,9 +834,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"]
@ -848,6 +875,34 @@ 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_check_ue_bonus_unicity(module_ids):
"""Vérifie qu'il n'y a qu'une seule UE bonus associée aux modules choisis"""
ues = [Module.query.get_or_404(module_id).ue for module_id in module_ids]
ues_bonus = {ue.id for ue in ues if ue.type == sco_codes_parcours.UE_SPORT}
if len(ues_bonus) > 1:
raise ScoValueError(
"""Les modules de bonus sélectionnés ne sont pas tous dans la même UE bonus.
Changez la sélection ou modifiez la structure du programme de formation.""",
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)
@ -866,11 +921,19 @@ def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
if evals:
msg += [
'<b>impossible de supprimer %s (%s) car il y a %d évaluations définies (<a href="moduleimpl_status?moduleimpl_id=%s" class="stdlink">supprimer les d\'abord</a>)</b>'
% (mod["code"], mod["titre"], len(evals), moduleimpl_id)
% (
mod["code"] or "(module sans code)",
mod["titre"],
len(evals),
moduleimpl_id,
)
]
ok = False
else:
msg += ["suppression de %s (%s)" % (mod["code"], mod["titre"])]
msg += [
"suppression de %s (%s)"
% (mod["code"] or "(module sans code)", mod["titre"] or "")
]
sco_moduleimpl.do_moduleimpl_delete(
moduleimpl_id, formsemestre_id=formsemestre_id
)
@ -900,7 +963,6 @@ def formsemestre_clone(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Copie du semestre",
sem,
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
@ -1240,7 +1302,7 @@ def formsemestre_delete(formsemestre_id):
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
H = [
html_sco_header.html_sem_header("Suppression du semestre", sem),
html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span>
<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
<b>un semestre ne doit jamais être supprimé</b> (on perd la mémoire des notes et de tous les événements liés à ce semestre !).</p>
@ -1519,7 +1581,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
</p>
"""
H = [
html_sco_header.html_sem_header("Coefficients des UE du semestre", sem),
html_sco_header.html_sem_header("Coefficients des UE du semestre"),
help,
]
#
@ -1631,7 +1693,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
formsemestre_id=formsemestre_id
) # > modif coef UE cap (modifs notes de _certains_ etudiants)
header = html_sco_header.html_sem_header("Coefficients des UE du semestre", sem)
header = html_sco_header.html_sem_header("Coefficients des UE du semestre")
return (
header
+ "\n".join(z)

View File

@ -37,6 +37,9 @@ import flask
from flask import url_for, g, request
from flask_login import current_user
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
@ -260,7 +263,8 @@ def formsemestre_ext_edit_ue_validations(formsemestre_id, etudid):
def _make_page(etud, sem, tf, message=""):
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
moy_gen = nt.get_etud_moy_gen(etud["etudid"])
H = [
html_sco_header.sco_header(

View File

@ -32,14 +32,16 @@ import time
import flask
from flask import url_for, g, request
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import ScoException, ScoValueError
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_codes_parcours import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
import app.scodoc.notesdb as ndb
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_find_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
@ -186,7 +188,9 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
raise ScoValueError("desinscription impossible: semestre verrouille")
# -- Si decisions de jury, desinscription interdite
nt = sco_cache.NotesTableCache.get(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if nt.etud_has_decision(etudid):
raise ScoValueError(
"desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)"
@ -374,7 +378,6 @@ def formsemestre_inscription_with_modules(
H = [
html_sco_header.html_sem_header(
"Inscription de %s dans ce semestre" % etud["nomprenom"],
sem,
)
]
F = html_sco_header.sco_footer()
@ -476,7 +479,8 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
raise ScoValueError("Modification impossible: semestre verrouille")
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_ue_status
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
F = html_sco_header.sco_footer()
H = [
@ -504,7 +508,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
modimpls_by_ue_ids[ue_id].append(mod["moduleimpl_id"])
modimpls_by_ue_names[ue_id].append(
"%s %s" % (mod["module"]["code"], mod["module"]["titre"])
"%s %s" % (mod["module"]["code"] or "", mod["module"]["titre"] or "")
)
vals = scu.get_request_args()
if not vals.get("tf_submitted", False):
@ -528,7 +532,7 @@ def formsemestre_inscription_option(etudid, formsemestre_id):
if ue["type"] != UE_STANDARD:
ue_descr += " <em>%s</em>" % UE_TYPE_NAME[ue["type"]]
ue_status = nt.get_etud_ue_status(etudid, ue_id)
if ue_status["is_capitalized"]:
if ue_status and ue_status["is_capitalized"]:
sem_origin = sco_formsemestre.get_formsemestre(ue_status["formsemestre_id"])
ue_descr += ' <a class="discretelink" href="formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="%s">(capitalisée le %s)' % (
sem_origin["formsemestre_id"],
@ -649,7 +653,7 @@ function chkbx_select(field_id, state) {
"%s (%s)"
% (
modsdict[x]["module"]["titre"],
modsdict[x]["module"]["code"],
modsdict[x]["module"]["code"] or "(module sans code)",
)
for x in a_desinscrire
]
@ -668,7 +672,7 @@ function chkbx_select(field_id, state) {
"%s (%s)"
% (
modsdict[x]["module"]["titre"],
modsdict[x]["module"]["code"],
modsdict[x]["module"]["code"] or "(module sans code)",
)
for x in a_inscrire
]
@ -786,7 +790,9 @@ def list_inscrits_ailleurs(formsemestre_id):
Pour chacun, donne la liste des semestres.
{ etudid : [ liste de sems ] }
"""
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etudids
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = nt.get_etudids()
d = {}
for etudid in etudids:
@ -802,7 +808,6 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
H = [
html_sco_header.html_sem_header(
"Inscriptions multiples parmi les étudiants du semestre ",
sem,
)
]
insd = list_inscrits_ailleurs(formsemestre_id)

View File

@ -35,7 +35,10 @@ from flask import url_for
from flask_login import current_user
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import Module
from app.models.formsemestre import FormSemestre
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
@ -48,7 +51,6 @@ from app.scodoc import sco_archives
from app.scodoc import sco_bulletins
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_compute_moy
from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
@ -105,10 +107,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"',
@ -594,7 +596,8 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
Liste des modules et de leurs coefficients
"""
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > liste evaluations
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
@ -635,7 +638,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
)
l = {
"UE": M["ue"]["acronyme"],
"Code": M["module"]["code"],
"Code": M["module"]["code"] or "",
"Module": M["module"]["abbrev"] or M["module"]["titre"],
"_Module_class": "scotext",
"Inscrits": len(ModInscrits),
@ -722,7 +725,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
% (request.base_url, formsemestre_id, with_evals),
page_title=title,
html_title=html_sco_header.html_sem_header(
"Description du semestre", sem, with_page_header=False
"Description du semestre", with_page_header=False
),
pdf_title=title,
preferences=sco_preferences.SemPreferences(formsemestre_id),
@ -915,34 +918,35 @@ def html_expr_diagnostic(diagnostics):
def formsemestre_status_head(formsemestre_id=None, page_title=None):
"""En-tête HTML des pages "semestre" """
semlist = sco_formsemestre.do_formsemestre_list(
args={"formsemestre_id": formsemestre_id}
)
if not semlist:
raise ScoValueError("Session inexistante (elle a peut être été supprimée ?)")
sem = semlist[0]
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
sem = FormSemestre.query.get(formsemestre_id)
if not sem:
raise ScoValueError("Semestre inexistant (il a peut être été supprimé ?)")
formation = sem.formation
parcours = formation.get_parcours()
page_title = page_title or "Modules de "
H = [
html_sco_header.html_sem_header(
page_title, sem, with_page_header=False, with_h2=False
page_title, with_page_header=False, with_h2=False
),
f"""<table>
<tr><td class="fichetitre2">Formation: </td><td>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=F['formation_id'])}"
class="discretelink" title="Formation {F['acronyme']}, v{F['version']}">{F['titre']}</a>""",
<a href="{url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=sem.formation.id)}"
class="discretelink" title="Formation {
formation.acronyme}, v{formation.version}">{formation.titre}</a>
""",
]
if sem["semestre_id"] >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem["semestre_id"]))
if sem["modalite"]:
H.append("&nbsp;en %(modalite)s" % sem)
if sem["etapes"]:
if sem.semestre_id >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id))
if sem.modalite:
H.append(f"&nbsp;en {sem.modalite}")
if sem.etapes:
H.append(
"&nbsp;&nbsp;&nbsp;(étape <b><tt>%s</tt></b>)"
% (sem["etapes_apo_str"] or "-")
f"""&nbsp;&nbsp;&nbsp;(étape <b><tt>{
sem.etapes_apo_str() or "-"
}</tt></b>)"""
)
H.append("</td></tr>")
@ -965,18 +969,16 @@ Il y a des notes en attente ! Le classement des étudiants n'a qu'une valeur ind
)
H.append("</table>")
sem_warning = ""
if sem["bul_hide_xml"]:
if sem.bul_hide_xml:
sem_warning += "Bulletins non publiés sur le portail. "
if sem["block_moyennes"]:
if sem.block_moyennes:
sem_warning += "Calcul des moyennes bloqué !"
if sem_warning:
H.append('<p class="fontorange"><em>' + sem_warning + "</em></p>")
if sem["semestre_id"] >= 0 and not sco_formsemestre.sem_une_annee(sem):
if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
H.append(
'<p class="fontorange"><em>Attention: ce semestre couvre plusieurs années scolaires !</em></p>'
)
# elif sco_preferences.get_preference( 'bul_display_publication', formsemestre_id):
# H.append('<p><em>Bulletins publiés sur le portail</em></p>')
return "".join(H)
@ -989,7 +991,9 @@ def formsemestre_status(formsemestre_id=None):
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id
)
nt = sco_cache.NotesTableCache.get(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt = res_sem.load_formsemestre_results(formsemestre)
# Construit la liste de tous les enseignants de ce semestre:
mails_enseignants = set(
[sco_users.user_info(ens_id)["email"] for ens_id in sem["responsables"]]
@ -1102,6 +1106,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>
"""
@ -1119,7 +1124,7 @@ def formsemestre_tableau_modules(
mod_descr = "Module " + (mod.titre or "")
if mod.is_apc():
coef_descr = ", ".join(
[f"{ue_acro}: {co}" for ue_acro, co in mod.ue_coefs_descr()]
[f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()]
)
if coef_descr:
mod_descr += " Coefs: " + coef_descr
@ -1146,30 +1151,19 @@ def formsemestre_tableau_modules(
f"""<tr class="formsemestre_status_ue"><td colspan="4">
<span class="status_ue_acro">{ue["acronyme"]}</span>
<span class="status_ue_title">{titre}</span>
</td><td>"""
</td><td colspan="2">"""
)
if can_edit:
H.append(
' <a href="edit_ue_expr?formsemestre_id=%s&ue_id=%s">'
% (formsemestre_id, ue["ue_id"])
)
H.append(
scu.icontag(
"formula",
title="Mode calcul moyenne d'UE",
style="vertical-align:middle",
)
)
if can_edit:
H.append("</a>")
expr = sco_compute_moy.get_ue_expression(
formsemestre_id, ue["ue_id"], html_quote=True
)
if expr:
H.append(
""" <span class="formula" title="mode de calcul de la moyenne d'UE">%s</span>"""
% expr
f""" <span class="formula" title="mode de calcul de la moyenne d'UE">{expr}</span>
<span class="warning">formule inutilisée en 9.2: <a href="{
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, ue_id=ue["ue_id"] )
}
">supprimer</a></span>"""
)
H.append("</td></tr>")
@ -1208,7 +1202,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

@ -35,11 +35,17 @@ from flask import url_for, g, request
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.scodoc.scolog import logdb
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models.notes import etud_has_notes_attente
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb
from app.scodoc.sco_codes_parcours import *
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc import html_sco_header
from app.scodoc import sco_abs
from app.scodoc import sco_codes_parcours
@ -47,10 +53,9 @@ from app.scodoc import sco_cache
from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_parcours_dut
from app.scodoc.sco_parcours_dut import etud_est_inscrit_ue
from app.scodoc import sco_photos
from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury
@ -65,9 +70,8 @@ def formsemestre_validation_etud_form(
sortcol=None,
readonly=True,
):
nt = sco_cache.NotesTableCache.get(
formsemestre_id
) # > get_table_moyennes_triees, get_etud_decision_sem
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
T = nt.get_table_moyennes_triees()
if not etudid and etud_index is None:
raise ValueError("formsemestre_validation_etud_form: missing argument etudid")
@ -195,7 +199,7 @@ def formsemestre_validation_etud_form(
decision_jury = Se.nt.get_etud_decision_sem(etudid)
# Bloque si note en attente
if nt.etud_has_notes_attente(etudid):
if etud_has_notes_attente(etudid, formsemestre_id):
H.append(
tf_error_message(
f"""Impossible de statuer sur cet étudiant: il a des notes en
@ -541,9 +545,8 @@ def formsemestre_recap_parcours_table(
else:
ass = ""
nt = sco_cache.NotesTableCache.get(
sem["formsemestre_id"]
) # > get_ues, get_etud_moy_gen, get_etud_ue_status
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
if is_cur:
type_sem = "*" # now unused
class_sem = "sem_courant"
@ -553,7 +556,7 @@ def formsemestre_recap_parcours_table(
else:
type_sem = ""
class_sem = "sem_autre"
if sem["formation_code"] != Se.formation["formation_code"]:
if sem["formation_code"] != Se.formation.formation_code:
class_sem += " sem_autre_formation"
if sem["bul_bgcolor"]:
bgcolor = sem["bul_bgcolor"]
@ -582,8 +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
ues = nt.get_ues(filter_sport=True, filter_non_inscrit=True, etudid=etudid)
# acronymes UEs auxquelles l'étudiant est inscrit:
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion()
ues = [
ue
for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
]
for ue in ues:
H.append('<td class="ue_acro"><span>%s</span></td>' % ue["acronyme"])
if len(ues) < Se.nb_max_ue:
@ -612,7 +624,7 @@ def formsemestre_recap_parcours_table(
if not sem["etat"]: # locked
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
default_sem_info += lockicon
if sem["formation_code"] != Se.formation["formation_code"]:
if sem["formation_code"] != Se.formation.formation_code:
default_sem_info += "Autre formation: %s" % sem["formation_code"]
H.append(
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
@ -633,7 +645,7 @@ def formsemestre_recap_parcours_table(
else:
code = ""
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
moy_ue = ue_status["moy"]
moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings
if code == ADM:
class_ue = "ue_adm"
@ -641,12 +653,12 @@ def formsemestre_recap_parcours_table(
class_ue = "ue_cmp"
else:
class_ue = "ue"
if ue_status["is_external"]: # validation externe
if ue_status and ue_status["is_external"]: # validation externe
explanation_ue.append("UE externe.")
# log('x'*12+' EXTERNAL %s' % notes_table.fmt_note(moy_ue)) XXXXXXX
# log('UE=%s' % pprint.pformat(ue))
# log('explanation_ue=%s\n'%explanation_ue)
if ue_status["is_capitalized"]:
if ue_status and ue_status["is_capitalized"]:
class_ue += " ue_capitalized"
explanation_ue.append(
"Capitalisée le %s." % (ue_status["event_date"] or "?")
@ -676,7 +688,7 @@ def formsemestre_recap_parcours_table(
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
or nt.parcours.ECTS_ONLY
):
etud_moy_infos = nt.get_etud_moy_infos(etudid)
etud_ects_infos = nt.get_etud_ects_pot(etudid)
H.append(
'<tr class="%s rcp_l2 sem_%s">' % (class_sem, sem["formsemestre_id"])
)
@ -686,16 +698,16 @@ def formsemestre_recap_parcours_table(
)
# total ECTS (affiché sous la moyenne générale)
H.append(
'<td class="sem_ects_tit"><a title="crédit potentiels (dont nb de fondamentaux)">ECTS:</a></td><td class="sem_ects">%g <span class="ects_fond">%g</span></td>'
% (etud_moy_infos["ects_pot"], etud_moy_infos["ects_pot_fond"])
'<td class="sem_ects_tit"><a title="crédit potentiels">ECTS:</a></td><td class="sem_ects">%g</td>'
% (etud_ects_infos["ects_pot"])
)
H.append('<td class="rcp_abs"></td>')
# ECTS validables dans chaque UE
for ue in ues:
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
H.append(
'<td class="ue">%g <span class="ects_fond">%g</span></td>'
% (ue_status["ects_pot"], ue_status["ects_pot_fond"])
'<td class="ue">%g</td>'
% (ue_status["ects_pot"] if ue_status else "")
)
H.append("<td></td></tr>")
@ -837,9 +849,7 @@ def formsemestre_validation_auto(formsemestre_id):
"Formulaire saisie automatisee des decisions d'un semestre"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [
html_sco_header.html_sem_header(
"Saisie automatique des décisions du semestre", sem
),
html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"),
"""
<ul>
<li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et
@ -867,9 +877,8 @@ def do_formsemestre_validation_auto(formsemestre_id):
"Saisie automatisee des decisions d'un semestre"
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
next_semestre_id = sem["semestre_id"] + 1
nt = sco_cache.NotesTableCache.get(
formsemestre_id
) # > get_etudids, get_etud_decision_sem,
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etudids = nt.get_etudids()
nb_valid = 0
conflicts = [] # liste des etudiants avec decision differente déjà saisie
@ -888,7 +897,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
)
and Se.barre_moy_ok
and Se.barres_ue_ok
and not nt.etud_has_notes_attente(etudid)
and not etud_has_notes_attente(etudid, formsemestre_id)
):
# check: s'il existe une decision ou autorisation et qu'elles sont differentes,
# warning (et ne fait rien)
@ -1048,7 +1057,7 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
"title": "Indice du semestre",
"explanation": "Facultatif: indice du semestre dans la formation",
"allow_null": True,
"allowed_values": [""] + [str(x) for x in range(11)],
"allowed_values": [""] + [x for x in range(11)],
"labels": ["-"] + list(range(11)),
},
),
@ -1122,9 +1131,11 @@ def do_formsemestre_validate_previous_ue(
Si le coefficient est spécifié, modifie le coefficient de
cette UE (utile seulement pour les semestres extérieurs).
"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
cnx = ndb.GetDBConnexion()
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_ue_status
if ue_coefficient != None:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre_id, ue_id, ue_coefficient

View File

@ -45,6 +45,9 @@ from flask import g, request
from flask import url_for, make_response
from app import db
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre, formsemestre
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
from app.models.groups import Partition
import app.scodoc.sco_utils as scu
@ -488,17 +491,14 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
<group ...>
...
"""
from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()
t0 = time.time()
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
etuds_set = {ins.etudid for ins in formsemestre.inscriptions}
sem = formsemestre.get_infos_dict() # transition TODO
groups = get_partition_groups(partition)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict
etuds_set = set(nt.inscrdict)
# Build XML:
t1 = time.time()
doc = Element("ajax-response")
@ -1277,13 +1277,13 @@ def groups_auto_repartition(partition_id=None):
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
formsemestre = FormSemestre.query.get(formsemestre_id)
# renvoie sur page édition groupes
dest_url = url_for(
"scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
)
if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
descr = [
("partition_id", {"input_type": "hidden"}),
@ -1301,7 +1301,7 @@ def groups_auto_repartition(partition_id=None):
H = [
html_sco_header.sco_header(page_title="Répartition des groupes"),
"<h2>Répartition des groupes de %s</h2>" % partition["partition_name"],
"<p>Semestre %s</p>" % sem["titreannee"],
f"<p>Semestre {formsemestre.titre_annee()}</p>",
"""<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau
des groupes (en utilisant la dernière moyenne générale disponible pour
@ -1343,7 +1343,7 @@ def groups_auto_repartition(partition_id=None):
# return '\n'.join(H) + tf[1] + html_sco_header.sco_footer()
group_ids.append(create_group(partition_id, group_name))
#
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > identdict
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
identdict = nt.identdict
# build: { civilite : liste etudids trie par niveau croissant }
civilites = set([x["civilite"] for x in identdict.values()])
@ -1384,9 +1384,8 @@ def _get_prev_moy(etudid, formsemestre_id):
etud = info[0]
Se = sco_parcours_dut.SituationEtudParcours(etud, formsemestre_id)
if Se.prev:
nt = sco_cache.NotesTableCache.get(
Se.prev["formsemestre_id"]
) # > get_etud_moy_gen
prev_sem = FormSemestre.query.get(Se.prev["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
return nt.get_etud_moy_gen(etudid)
else:
return 0.0
@ -1438,18 +1437,19 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
def do_evaluation_listeetuds_groups(
evaluation_id, groups=None, getallstudents=False, include_dems=False
evaluation_id, groups=None, getallstudents=False, include_demdef=False
):
"""Donne la liste des etudids inscrits a cette evaluation dans les
groupes indiqués.
Si getallstudents==True, donne tous les etudiants inscrits a cette
evaluation.
Si include_dems, compte aussi les etudiants démissionnaires
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
(sinon, par défaut, seulement les 'I')
Résultat: [ (etudid, etat) ], etat='I', 'D', 'DEF'
"""
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et include_dems faux
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et
# include_demdef faux
fromtables = [
"notes_moduleimpl_inscription Im",
"notes_formsemestre_inscription Isem",
@ -1481,7 +1481,7 @@ def do_evaluation_listeetuds_groups(
and E.id = %(evaluation_id)s
"""
)
if not include_dems:
if not include_demdef:
req += " and Isem.etat='I'"
req += r
cnx = ndb.GetDBConnexion()

View File

@ -302,7 +302,12 @@ class DisplayedGroupsInfos(object):
if group_ids:
group_ids = [group_ids] # cas ou un seul parametre, pas de liste
else:
group_ids = [int(g) for g in group_ids]
try:
group_ids = [int(g) for g in group_ids]
except ValueError as exc:
raise ScoValueError(
"identifiant de groupe invalide (mettre à jour vos bookmarks ?)"
) from exc
if not formsemestre_id and moduleimpl_id:
mods = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)
if len(mods) != 1:
@ -815,7 +820,7 @@ def tab_absences_html(groups_infos, etat=None):
% (groups_infos.base_url, groups_infos.groups_titles),
"""<li><a class="stdlink" href="trombino?%s&format=pdf">Trombinoscope en PDF</a></li>"""
% groups_infos.groups_query_args,
"""<li><a class="stdlink" href="pdf_trombino_tours?%s&format=pdf">Trombinoscope en PDF (format "IUT de Tours")</a></li>"""
"""<li><a class="stdlink" href="pdf_trombino_tours?%s&format=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>"""
% groups_infos.groups_query_args,
"""<li><a class="stdlink" href="pdf_feuille_releve_absences?%s&format=pdf">Feuille relevé absences hebdomadaire (beta)</a></li>"""
% groups_infos.groups_query_args,

View File

@ -438,7 +438,7 @@ def build_page(
ignore_jury_checked = ""
H = [
html_sco_header.html_sem_header(
"Passages dans le semestre", sem, with_page_header=False
"Passages dans le semestre", with_page_header=False
),
"""<form name="f" method="post" action="%s">""" % request.base_url,
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>

View File

@ -31,13 +31,17 @@
import flask
from flask import url_for, g, request
from app import log
from app import models
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.models import FormSemestre
from app.models.evaluations import Evaluation
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 moy_mod
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
@ -241,7 +245,7 @@ def _make_table_notes(
if is_apc:
modimpl = ModuleImpl.query.get(moduleimpl_id)
is_conforme = modimpl.check_apc_conformity()
evals_poids, ues = moy_mod.df_load_evaluations_poids(moduleimpl_id)
evals_poids, ues = moy_mod.load_evaluations_poids(moduleimpl_id)
if not ues:
is_apc = False
else:
@ -309,7 +313,7 @@ def _make_table_notes(
anonymous_lst_key = "etudid"
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
E["evaluation_id"], groups, include_dems=True
E["evaluation_id"], groups, include_demdef=True
)
for etudid, etat in etudid_etats:
css_row_class = None
@ -432,7 +436,7 @@ def _make_table_notes(
if is_apc:
# Ajoute une colonne par UE
_add_apc_columns(
moduleimpl_id,
modimpl,
evals_poids,
ues,
rows,
@ -785,7 +789,9 @@ def _add_moymod_column(
):
"""Ajoute la colonne moymod à rows"""
col_id = "moymod"
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_etud_mod_moy
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
nb_notes = 0
sum_notes = 0
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
@ -815,7 +821,7 @@ def _add_moymod_column(
def _add_apc_columns(
moduleimpl_id,
modimpl,
evals_poids,
ues,
rows,
@ -834,18 +840,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_results(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

@ -29,6 +29,7 @@
"""
from flask_login import current_user
import psycopg2
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -271,7 +272,12 @@ _moduleimpl_inscriptionEditor = ndb.EditableTable(
def do_moduleimpl_inscription_create(args, formsemestre_id=None):
"create a moduleimpl_inscription"
cnx = ndb.GetDBConnexion()
r = _moduleimpl_inscriptionEditor.create(cnx, args)
try:
r = _moduleimpl_inscriptionEditor.create(cnx, args)
except psycopg2.errors.UniqueViolation as exc:
raise ScoValueError(
"Inscription impossible car déjà existante: vérifiez la situation"
)
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > moduleimpl_inscription

View File

@ -33,6 +33,10 @@ import flask
from flask import url_for, g, request
from flask_login import current_user
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
@ -88,7 +92,11 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
"Appliquer les modifications".
</p>
"""
% (moduleimpl_id, mod["titre"], mod["code"]),
% (
moduleimpl_id,
mod["titre"] or "(module sans titre)",
mod["code"] or "(module sans code)",
),
]
# Liste des inscrits à ce semestre
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
@ -304,8 +312,8 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append(
'<tr class="formsemestre_status"><td>%s</td><td class="formsemestre_status_code">%s</td><td class="formsemestre_status_inscrits">%s</td><td>%s</td></tr>'
% (
mod["ue"]["acronyme"],
mod["module"]["code"],
mod["ue"]["acronyme"] or "",
mod["module"]["code"] or "(module sans code)",
mod["nb_inscrits"],
c_link,
)
@ -333,7 +341,11 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
c_link = mod["module"]["titre"]
H.append(
'<tr class="formsemestre_status_green"><td>%s</td><td class="formsemestre_status_code">%s</td><td>%s</td></tr>'
% (mod["ue"]["acronyme"], mod["module"]["code"], c_link)
% (
mod["ue"]["acronyme"],
mod["module"]["code"] or "(module sans code)",
c_link,
)
)
H.append("</table>")
@ -402,14 +414,14 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append("</ul></li>")
H.append("</ul>")
H.append(
"""<hr/><p class="help">Cette page décrit les inscriptions actuelles.
Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en
cliquant sur la ligne du module.</p>
<p class="help">Note: la déinscription d'un module ne perd pas les notes. Ainsi, si
l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.</p>
"""
)
H.append(
"""<hr/><p class="help">Cette page décrit les inscriptions actuelles.
Vous pouvez changer (si vous en avez le droit) les inscrits dans chaque module en
cliquant sur la ligne du module.</p>
<p class="help">Note: la déinscription d'un module ne perd pas les notes. Ainsi, si
l'étudiant est ensuite réinscrit au même module, il retrouvera ses notes.</p>
"""
)
H.append(html_sco_header.sco_footer())
return "\n".join(H)
@ -479,19 +491,21 @@ def get_etuds_with_capitalized_ue(formsemestre_id):
returns { ue_id : [ { infos } ] }
"""
UECaps = scu.DictDefault(defaultvalue=[])
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_ues, get_etud_ue_status
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id}
)
ues = nt.get_ues()
ues = nt.get_ues_stat_dict()
for ue in ues:
for etud in inscrits:
status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
if status["was_capitalized"]:
ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
if ue_status and ue_status["was_capitalized"]:
UECaps[ue["ue_id"]].append(
{
"etudid": etud["etudid"],
"ue_status": status,
"ue_status": ue_status,
"is_ins": is_inscrit_ue(
etud["etudid"], formsemestre_id, ue["ue_id"]
),
@ -565,17 +579,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

@ -33,10 +33,14 @@ from flask import g, url_for
from flask_login import current_user
from app.auth.models import User
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models import ModuleImpl
from app.models.evaluations import Evaluation
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoInvalidIdType
from app.scodoc.sco_parcours_dut import formsemestre_has_decisions
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
@ -157,28 +161,31 @@ def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0):
return htmlutils.make_menu("actions", menuEval, alone=True)
def _ue_coefs_html(coefs_descr) -> str:
def _ue_coefs_html(coefs_lst) -> str:
""" """
max_coef = max([x[1] for x in coefs_descr]) if coefs_descr else 1.0
max_coef = max([x[1] for x in coefs_lst]) if coefs_lst else 1.0
H = """
<div id="modimpl_coefs">
<div>Coefficients vers les UE</div>
"""
if coefs_descr:
H += f"""
if coefs_lst:
H += (
f"""
<div class="coefs_histo" style="--max:{max_coef}">
""" + "\n".join(
[
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue_acronyme}</div>"""
for ue_acronyme, coef in coefs_descr
]
"""
+ "\n".join(
[
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
]
)
+ "</div>"
)
else:
H += """<div class="missing_value">non définis</span>"""
H += """
</div>
</div>
"""
H += """<div class="missing_value">non définis</div>"""
H += "</div>"
return H
@ -196,7 +203,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
moduleimpl_id=M["moduleimpl_id"]
)
nt = sco_cache.NotesTableCache.get(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(modimpl.formsemestre)
mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
mod_evals.sort(
key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True
@ -216,7 +224,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}"
),
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{Mod['code']}</tt> {Mod['titre']}</h2>
<tt>{Mod['code']}</tt> {Mod['titre']}
{"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""}
</h2>
<div class="moduleimpl_tableaubord moduleimpl_type_{
scu.ModuleType(Mod['module_type']).name.lower()}">
<table>
@ -259,7 +269,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
H.append(scu.icontag("lock32_img", title="verrouillé"))
H.append("""</td><td class="fichetitre2">""")
if modimpl.module.is_apc():
H.append(_ue_coefs_html(modimpl.module.ue_coefs_descr()))
H.append(_ue_coefs_html(modimpl.module.ue_coefs_list()))
else:
H.append(f"Coef. dans le semestre: {modimpl.module.coefficient}")
H.append("""</td><td></td></tr>""")
@ -285,21 +295,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>'
@ -326,15 +331,16 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
#
if not modimpl.check_apc_conformity():
H.append(
"""<ul class="tf-msg"><li class="tf-msg warning conformite">Les poids des évaluations de ce module ne sont pas encore conformes au PN.
"""<div class="warning conformite">Les poids des évaluations de ce module ne sont
pas encore conformes au PN.
Ses notes ne peuvent pas être prises en compte dans les moyennes d'UE.
</li></ul>"""
</div>"""
)
#
if has_expression and nt.expr_diagnostics:
H.append(sco_formsemestre_status.html_expr_diagnostic(nt.expr_diagnostics))
#
if nt.sem_has_decisions():
if formsemestre_has_decisions(formsemestre_id):
H.append(
"""<ul class="tf-msg"><li class="tf-msg warning">Décisions de jury saisies: seul le responsable du semestre peut saisir des notes (il devra modifier les décisions de jury).</li></ul>"""
)

View File

@ -36,6 +36,7 @@ from flask_login import current_user
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.models.etudiants import make_etud_args
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_archives_etud
@ -156,7 +157,7 @@ def ficheEtud(etudid=None):
# la sidebar est differente s'il y a ou pas un etudid
# voir html_sidebar.sidebar()
g.etudid = etudid
args = sco_etud.make_etud_args(etudid=etudid)
args = make_etud_args(etudid=etudid)
etuds = sco_etud.etudident_list(cnx, args)
if not etuds:
log("ficheEtud: etudid=%s request.args=%s" % (etudid, request.args))

View File

@ -28,11 +28,15 @@
"""Semestres: gestion parcours DUT (Arreté du 13 août 2005)
"""
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre, UniteEns
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.scodoc.scolog import logdb
from app.scodoc import sco_cache
from app.scodoc import sco_cache, sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formations
from app.scodoc.sco_codes_parcours import (
@ -107,9 +111,8 @@ class DecisionSem(object):
def SituationEtudParcours(etud, formsemestre_id):
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
nt = sco_cache.NotesTableCache.get(
formsemestre_id
) # > get_etud_decision_sem, get_etud_moy_gen, get_ues, get_etud_ue_status, etud_check_conditions_ues
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
parcours = nt.parcours
#
if parcours.ECTS_ONLY:
@ -130,7 +133,7 @@ class SituationEtudParcoursGeneric(object):
self.formsemestre_id = formsemestre_id
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
self.nt = nt
self.formation = self.nt.formation
self.formation = self.nt.formsemestre.formation
self.parcours = self.nt.parcours
# Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)
# pour le DUT, le dernier est toujours S4.
@ -294,11 +297,12 @@ class SituationEtudParcoursGeneric(object):
for sem in self.get_semestres():
if (
sem["semestre_id"] == n1
and sem["formation_code"] == self.formation["formation_code"]
and sem["formation_code"] == self.formation.formation_code
):
nt = sco_cache.NotesTableCache.get(
sem["formsemestre_id"]
) # > get_etud_decision_sem
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre
)
decision = nt.get_etud_decision_sem(self.etudid)
if decision and (
code_semestre_validant(decision["code"])
@ -311,10 +315,9 @@ class SituationEtudParcoursGeneric(object):
"""True si les semestres dont les indices sont donnés en argument (modifié)
sont validés. En sortie, sem_idx_set contient ceux qui n'ont pas été validés."""
for sem in self.get_semestres():
if sem["formation_code"] == self.formation["formation_code"]:
nt = sco_cache.NotesTableCache.get(
sem["formsemestre_id"]
) # > get_etud_decision_sem
if sem["formation_code"] == self.formation.formation_code:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
decision = nt.get_etud_decision_sem(self.etudid)
if decision and code_semestre_validant(decision["code"]):
# validé
@ -324,14 +327,19 @@ class SituationEtudParcoursGeneric(object):
def _comp_semestres(self):
# etud['sems'] est trie par date decroissante (voir fill_etuds_info)
if not "sems" in self.etud:
self.etud["sems"] = sco_etud.etud_inscriptions_infos(
self.etud["etudid"], self.etud["ne"]
)["sems"]
sems = self.etud["sems"][:] # copy
sems.reverse()
# Nb max d'UE et acronymes
ue_acros = {} # acronyme ue : 1
nb_max_ue = 0
for sem in sems:
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"]) # > get_ues
ues = nt.get_ues(filter_sport=True)
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ues = nt.get_ues_stat_dict(filter_sport=True)
for ue in ues:
ue_acros[ue["acronyme"]] = 1
nb_ue = len(ues)
@ -398,9 +406,8 @@ class SituationEtudParcoursGeneric(object):
if not sem:
code = "" # non inscrit à ce semestre
else:
nt = sco_cache.NotesTableCache.get(
sem["formsemestre_id"]
) # > get_etud_decision_sem
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
decision = nt.get_etud_decision_sem(self.etudid)
if decision:
code = decision["code"]
@ -419,9 +426,7 @@ class SituationEtudParcoursGeneric(object):
self.moy_gen >= (self.parcours.BARRE_MOY - scu.NOTES_TOLERANCE)
)
# conserve etat UEs
ue_ids = [
x["ue_id"] for x in self.nt.get_ues(etudid=self.etudid, filter_sport=True)
]
ue_ids = [x["ue_id"] for x in self.nt.get_ues_stat_dict(filter_sport=True)]
self.ues_status = {} # ue_id : status
for ue_id in ue_ids:
self.ues_status[ue_id] = self.nt.get_etud_ue_status(self.etudid, ue_id)
@ -458,7 +463,7 @@ class SituationEtudParcoursGeneric(object):
prev = None
while i >= 0:
if (
self.sems[i]["formation_code"] == self.formation["formation_code"]
self.sems[i]["formation_code"] == self.formation.formation_code
and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
):
prev = self.sems[i]
@ -470,8 +475,8 @@ class SituationEtudParcoursGeneric(object):
# Verifications basiques:
# ?
# Code etat du semestre precedent:
nt = sco_cache.NotesTableCache.get(prev["formsemestre_id"])
# > get_etud_decision_sem, get_etud_moy_gen, etud_check_conditions_ues
formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
self.prev_decision = nt.get_etud_decision_sem(self.etudid)
self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid)
self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0]
@ -525,11 +530,13 @@ class SituationEtudParcoursGeneric(object):
validated = False
for sem in self.sems:
if (
sem["formation_code"] == self.formation["formation_code"]
sem["formation_code"] == self.formation.formation_code
and sem["semestre_id"] == s
):
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
# > get_etud_decision_sem
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(
formsemestre
)
decision = nt.get_etud_decision_sem(self.etudid)
if decision and code_semestre_validant(decision["code"]):
validated = True
@ -641,7 +648,7 @@ class SituationEtudParcoursGeneric(object):
cnx,
{
"etudid": self.etudid,
"formation_code": self.formation["formation_code"],
"formation_code": self.formation.formation_code,
"semestre_id": next_semestre_id,
"origin_formsemestre_id": self.formsemestre_id,
},
@ -678,10 +685,10 @@ class SituationEtudParcoursECTS(SituationEtudParcoursGeneric):
Dans ce type de parcours, on n'utilise que ADM, AJ, et ADJ (?).
"""
etud_moy_infos = self.nt.get_etud_moy_infos(self.etudid)
etud_ects_infos = self.nt.get_etud_ects_pot(self.etudid)
if (
etud_moy_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
and etud_moy_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
etud_ects_infos["ects_pot"] >= self.parcours.ECTS_BARRE_VALID_YEAR
and etud_ects_infos["ects_pot"] >= self.parcours.ECTS_FONDAMENTAUX_PER_YEAR
):
choices = [
DecisionSem(
@ -903,8 +910,9 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
"""
valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False)
cnx = ndb.GetDBConnexion()
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > get_ues, get_etud_ue_status
ue_ids = [x["ue_id"] for x in nt.get_ues(etudid=etudid, filter_sport=True)]
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
ue_ids = [x["ue_id"] for x in nt.get_ues_stat_dict(filter_sport=True)]
for ue_id in ue_ids:
ue_status = nt.get_etud_ue_status(etudid, ue_id)
if not assiduite:
@ -952,6 +960,9 @@ def do_formsemestre_validate_ue(
is_external=False,
):
"""Ajoute ou change validation UE"""
if semestre_id is None:
ue = UniteEns.query.get_or_404(ue_id)
semestre_id = ue.semestre_idx
args = {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
@ -969,16 +980,18 @@ def do_formsemestre_validate_ue(
if formsemestre_id:
cond += " and formsemestre_id=%(formsemestre_id)s"
if semestre_id:
cond += " and semestre_id=%(semestre_id)s"
cond += " and (semestre_id=%(semestre_id)s or semestre_id is NULL)"
log(f"formsemestre_validate_ue: deleting where {cond}, args={args})")
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
# insert
args["code"] = code
if code == ADM:
if moy_ue is None:
# stocke la moyenne d'UE capitalisée:
moy_ue = nt.get_etud_ue_status(etudid, ue_id)["moy"]
ue_status = nt.get_etud_ue_status(etudid, ue_id)
moy_ue = ue_status["moy"] if ue_status else ""
args["moy_ue"] = moy_ue
log("formsemestre_validate_ue: %s" % args)
log("formsemestre_validate_ue: create %s" % args)
if code != None:
scolar_formsemestre_validation_create(cnx, args)
else:
@ -1000,7 +1013,7 @@ def formsemestre_has_decisions(formsemestre_id):
def etud_est_inscrit_ue(cnx, etudid, formsemestre_id, ue_id):
"""Vrai si l'étudiant est inscrit a au moins un module de cette UE dans ce semestre"""
"""Vrai si l'étudiant est inscrit à au moins un module de cette UE dans ce semestre"""
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""SELECT mi.*
@ -1037,7 +1050,9 @@ def formsemestre_get_autorisation_inscription(etudid, origin_formsemestre_id):
)
def formsemestre_get_etud_capitalisation(sem, etudid):
def formsemestre_get_etud_capitalisation(
formation_id: int, semestre_idx: int, date_debut, etudid: int
) -> list[dict]:
"""Liste des UE capitalisées (ADM) correspondant au semestre sem et à l'étudiant.
Recherche dans les semestres de la même formation (code) avec le même
@ -1055,30 +1070,32 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute(
"""select distinct SFV.*, ue.ue_code from notes_ue ue, notes_formations nf,
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
"""
SELECT DISTINCT SFV.*, ue.ue_code
FROM notes_ue ue, notes_formations nf,
notes_formations nf2, scolar_formsemestre_validation SFV, notes_formsemestre sem
WHERE ue.formation_id = nf.id
WHERE ue.formation_id = nf.id
and nf.formation_code = nf2.formation_code
and nf2.id=%(formation_id)s
and SFV.ue_id = ue.id
and SFV.code = 'ADM'
and SFV.etudid = %(etudid)s
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
and ( (sem.id = SFV.formsemestre_id
and sem.date_debut < %(date_debut)s
and sem.semestre_id = %(semestre_id)s )
or (
((SFV.formsemestre_id is NULL) OR (SFV.is_external)) -- les UE externes ou "anterieures"
AND (SFV.semestre_id is NULL OR SFV.semestre_id=%(semestre_id)s)
) )
""",
{
"etudid": etudid,
"formation_id": sem["formation_id"],
"semestre_id": sem["semestre_id"],
"date_debut": ndb.DateDMYtoISO(sem["date_debut"]),
"formation_id": formation_id,
"semestre_id": semestre_idx,
"date_debut": date_debut,
},
)

Some files were not shown because too many files have changed in this diff Show More