master #320
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -169,5 +169,7 @@ Thumbs.db
|
|||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
copy
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>¬e=<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")
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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]
|
||||
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[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
d[modimpl.module.code] = {
|
||||
"id": modimpl.id,
|
||||
"coef": coef,
|
||||
"moyenne": fmt_note(
|
||||
etud_moy_module[
|
||||
self.modimpl_coefs_df.columns.get_loc(mi.id)
|
||||
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
|
||||
|
|
|
@ -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])
|
||||
# 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)
|
||||
try:
|
||||
x_eval.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(
|
||||
results.modimpls_evals_notes[
|
||||
results.modimpls_results[
|
||||
e.moduleimpl_id
|
||||
][e.id][etud.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)
|
||||
"""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"):
|
||||
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
44
app/comp/aux_stats.py
Normal 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
717
app/comp/bonus_spo.py
Normal 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
|
|
@ -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"
|
||||
|
|
|
@ -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
154
app/comp/jury.py
Normal 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
|
|
@ -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.
|
||||
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).
|
||||
"""
|
||||
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.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 df_load_modimpl_notes(moduleimpl_id: int) -> tuple:
|
||||
"""Construit un dataframe avec toutes les notes de toutes les évaluations du module.
|
||||
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)
|
||||
|
||||
Résultat: (evals_notes, liste de évaluations du moduleimpl,
|
||||
liste de booleens indiquant si l'évaluation est "complete")
|
||||
|
||||
L'ensemble des étudiants est celui des inscrits au SEMESTRE.
|
||||
|
||||
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)
|
||||
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'évaluation "complete" (prise en compte dans les calculs) si:
|
||||
É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)
|
||||
- soit elle a été déclarée "à prise en compte immédiate" (publish_incomplete)
|
||||
|
||||
N'utilise pas de cache ScoDoc.
|
||||
É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.
|
||||
"""
|
||||
# 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:
|
||||
moduleimpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
self.etudids = self._etudids()
|
||||
|
||||
# --- 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,72 +190,105 @@ 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,
|
||||
)
|
||||
# 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
|
||||
* 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
|
||||
|
||||
|
||||
def compute_module_moy(
|
||||
evals_notes_df: pd.DataFrame,
|
||||
class ModuleImplResultsAPC(ModuleImplResults):
|
||||
"Calcul des moyennes de modules à la mode BUT"
|
||||
|
||||
def compute_module_moy(
|
||||
self,
|
||||
evals_poids_df: pd.DataFrame,
|
||||
evaluations: list,
|
||||
evaluations_completes: list,
|
||||
) -> pd.DataFrame:
|
||||
) -> 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.
|
||||
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 à des notes)
|
||||
ou NaN si les évaluations (dans lesquelles l'étudiant a des notes)
|
||||
ne donnent pas de coef vers cette UE.
|
||||
"""
|
||||
nb_etuds, nb_evals = evals_notes_df.shape
|
||||
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)
|
||||
# Coefficients des évaluations, met à zéro ceux des évals incomplètes:
|
||||
evals_coefs = (
|
||||
np.array(
|
||||
[e.coefficient for e in evaluations],
|
||||
dtype=float,
|
||||
)
|
||||
* evaluations_completes
|
||||
).reshape(-1, 1)
|
||||
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)
|
||||
# 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
|
||||
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
|
||||
|
@ -231,17 +296,200 @@ def compute_module_moy(
|
|||
# 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,
|
||||
np.stack([self.evals_notes.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)
|
||||
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)
|
||||
etuds_moy_module_df = pd.DataFrame(
|
||||
etuds_moy_module, index=evals_notes_df.index, columns=evals_poids_df.columns
|
||||
|
||||
# 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,
|
||||
)
|
||||
return etuds_moy_module_df
|
||||
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 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)
|
||||
"""
|
||||
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
|
||||
)
|
||||
# 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
|
||||
|
|
|
@ -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 } où 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 } où 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
|
||||
|
|
|
@ -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,9 +60,20 @@ 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(
|
||||
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)
|
||||
modules = modules.filter_by(semestre_id=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:
|
||||
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]
|
||||
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
|
||||
#
|
||||
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
134
app/comp/res_but.py
Normal 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
34
app/comp/res_cache.py
Normal 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
248
app/comp/res_classic.py
Normal 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
713
app/comp/res_common.py
Normal 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 là
|
||||
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
56
app/comp/res_sem.py
Normal 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]
|
29
app/entreprises/__init__.py
Normal file
29
app/entreprises/__init__.py
Normal 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
289
app/entreprises/forms.py
Normal 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
137
app/entreprises/models.py
Normal 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
858
app/entreprises/routes.py
Normal 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,
|
||||
)
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
76
app/forms/main/config_main.py
Normal file
76
app/forms/main/config_main.py
Normal 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,
|
||||
)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,43 +87,87 @@ 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 là 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:
|
||||
from app.scodoc import bonus_sport
|
||||
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c is None:
|
||||
return None
|
||||
|
@ -134,23 +178,10 @@ class ScoDocSiteConfig(db.Model):
|
|||
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.
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
|
@ -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):
|
||||
|
|
|
@ -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 !")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"),)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
109
app/models/validations.py
Normal 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"),
|
||||
)
|
|
@ -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 []
|
||||
|
|
|
@ -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)
|
||||
|
||||
# ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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:
|
||||
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
|
||||
return nt._modimpls[modimplids.index(modimpl_id)]
|
||||
|
||||
|
||||
# **********************************************
|
||||
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"]
|
||||
|
|
|
@ -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"]]
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
|||
où 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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
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>"""
|
||||
% etud
|
||||
<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 ""
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
if nt.bonus is not None:
|
||||
x = scu.fmt_note(nt.bonus[etudid], keep_numeric=True)
|
||||
else:
|
||||
x = ""
|
||||
if isinstance(x, str):
|
||||
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,8 +617,16 @@ 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 rg[0] is None:
|
||||
mod["mod_rang_txt"] = ""
|
||||
else:
|
||||
if mod_attente: # nt.get_moduleimpls_attente():
|
||||
mod["mod_rang"] = scu.RANG_ATTENTE_STR
|
||||
else:
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
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)
|
||||
if matiere is None:
|
||||
raise ScoValueError("invalid matiere !")
|
||||
ue = matiere.ue
|
||||
parcours = ue.formation.get_parcours()
|
||||
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,11 +732,13 @@ def module_edit(module_id=None):
|
|||
)
|
||||
)
|
||||
else:
|
||||
# l'UE peut changer
|
||||
# l'UE de rattachement peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
old_ue_id = a_module.ue.id
|
||||
new_ue_id = int(tf[2]["ue_id"])
|
||||
if (old_ue_id != new_ue_id) and in_use:
|
||||
new_ue = UniteEns.query.get_or_404(new_ue_id)
|
||||
if new_ue.semestre_idx != a_module.ue.semestre_idx:
|
||||
# pas changer de semestre un module utilisé !
|
||||
raise ScoValueError(
|
||||
"Module utilisé: il ne peut pas être changé de semestre !"
|
||||
|
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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,6 +301,9 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
"type": "int",
|
||||
},
|
||||
),
|
||||
]
|
||||
if can_change_semestre_id:
|
||||
form_descr += [
|
||||
(
|
||||
"semestre_idx",
|
||||
{
|
||||
|
@ -296,11 +311,18 @@ def ue_edit(ue_id=None, create=False, formation_id=None):
|
|||
"type": "int",
|
||||
"allow_null": False,
|
||||
"title": parcours.SESSION_NAME.capitalize(),
|
||||
"explanation": "%s de l'UE dans la formation" % parcours.SESSION_NAME,
|
||||
"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} <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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,6 +890,50 @@ def fill_etuds_info(etuds):
|
|||
etud.update(adr)
|
||||
format_etud_ident(etud)
|
||||
|
||||
etud.update(etud_inscriptions_infos(etudid, etud["ne"]))
|
||||
|
||||
# nettoyage champs souvent vides
|
||||
if etud.get("nomlycee"):
|
||||
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
|
||||
if etud["villelycee"]:
|
||||
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
|
||||
etud["ilycee"] += "<br/>"
|
||||
else:
|
||||
if etud.get("codelycee"):
|
||||
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
|
||||
else:
|
||||
etud["ilycee"] = ""
|
||||
rap = ""
|
||||
if etud.get("rapporteur") or etud.get("commentaire"):
|
||||
rap = "Note du rapporteur"
|
||||
if etud.get("rapporteur"):
|
||||
rap += " (%s)" % etud["rapporteur"]
|
||||
rap += ": "
|
||||
if etud.get("commentaire"):
|
||||
rap += "<em>%s</em>" % etud["commentaire"]
|
||||
etud["rap"] = rap
|
||||
|
||||
# if etud['boursier_prec']:
|
||||
# pass
|
||||
|
||||
if etud.get("telephone"):
|
||||
etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
|
||||
else:
|
||||
etud["telephonestr"] = ""
|
||||
if etud.get("telephonemobile"):
|
||||
etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
|
||||
etud["telephonemobile"]
|
||||
)
|
||||
else:
|
||||
etud["telephonemobilestr"] = ""
|
||||
|
||||
|
||||
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}
|
||||
|
@ -933,13 +958,10 @@ def fill_etuds_info(etuds):
|
|||
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)
|
||||
etud["situation"] = descr_situation_etud(etudid, ne)
|
||||
else:
|
||||
if etud["sems"]:
|
||||
if etud["sems"][0]["dateord"] > time.strftime(
|
||||
"%Y-%m-%d", time.localtime()
|
||||
):
|
||||
if etud["sems"][0]["dateord"] > time.strftime("%Y-%m-%d", time.localtime()):
|
||||
etud["inscription"] = "futur"
|
||||
etud["situation"] = "futur élève"
|
||||
else:
|
||||
|
@ -950,47 +972,12 @@ def fill_etuds_info(etuds):
|
|||
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"] = "?"
|
||||
|
||||
# nettoyage champs souvents vides
|
||||
if etud["nomlycee"]:
|
||||
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
|
||||
if etud["villelycee"]:
|
||||
etud["ilycee"] += " (%s)" % etud["villelycee"]
|
||||
etud["ilycee"] += "<br/>"
|
||||
else:
|
||||
if etud["codelycee"]:
|
||||
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
|
||||
else:
|
||||
etud["ilycee"] = ""
|
||||
rap = ""
|
||||
if etud["rapporteur"] or etud["commentaire"]:
|
||||
rap = "Note du rapporteur"
|
||||
if etud["rapporteur"]:
|
||||
rap += " (%s)" % etud["rapporteur"]
|
||||
rap += ": "
|
||||
if etud["commentaire"]:
|
||||
rap += "<em>%s</em>" % etud["commentaire"]
|
||||
etud["rap"] = rap
|
||||
|
||||
# if etud['boursier_prec']:
|
||||
# pass
|
||||
|
||||
if etud["telephone"]:
|
||||
etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
|
||||
else:
|
||||
etud["telephonestr"] = ""
|
||||
if etud["telephonemobile"]:
|
||||
etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
|
||||
etud["telephonemobile"]
|
||||
)
|
||||
else:
|
||||
etud["telephonemobilestr"] = ""
|
||||
return etud
|
||||
|
||||
|
||||
def descr_situation_etud(etudid, ne=""):
|
||||
"""chaine decrivant la situation actuelle de l'etudiant"""
|
||||
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":
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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é ?
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>""",
|
||||
]
|
||||
|
|
|
@ -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:
|
||||
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"])
|
||||
# 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
|
||||
)
|
||||
if m["formation_id"] != formation_id:
|
||||
log(
|
||||
"XXX createwithmodules: formation_id=%s\n\tm=%s"
|
||||
% (formation_id, str(m))
|
||||
.order_by(Module.module_type, UniteEns.numero, Module.numero)
|
||||
.all()
|
||||
)
|
||||
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
|
||||
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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(" 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" en {sem.modalite}")
|
||||
if sem.etapes:
|
||||
H.append(
|
||||
" (étape <b><tt>%s</tt></b>)"
|
||||
% (sem["etapes_apo_str"] or "-")
|
||||
f""" (é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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ], où 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()
|
||||
|
|
|
@ -302,7 +302,12 @@ class DisplayedGroupsInfos(object):
|
|||
if group_ids:
|
||||
group_ids = [group_ids] # cas ou un seul parametre, pas de liste
|
||||
else:
|
||||
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,
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
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
|
||||
|
|
|
@ -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>")
|
||||
|
||||
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
"""
|
||||
+ "\n".join(
|
||||
[
|
||||
f"""<div style="--coef:{coef}"><div>{coef}</div>{ue_acronyme}</div>"""
|
||||
for ue_acronyme, coef in coefs_descr
|
||||
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>'
|
||||
)
|
||||
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
|
||||
'<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(
|
||||
# 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>"""
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,7 +1070,9 @@ 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,
|
||||
"""
|
||||
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
|
||||
|
@ -1076,9 +1093,9 @@ def formsemestre_get_etud_capitalisation(sem, etudid):
|
|||
""",
|
||||
{
|
||||
"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
Loading…
Reference in New Issue
Block a user