Merge branch 'master' into prepajury9

This commit is contained in:
Jean-Marie Place 2023-01-26 11:54:38 +01:00
commit 60866d530e
58 changed files with 1602 additions and 288 deletions

View File

@ -550,3 +550,22 @@ def scodoc_flash_status_messages():
f"Mode test: mails redirigés vers {email_test_mode_address}",
category="warning",
)
def critical_error(msg):
"""Handle a critical error: flush all caches, display message to the user"""
import app.scodoc.sco_utils as scu
log(f"\n*** CRITICAL ERROR: {msg}")
send_scodoc_alarm(f"CRITICAL ERROR: {msg}", msg)
clear_scodoc_cache()
raise ScoValueError(
f"""
Une erreur est survenue.
Si le problème persiste, merci de contacter le support ScoDoc via
{scu.SCO_DISCORD_ASSISTANCE}
{msg}
"""
)

View File

@ -19,6 +19,7 @@ from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import GroupDescr, Partition
from app.models.groups import group_membership
from app.scodoc import sco_cache
from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
@ -170,24 +171,15 @@ def set_etud_group(etudid: int, group_id: int):
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
return json_error(404, "etud non inscrit au formsemestre du groupe")
groups = (
GroupDescr.query.filter_by(partition_id=group.partition.id)
.join(group_membership)
.filter_by(etudid=etudid)
sco_groups.change_etud_group_in_partition(
etudid, group_id, group.partition.to_dict()
)
ok = False
for other_group in groups:
if other_group.id == group_id:
ok = True
else:
other_group.etuds.remove(etud)
if not ok:
group.etuds.append(etud)
log(f"set_etud_group({etud}, {group})")
db.session.commit()
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
return jsonify({"group_id": group_id, "etudid": etudid})
@ -207,6 +199,8 @@ def group_remove_etud(group_id: int, etudid: int):
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if etud in group.etuds:
group.etuds.remove(etud)
db.session.commit()
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
groups = (
GroupDescr.query.filter_by(partition_id=partition_id)
.join(group_membership)
@ -262,8 +258,10 @@ def group_create(partition_id: int):
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.groups_editable:
return json_error(404, "partition non editable")
return json_error(403, "partition non editable")
data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name")
if group_name is None:
@ -294,8 +292,10 @@ def group_delete(group_id: int):
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.groups_editable:
return json_error(404, "partition non editable")
return json_error(403, "partition non editable")
formsemestre_id = group.partition.formsemestre_id
log(f"deleting {group}")
db.session.delete(group)
@ -318,8 +318,10 @@ def group_edit(group_id: int):
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.groups_editable:
return json_error(404, "partition non editable")
return json_error(403, "partition non editable")
data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name")
if group_name is not None:
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
if not formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
data = request.get_json(force=True) # may raise 400 Bad Request
partition_name = data.get("partition_name")
if partition_name is None:
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
if not formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, int) and not all(
isinstance(x, int) for x in partition_ids
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, int) and not all(
isinstance(x, int) for x in group_ids
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
data = request.get_json(force=True) # may raise 400 Bad Request
modified = False
partition_name = data.get("partition_name")
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.partition_name:
return json_error(404, "ne peut pas supprimer la partition par défaut")
is_parcours = partition.is_parcours()

View File

@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
avec la même interface.
"""
import collections
from typing import Union
from flask import g, url_for
@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException, ScoValueError
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT
@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
def parcours_validated(self):
"True si le parcours est validé"
return False # XXX TODO
class EtudCursusBUT:
"""L'état de l'étudiant dans son cursus BUT
Liste des niveaux validés/à valider
"""
def __init__(self, etud: Identite, formation: Formation):
"""formation indique la spécialité préparée"""
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
if formation.id not in (
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
):
raise ScoValueError(
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
)
if not formation.referentiel_competence:
raise ScoNoReferentielCompetences(formation=formation)
#
self.etud = etud
self.formation = formation
self.inscriptions = sorted(
[
ins
for ins in etud.formsemestre_inscriptions
if ins.formsemestre.formation.referentiel_competence
and (
ins.formsemestre.formation.referentiel_competence.id
== formation.referentiel_competence.id
)
],
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
)
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
"{ annee : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
annee, self.parcour
)[1]
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
niveaux_d[self.parcour.id] if self.parcour else []
)
self.niveaux.update(
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
)
# Probablement inutile:
# # Cherche les validations de jury enregistrées pour chaque niveau
# self.validations_by_niveau = collections.defaultdict(lambda: [])
# " { niveau_id : [ ApcValidationRCUE ] }"
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
# self.validations_by_niveau[validation_rcue.niveau().id].append(
# validation_rcue
# )
# self.validation_by_niveau = {
# niveau_id: sorted(
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
# )[0]
# for niveau_id, validations in self.validations_by_niveau.items()
# }
# "{ niveau_id : meilleure validation pour ce niveau }"
self.validation_par_competence_et_annee = {}
"{ competence_id : { 'BUT1' : validation_rcue, ... } }"
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
niveau = validation_rcue.niveau()
if not niveau.competence.id in self.validation_par_competence_et_annee:
self.validation_par_competence_et_annee[niveau.competence.id] = {}
previous_validation = self.validation_par_competence_et_annee.get(
niveau.competence.id
).get(validation_rcue.annee())
# prend la "meilleure" validation
if (not previous_validation) or (
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
> sco_codes.BUT_CODES_ORDERED[previous_validation.code]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
] = validation_rcue
self.competences = {
competence.id: competence
for competence in (
self.parcour.query_competences()
if self.parcour
else self.formation.referentiel_competence.get_competences_tronc_commun()
)
}
"cache { competence_id : competence }"
def to_dict(self):
"""
{
competence_id : {
annee : meilleure_validation
}
}
"""
return {
competence.id: {
annee: {
self.validation_par_competence_et_annee.get(competence.id, {}).get(
annee
)
}
for annee in ("BUT1", "BUT2", "BUT3")
}
for competence in self.competences.values()
}

View File

@ -64,7 +64,7 @@ import re
from typing import Union
import numpy as np
from flask import g, url_for
from flask import flash, g, url_for
from app import db
from app import log
@ -554,7 +554,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"""Liste des regroupements d'UE à considérer cette année.
On peut avoir un RCUE à cheval sur plusieurs années (redoublants avec UE capitalisées).
Si on n'a pas les deux semestres, aucun RCUE.
Raises ScoValueError s'il y a des UE sans RCUE. <= ??? XXX
"""
if self.formsemestre_pair is None or self.formsemestre_impair is None:
return []
@ -570,6 +569,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
not in CODES_UE_VALIDES
):
continue # ignore cette UE antérieure non capitalisée
# et l'UE impaire doit être actuellement meilleure que
# celle éventuellement capitalisée
if self.decisions_ues[ue_impair.id].ue_status["is_capitalized"]:
continue # ignore cette UE car capitalisée et actuelle moins bonne
if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
rcue = RegroupementCoherentUE(
self.etud,
@ -690,20 +693,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
db.session.commit()
def record(self, code: str, no_overwrite=False):
def record(self, code: str, no_overwrite=False) -> bool:
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
Si no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien.
"""
if self.inscription_etat != scu.INSCRIT:
return
return False
if code and not code in self.codes:
raise ScoValueError(
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
return False # no change
if self.validation:
db.session.delete(self.validation)
db.session.commit()
@ -743,9 +746,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
next_semestre_id,
)
self.recorded = True
db.session.commit()
self.recorded = True
self.invalidate_formsemestre_cache()
return True
def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres"
@ -756,13 +760,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id)
def record_all(self, no_overwrite: bool = True):
def record_all(
self, no_overwrite: bool = True, only_validantes: bool = False
) -> bool:
"""Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique".
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente.
- Pour les RCUE: n'enregistre que si la nouvelle décision est plus favorable que l'ancienne.
Si only_validantes, n'enregistre que des décisions "validantes" de droit: ADM ou CMP.
Return: True si au moins un code modifié et enregistré.
"""
# Toujours valider dans l'ordre UE, RCUE, Année:
modif = False
# Toujours valider dans l'ordre UE, RCUE, Année
annee_scolaire = self.formsemestre.annee_scolaire()
# UEs
for dec_ue in self.decisions_ues.values():
@ -771,25 +782,40 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
# rappel: le code par défaut est en tête
code = dec_ue.codes[0] if dec_ue.codes else None
# enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml
dec_ue.record(code, no_overwrite=no_overwrite)
# RCUE : enregistre seulement si pas déjà validé "mieux"
if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
# enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml
modif |= dec_ue.record(code, no_overwrite=no_overwrite)
# RCUE :
for dec_rcue in self.decisions_rcue_by_niveau.values():
code = dec_rcue.codes[0] if dec_rcue.codes else None
if (not dec_rcue.recorded) and (
(not dec_rcue.validation)
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
< BUT_CODES_ORDERED.get(code, 0)
if (
(not dec_rcue.recorded)
and ( # enregistre seulement si pas déjà validé "mieux"
(not dec_rcue.validation)
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0)
< BUT_CODES_ORDERED.get(code, 0)
)
and ( # décision validante de droit ?
(
(not only_validantes)
or code in sco_codes.CODES_RCUE_VALIDES_DE_DROIT
)
)
):
dec_rcue.record(code, no_overwrite=no_overwrite)
modif |= dec_rcue.record(code, no_overwrite=no_overwrite)
# Année:
if not self.recorded:
# rappel: le code par défaut est en tête
code = self.codes[0] if self.codes else None
# enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml
self.record(code, no_overwrite=no_overwrite)
if (
not only_validantes
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
modif |= self.record(code, no_overwrite=no_overwrite)
return modif
def erase(self, only_one_sem=False):
"""Efface les décisions de jury de cet étudiant
@ -1002,23 +1028,23 @@ class DecisionsProposeesRCUE(DecisionsProposees):
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}"""
def record(self, code: str, no_overwrite=False):
def record(self, code: str, no_overwrite=False) -> bool:
"""Enregistre le code RCUE.
Note:
- si le RCUE est ADJ, les UE non validées sont passées à ADJ
XXX on pourra imposer ici d'autres règles de cohérence
"""
if self.rcue is None:
return # pas de RCUE a enregistrer
return False # pas de RCUE a enregistrer
if self.inscription_etat != scu.INSCRIT:
return
return False
if code and not code in self.codes:
raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}"
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
return False # no change
parcours_id = self.parcour.id if self.parcour is not None else None
if self.validation:
db.session.delete(self.validation)
@ -1051,7 +1077,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
dec_ue = deca.decisions_ues.get(ue_id)
if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
log(f"rcue.record: force ADJR sur {dec_ue}")
dec_ue.record("ADJR")
flash(
f"""UEs du RCUE "{dec_ue.ue.niveau_competence.competence.titre}" passées en ADJR"""
)
dec_ue.record(sco_codes.ADJR)
# Valide les niveaux inférieurs de la compétence (code ADSUP)
# TODO
if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre(
@ -1063,6 +1095,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
)
self.code_valide = code # mise à jour état
self.recorded = True
return True
def erase(self):
"""Efface la décision de jury de cet étudiant pour cet RCUE"""
@ -1194,9 +1227,10 @@ class DecisionsProposeesUE(DecisionsProposees):
self.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes"
def record(self, code: str, no_overwrite=False):
def record(self, code: str, no_overwrite=False) -> bool:
"""Enregistre le code jury pour cette UE.
Si no_overwrite, n'enregistre pas s'il y a déjà un code.
Return: True si code enregistré (modifié)
"""
if code and not code in self.codes:
raise ScoValueError(
@ -1204,7 +1238,7 @@ class DecisionsProposeesUE(DecisionsProposees):
)
if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True
return # no change
return False # no change
self.erase()
if code is None:
self.validation = None
@ -1235,6 +1269,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
self.code_valide = code # mise à jour
self.recorded = True
return True
def erase(self):
"""Efface la décision de jury de cet étudiant pour cette UE"""

View File

@ -284,6 +284,10 @@ class RowCollector:
self["_nom_disp_order"] = etud.sort_key
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
self["_nom_short_data"] = {
"etudid": etud.id,
"nomprenom": etud.nomprenom,
}
if with_links:
self["_nom_short_order"] = etud.sort_key
self["_nom_short_target"] = url_for(
@ -368,10 +372,6 @@ class RowCollector:
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
"col_rcue col_rcues_validables" + klass,
)
self["_rcues_validables_data"] = {
"etudid": deca.etud.id,
"nomprenom": deca.etud.nomprenom,
}
if len(deca.rcues_annee) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:

View File

@ -18,29 +18,29 @@ from app.scodoc.sco_exceptions import ScoValueError
def formsemestre_validation_auto_but(
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
) -> int:
"""Calcul automatique des décisions de jury sur une année BUT.
Ne modifie jamais de décisions de l'année scolaire précédente, même
"""Calcul automatique des décisions de jury sur une "année" BUT.
- N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval".
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit).
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc
(mode à n'utiliser que pour les tests)
- Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
ce qui est utilisé pour certains tests unitaires).
- Normalement, only_adm est True et on n'enregistre que les décisions validantes
de droit: ADM ou CMP.
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
Si no_overwrite est vrai (défaut), ne -écrit jamais les codes déjà enregistrés
(utiliser faux pour certains tests)
Returns: nombre d'étudiants "admis"
Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
"""
if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0
nb_etud_modif = 0
with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.admis: # année réussie
nb_admis += 1
if deca.admis or not only_adm:
deca.record_all(no_overwrite=no_overwrite)
nb_etud_modif += deca.record_all(
no_overwrite=no_overwrite, only_validantes=only_adm
)
db.session.commit()
return nb_admis
return nb_etud_modif

View File

@ -196,7 +196,7 @@ def _gen_but_niveau_ue(
<div>UE en cours
{ "sans notes" if np.isnan(dec_ue.moy_ue)
else
("avec moyenne" + scu.fmt_note(dec_ue.moy_ue))
("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
}
</div>
</div>
@ -205,9 +205,10 @@ def _gen_but_niveau_ue(
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
if dec_ue.code_valide:
scoplement = f"""<div class="scoplement">
Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
<div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")}
à {dec_ue.validation.event_date.strftime("%Hh%M")}
</div>
</div>
"""
else:
scoplement = ""

View File

@ -39,6 +39,7 @@ from dataclasses import dataclass
import numpy as np
import pandas as pd
import app
from app import db
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
from app.scodoc import sco_cache
@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
if nb_etuds == 0:
return pd.Series()
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1)
assert evals_coefs.shape == (nb_evals,)
if evals_coefs.shape != (nb_evals,):
app.critical_error("compute_module_moy: vals_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

View File

@ -543,6 +543,10 @@ class ResultatsSemestre(ResultatsCache):
formsemestre_id=self.formsemestre.id,
etudid=etudid,
)
row["_nom_short_data"] = {
"etudid": etud.id,
"nomprenom": etud.nomprenom,
}
row["_nom_short_target_attrs"] = f'class="etudinfo" id="{etudid}"'
row["_nom_disp_target"] = row["_nom_short_target"]
row["_nom_disp_target_attrs"] = row["_nom_short_target_attrs"]
@ -905,7 +909,7 @@ class ResultatsSemestre(ResultatsCache):
}
first = True
for i, cid in enumerate(fields):
titles[f"_{cid}_col_order"] = 10000 + i # tout à droite
titles[f"_{cid}_col_order"] = 100000 + i # tout à droite
if first:
titles[f"_{cid}_class"] = "admission admission_first"
first = False

View File

@ -16,6 +16,7 @@ import flask_login
import app
from app.auth.models import User
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
class ZUser(object):
@ -180,19 +181,24 @@ def scodoc7func(func):
else:
arg_names = argspec.args
for arg_name in arg_names: # pour chaque arg de la fonction vue
if arg_name == "REQUEST": # ne devrait plus arriver !
# debug check, TODO remove after tests
raise ValueError("invalid REQUEST parameter !")
else:
# peut produire une KeyError s'il manque un argument attendu:
v = req_args[arg_name]
# try to convert all arguments to INTEGERS
# necessary for db ids and boolean values
try:
v = int(v)
except (ValueError, TypeError):
pass
pos_arg_values.append(v)
# peut produire une KeyError s'il manque un argument attendu:
v = req_args[arg_name]
# try to convert all arguments to INTEGERS
# necessary for db ids and boolean values
try:
v = int(v) if v else v
except (ValueError, TypeError) as exc:
if arg_name in {
"etudid",
"formation_id",
"formsemestre_id",
"module_id",
"moduleimpl_id",
"partition_id",
"ue_id",
}:
raise ScoValueError("page introuvable (id invalide)") from exc
pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)
# current_app.logger.info("req_args=%s" % req_args)
# Add keyword arguments

View File

@ -63,6 +63,7 @@ class CodesDecisionsForm(FlaskForm):
ABL = _build_code_field("ABL")
ADC = _build_code_field("ADC")
ADJ = _build_code_field("ADJ")
ADJR = _build_code_field("ADJR")
ADM = _build_code_field("ADM")
AJ = _build_code_field("AJ")
ATB = _build_code_field("ATB")

View File

@ -94,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return ""
return self.version_orebut.split()[0]
def to_dict(self):
def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True):
"""Représentation complète du ref. de comp.
comme un dict.
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
"""
return {
"dept_id": self.dept_id,
@ -111,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
if self.scodoc_date_loaded
else "",
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences},
"parcours": {x.code: x.to_dict() for x in self.parcours},
"competences": {
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
for x in self.competences
},
"parcours": {
x.code: x.to_dict()
for x in (self.parcours if parcours is None else parcours)
},
}
def get_niveaux_by_parcours(
@ -174,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return parcours, niveaux_by_parcours_no_tc
def get_competences_tronc_commun(self) -> list["ApcCompetence"]:
"""Liste des compétences communes à tous les parcours du référentiel."""
parcours = self.parcours.all()
if not parcours:
return []
ids = set.intersection(
*[
{competence.id for competence in parcour.query_competences()}
for parcour in parcours
]
)
return sorted(
[
competence
for competence in parcours[0].query_competences()
if competence.id in ids
],
key=lambda c: c.numero or 0,
)
class ApcCompetence(db.Model, XMLModel):
"Compétence"
@ -215,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre!r}>"
def to_dict(self):
def to_dict(self, with_app_critiques=True):
"repr dict recursive sur situations, composantes, niveaux"
return {
"id_orebut": self.id_orebut,
@ -227,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
"composantes_essentielles": [
x.to_dict() for x in self.composantes_essentielles
],
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
"niveaux": {
x.annee: x.to_dict(with_app_critiques=with_app_critiques)
for x in self.niveaux
},
}
def to_dict_bul(self) -> dict:
@ -293,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel):
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
def to_dict(self):
"as a dict, recursif sur les AC"
def to_dict(self, with_app_critiques=True):
"as a dict, recursif (ou non) sur les AC"
return {
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {},
}
def to_dict_bul(self):
@ -471,6 +504,14 @@ class ApcParcours(db.Model, XMLModel):
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
return d
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
"Les compétences associées à ce parcours"
return (
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
.filter_by(parcours_id=self.id)
.order_by(ApcCompetence.numero)
)
class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)

View File

@ -71,11 +71,22 @@ class ApcValidationRCUE(db.Model):
<em>enregistrée le {self.date.strftime("%d/%m/%Y")}
à {self.date.strftime("%Hh%M")}</em>"""
def annee(self) -> str:
"""l'année BUT concernée: "BUT1", "BUT2" ou "BUT3" """
niveau = self.niveau()
return niveau.annee if niveau else None
def niveau(self) -> ApcNiveau:
"""Le niveau de compétence associé à cet RCUE."""
# Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence
def to_dict(self):
"as a dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def to_dict_bul(self) -> dict:
"Export dict pour bulletins: le code et le niveau de compétence"
niveau = self.niveau()

View File

@ -55,7 +55,8 @@ class Formation(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
def to_html(self) -> str:
"titre complet pour affichage"

View File

@ -63,51 +63,51 @@ class FormSemestre(db.Model):
"False si verrouillé"
modalite = db.Column(
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
) # "FI", "FAP", "FC", ...
# gestion compensation sem DUT:
)
"Modalité de formation: 'FI', 'FAP', 'FC', ..."
gestion_compensation = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# ne publie pas le bulletin XML ou JSON:
"gestion compensation sem DUT (inutilisé en APC)"
bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# Bloque le calcul des moyennes (générale et d'UE)
"ne publie pas le bulletin XML ou JSON"
block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# Bloque le calcul de la moyenne générale (utile pour BUT)
"Bloque le calcul des moyennes (générale et d'UE)"
block_moyenne_generale = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
"Si vrai, la moyenne générale indicative BUT n'est pas calculée"
# semestres decales (pour gestion jurys):
gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# couleur fond bulletins HTML:
"Semestres décalés (pour gestion jurys DUT, pas implémenté ou utile en BUT)"
bul_bgcolor = db.Column(
db.String(SHORT_STR_LEN),
default="white",
server_default="white",
nullable=False,
)
# autorise resp. a modifier semestre:
"couleur fond bulletins HTML"
resp_can_edit = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false"
)
# autorise resp. a modifier slt les enseignants:
"autorise resp. à modifier le formsemestre"
resp_can_change_ens = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
)
# autorise les ens a creer des evals:
"autorise resp. a modifier slt les enseignants"
ens_can_edit_eval = db.Column(
db.Boolean(), nullable=False, default=False, server_default="False"
)
# code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'
"autorise les enseignants à créer des évals dans leurs modimpls"
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
# code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
elt_annee_apo = db.Column(db.Text())
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
# Relations:
etapes = db.relationship(

View File

@ -87,6 +87,7 @@ class Partition(db.Model):
def to_dict(self, with_groups=False) -> dict:
"""as a dict, with or without groups"""
d = dict(self.__dict__)
d["partition_id"] = self.id
d.pop("_sa_instance_state", None)
d.pop("formsemestre", None)

View File

@ -111,6 +111,7 @@ class UniteEns(db.Model):
e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
e["parcour"] = self.parcour.to_dict() if self.parcour else None
if with_module_ue_coefs:
if convert_objects:
e["module_ue_coefs"] = [
@ -219,6 +220,8 @@ class UniteEns(db.Model):
db.session.add(self)
db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_niveau_competence( {self}, {niveau} )")
def set_parcour(self, parcour: ApcParcours):
@ -246,6 +249,8 @@ class UniteEns(db.Model):
self.niveau_competence = None
db.session.add(self)
db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_parcour( {self}, {parcour} )")

View File

@ -459,8 +459,7 @@ class JuryPE(object):
etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
(_, parcours) = sco_report.get_codeparcoursetud(etud)
if (
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values()))
> 0
len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
): # Eliminé car NAR apparait dans le parcours
reponse = True
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
@ -563,9 +562,8 @@ class JuryPE(object):
dec = nt.get_etud_decision_sem(
etudid
) # quelle est la décision du jury ?
if dec and dec["code"] in list(
sco_codes_parcours.CODES_SEM_VALIDES.keys()
): # isinstance( sesMoyennes[i+1], float) and
if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
# isinstance( sesMoyennes[i+1], float) and
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
leFid = sem["formsemestre_id"]
else:

View File

@ -1084,7 +1084,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
recipients = [recipient_addr]
sender = sco_preferences.get_preference("email_from_addr", formsemestre_id)
if copy_addr:
bcc = copy_addr.strip()
bcc = copy_addr.strip().split(",")
else:
bcc = ""
@ -1094,7 +1094,7 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
subject,
sender,
recipients,
bcc=[bcc],
bcc=bcc,
text_body=hea,
attachments=[
{"filename": filename, "mimetype": scu.PDF_MIMETYPE, "data": pdfdata}

View File

@ -187,16 +187,23 @@ CODES_EXPL = {
# Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
CODES_SEM_VALIDES_DE_DROIT = {ADM, ADC}
CODES_SEM_VALIDES = CODES_SEM_VALIDES_DE_DROIT | {ADJ} # semestre validé
CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR: 1} # reorientation
CODES_SEM_REO = {NAR} # reorientation
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
"UE validée"
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
"Niveau RCUE validé"
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True, ADJR: True} # UE validée
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
# Pour le BUT:
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
@ -226,17 +233,17 @@ BUT_CODES_ORDERED = {
def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre"
return CODES_SEM_VALIDES.get(code, False)
return code in CODES_SEM_VALIDES
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)
return code in CODES_SEM_ATTENTES
def code_ue_validant(code: str) -> bool:
"Vrai si ce code d'UE est validant (ie attribue les ECTS)"
return CODES_UE_VALIDES.get(code, False)
return code in CODES_UE_VALIDES
DEVENIR_EXPL = {

View File

@ -890,7 +890,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
car ils ne dépendent que de la note d'UE et de la validation ou non du semestre.
Les UE des semestres NON ASSIDUS ne sont jamais validées (code AJ).
"""
valid_semestre = CODES_SEM_VALIDES.get(code_etat_sem, False)
valid_semestre = code_etat_sem in CODES_SEM_VALIDES
cnx = ndb.GetDBConnexion()
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

View File

@ -99,7 +99,7 @@ def html_edit_formation_apc(
H = [
render_template(
"pn/form_ues.html",
"pn/form_ues.j2",
formation=formation,
semestre_ids=semestre_ids,
editable=editable,
@ -122,7 +122,7 @@ def html_edit_formation_apc(
).first()
H += [
render_template(
"pn/form_mods.html",
"pn/form_mods.j2",
formation=formation,
titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource",
@ -138,7 +138,7 @@ def html_edit_formation_apc(
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template(
"pn/form_mods.html",
"pn/form_mods.j2",
formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ",
@ -154,7 +154,7 @@ def html_edit_formation_apc(
if ues_by_sem[semestre_idx].count() > 0
else "",
render_template(
"pn/form_mods.html",
"pn/form_mods.j2",
formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module",
@ -196,7 +196,7 @@ def html_ue_infos(ue):
and ue.matieres.count() == 0
)
return render_template(
"pn/ue_infos.html",
"pn/ue_infos.j2",
titre=f"UE {ue.acronyme} {ue.titre}",
ue=ue,
formsemestres=formsemestres,

View File

@ -723,7 +723,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.js",
],
page_title=f"Programme {formation.acronyme}",
page_title=f"Programme {formation.acronyme} v{formation.version}",
),
f"""<h2>{formation.to_html()} {lockicon}
</h2>
@ -765,7 +765,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
# Description de la formation
H.append(
render_template(
"pn/form_descr.html",
"pn/form_descr.j2",
formation=formation,
parcours=parcours,
editable=editable,
@ -913,8 +913,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml')
}">Export XML de la formation</a>
(permet de la sauvegarder pour l'échanger avec un autre site)
}">Export XML de la formation</a> ou
<a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml', export_codes_apo=0)
}">sans codes Apogée</a>
(permet de l'enregistrer pour l'échanger avec un autre site)
</li>
<li><a class="stdlink" href="{

View File

@ -109,6 +109,7 @@ def formation_export(
export_ids=False,
export_tags=True,
export_external_ues=False,
export_codes_apo=True,
format=None,
):
"""Get a formation, with UE, matieres, modules
@ -116,30 +117,45 @@ def formation_export(
"""
formation: Formation = Formation.query.get_or_404(formation_id)
f_dict = formation.to_dict(with_refcomp_attrs=True)
selector = {"formation_id": formation_id}
if not export_ids:
del f_dict["formation_id"]
del f_dict["dept_id"]
ues = formation.ues
if not export_external_ues:
selector["is_external"] = False
ues = sco_edit_ue.ue_list(selector)
f_dict["ue"] = ues
for ue_dict in ues:
ue_id = ue_dict["ue_id"]
ues = ues.filter_by(is_external=False)
ues = ues.all()
ues.sort(key=lambda u: (u.semestre_idx or 0, u.numero or 0, u.acronyme))
f_dict["ue"] = []
for ue in ues:
ue_dict = ue.to_dict()
f_dict["ue"].append(ue_dict)
ue_dict.pop("module_ue_coefs", None)
if formation.is_apc():
# BUT: indique niveau de compétence associé à l'UE
ue = UniteEns.query.get(ue_id)
if ue.niveau_competence:
ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
ue_dict["reference"] = ue_id # pour les coefficients
# Et le parcour:
if ue.parcour:
ue_dict["parcour"] = [ue.parcour.to_dict(with_annees=False)]
ue_dict["reference"] = ue.id # pour les coefficients
if not export_ids:
del ue_dict["id"]
del ue_dict["ue_id"]
del ue_dict["formation_id"]
if "niveau_competence_id" in ue_dict:
del ue_dict["niveau_competence_id"]
for id_id in (
"id",
"ue_id",
"formation_id",
"parcour_id",
"niveau_competence_id",
):
ue_dict.pop(id_id, None)
if not export_codes_apo:
ue_dict.pop("code_apogee", None)
if ue_dict["ects"] is None:
del ue_dict["ects"]
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
mats.sort(key=lambda m: m["numero"] or 0)
ue_dict["matiere"] = mats
for mat in mats:
matiere_id = mat["matiere_id"]
@ -148,6 +164,7 @@ def formation_export(
del mat["matiere_id"]
del mat["ue_id"]
mods = sco_edit_module.module_list({"matiere_id": matiere_id})
mods.sort(key=lambda m: (m["numero"] or 0, m["code"]))
mat["module"] = mods
for mod in mods:
module_id = mod["module_id"]
@ -183,6 +200,8 @@ def formation_export(
del mod["matiere_id"]
del mod["module_id"]
del mod["formation_id"]
if not export_codes_apo:
del mod["code_apogee"]
if mod["ects"] is None:
del mod["ects"]
@ -323,14 +342,30 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
referentiel_competence_id, ue_info[1]
)
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
ue: UniteEns = UniteEns.query.get(ue_id)
assert ue
if xml_ue_id:
ues_old2new[xml_ue_id] = ue_id
# élément optionnel présent dans les exports BUT:
ue_reference = ue_info[1].get("reference")
if ue_reference:
ue_reference_to_id[int(ue_reference)] = ue_id
# -- create matieres
for mat_info in ue_info[2]:
if mat_info[0] == "parcour":
# Parcours (BUT)
code_parcours = mat_info[1]["code"]
parcour = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcour:
ue.parcour = parcour
db.session.add(ue)
else:
log(f"Warning: parcours {code_parcours} inexistant !")
continue
assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
@ -382,12 +417,12 @@ def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
# associe les parcours de ce module (BUT)
if referentiel_competence_id is not None:
code_parcours = child[1]["code"]
parcours = ApcParcours.query.filter_by(
parcour = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcours:
module.parcours.append(parcours)
if parcour:
module.parcours.append(parcour)
db.session.add(module)
else:
log(

View File

@ -1185,7 +1185,10 @@ def do_formsemestre_clone(
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions.
New dates, responsable_id
"""
log("cloning %s" % orig_formsemestre_id)
log(f"cloning orig_formsemestre_id")
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
orig_formsemestre_id
)
orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
cnx = ndb.GetDBConnexion()
# 1- create sem
@ -1196,7 +1199,8 @@ def do_formsemestre_clone(
args["date_fin"] = date_fin
args["etat"] = 1 # non verrouillé
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
log("created formsemestre %s" % formsemestre_id)
log(f"created formsemestre {formsemestre_id}")
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
# 2- create moduleimpls
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
for mod_orig in mods_orig:
@ -1258,7 +1262,12 @@ def do_formsemestre_clone(
args["formsemestre_id"] = formsemestre_id
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
# 5- Copy partitions and groups
# 6- Copie les parcours
formsemestre.parcours = formsemestre_orig.parcours
db.session.add(formsemestre)
db.session.commit()
# 7- Copy partitions and groups
if clone_partitions:
sco_groups_copy.clone_partitions_and_groups(
orig_formsemestre_id, formsemestre_id

View File

@ -648,12 +648,12 @@ def formsemestre_description_table(
titles = {title: title for title in columns_ids}
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
titles["ects"] = "ECTS"
titles["jour"] = "Evaluation"
titles["jour"] = "Évaluation"
titles["description"] = ""
titles["coefficient"] = "Coef. éval."
titles["evalcomplete_str"] = "Complète"
titles["parcours"] = "Parcours"
titles["publish_incomplete_str"] = "Toujours Utilisée"
titles["publish_incomplete_str"] = "Toujours utilisée"
title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}"
R = []
@ -732,6 +732,8 @@ def formsemestre_description_table(
evals.reverse() # ordre chronologique
# Ajoute etat:
for e in evals:
e["_jour_order"] = e["jour"].isoformat()
e["jour"] = e["jour"].strftime("%d/%m/%Y") if e["jour"] else ""
e["UE"] = l["UE"]
e["_UE_td_attrs"] = l["_UE_td_attrs"]
e["Code"] = l["Code"]

View File

@ -781,8 +781,8 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
)
# Choix code semestre:
codes = list(sco_codes_parcours.CODES_JURY_SEM)
codes.sort() # fortuitement, cet ordre convient bien !
codes = sorted(sco_codes_parcours.CODES_JURY_SEM)
# fortuitement, cet ordre convient bien !
H.append(
'<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'

View File

@ -664,8 +664,10 @@ def set_group(etudid: int, group_id: int) -> bool:
return True
def change_etud_group_in_partition(etudid, group_id, partition=None):
"""Inscrit etud au groupe de cette partition, et le desinscrit d'autres groupes de cette partition."""
def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None):
"""Inscrit etud au groupe de cette partition,
et le desinscrit d'autres groupes de cette partition.
"""
log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
# 0- La partition
@ -706,7 +708,7 @@ def change_etud_group_in_partition(etudid, group_id, partition=None):
cnx.commit()
# 5- Update parcours
formsemestre = FormSemestre.query.get(formsemestre_id)
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
formsemestre.update_inscriptions_parcours_from_groups()
# 6- invalidate cache
@ -1558,11 +1560,14 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
def do_evaluation_listeetuds_groups(
evaluation_id, groups=None, getallstudents=False, include_demdef=False
):
"""Donne la liste des etudids inscrits a cette evaluation dans les
evaluation_id: int,
groups=None,
getallstudents: bool = False,
include_demdef: bool = False,
) -> list[tuple[int, str]]:
"""Donne la liste non triée des etudids inscrits à cette évaluation dans les
groupes indiqués.
Si getallstudents==True, donne tous les etudiants inscrits a cette
Si getallstudents==True, donne tous les étudiants inscrits à cette
evaluation.
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
(sinon, par défaut, seulement les 'I')

View File

@ -36,6 +36,7 @@ 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.models import FormSemestre
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
@ -175,6 +176,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
(la liste doit avoir été vérifiée au préalable)
En option: inscrit aux mêmes groupes que dans le semestre origine
"""
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
formsemestre.setup_parcours_groups()
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
for etudid in etudids:
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules(
@ -190,7 +193,6 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
# du nom de la partition: évidemment, cela ne marche pas si on a les
# même noms de groupes dans des partitions différentes)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
log("cherche groupes de %(nom)s" % etud)
# recherche le semestre origine (il serait plus propre de l'avoir conservé!)
if len(etud["sems"]) < 2:
@ -201,13 +203,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
prev_formsemestre["formsemestre_id"] if prev_formsemestre else None,
)
cursem_groups_by_name = dict(
[
(g["group_name"], g)
for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
if g["group_name"]
]
)
cursem_groups_by_name = {
g["group_name"]: g
for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
if g["group_name"]
}
# forme la liste des groupes présents dans les deux semestres:
partition_groups = [] # [ partition+group ] (ds nouveau sem.)
@ -217,14 +217,13 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
new_group = cursem_groups_by_name[prev_group_name]
partition_groups.append(new_group)
# inscrit aux groupes
# Inscrit aux groupes
for partition_group in partition_groups:
if partition_group["groups_editable"]:
sco_groups.change_etud_group_in_partition(
etudid,
partition_group["group_id"],
partition_group,
)
sco_groups.change_etud_group_in_partition(
etudid,
partition_group["group_id"],
partition_group,
)
def do_desinscrit(sem, etudids):
@ -481,11 +480,12 @@ def build_page(
def formsemestre_inscr_passage_help(sem):
return (
"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
<p>Cette page permet d'inscrire des étudiants dans le semestre destination
<a class="stdlink"
href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a>,
href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=sem["formsemestre_id"] )
}">{sem['titreannee']}</a>,
et d'en désincrire si besoin.
</p>
<p>Les étudiants sont groupés par semestres d'origines. Ceux qui sont en caractères
@ -495,10 +495,13 @@ def formsemestre_inscr_passage_help(sem):
<p>Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres
étudiants à inscrire dans le semestre destination.</p>
<p>Si vous -selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.</p>
<p>Le bouton <em>inscrire aux mêmes groupes</em> ne prend en compte que les groupes qui existent
dans les deux semestres: pensez à créer les partitions et groupes que vous souhaitez conserver
<b>avant</b> d'inscrire les étudiants.
</p>
<p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !</p>
</div>"""
% sem
)
</div>
"""
def etuds_select_boxes(
@ -574,13 +577,13 @@ def etuds_select_boxes(
if with_checkbox:
H.append(
""" (Select.
<a href="#" onclick="sem_select('%(id)s', true);">tous</a>
<a href="#" onclick="sem_select('%(id)s', false );">aucun</a>""" # "
<a href="#" class="stdlink" onclick="sem_select('%(id)s', true);">tous</a>
<a href="#" class="stdlink" onclick="sem_select('%(id)s', false );">aucun</a>""" # "
% infos
)
if sel_inscrits:
H.append(
"""<a href="#" onclick="sem_select_inscrits('%(id)s');">inscrits</a>"""
"""<a href="#" class="stdlink" onclick="sem_select_inscrits('%(id)s');">inscrits</a>"""
% infos
)
if with_checkbox or sel_inscrits:

View File

@ -41,6 +41,7 @@ from app import log
from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
@ -505,7 +506,11 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
"""
]
table_inscr = _table_but_ue_inscriptions(res)
ue_ids = set.union(*(set(x.keys()) for x in table_inscr.values()))
ue_ids = (
set.union(*(set(x.keys()) for x in table_inscr.values()))
if table_inscr
else set()
)
ues = sorted(
(UniteEns.query.get(ue_id) for ue_id in ue_ids),
key=lambda u: (u.numero or 0, u.acronyme),

View File

@ -30,14 +30,15 @@
Fiche description d'un étudiant et de son parcours
"""
from flask import abort, url_for, g, request
from flask import abort, url_for, g, render_template, request
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.but import jury_but_view
from app.models.etudiants import make_etud_args
from app.but import cursus_but, jury_but_view
from app.models.etudiants import Identite, make_etud_args
from app.models.formsemestre import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_archives_etud
@ -169,11 +170,12 @@ def ficheEtud(etudid=None):
if not etuds:
log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}")
raise ScoValueError("Étudiant inexistant !")
etud = etuds[0]
etudid = etud["etudid"]
sco_etud.fill_etuds_info([etud])
etud_ = etuds[0] # transition: etud_ à éliminer et remplacer par etud
etudid = etud_["etudid"]
etud = Identite.query.get(etudid)
sco_etud.fill_etuds_info([etud_])
#
info = etud
info = etud_
info["ScoURL"] = scu.ScoURL()
info["authuser"] = authuser
info["info_naissance"] = info["date_naissance"]
@ -181,7 +183,7 @@ def ficheEtud(etudid=None):
info["info_naissance"] += " à " + info["lieu_naissance"]
if info["dept_naissance"]:
info["info_naissance"] += f" ({info['dept_naissance']})"
info["etudfoto"] = sco_photos.etud_photo_html(etud)
info["etudfoto"] = sco_photos.etud_photo_html(etud_)
if (
(not info["domicile"])
and (not info["codepostaldomicile"])
@ -206,7 +208,7 @@ def ficheEtud(etudid=None):
info["emaillink"] = ", ".join(
[
'<a class="stdlink" href="mailto:%s">%s</a>' % (m, m)
for m in [etud["email"], etud["emailperso"]]
for m in [etud_["email"], etud_["emailperso"]]
if m
]
)
@ -277,7 +279,7 @@ def ficheEtud(etudid=None):
sem_info[sem["formsemestre_id"]] = grlink
if info["sems"]:
Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"])
info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se,
etudid,
@ -452,7 +454,19 @@ def ficheEtud(etudid=None):
info["bourse_span"] = ""
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche...
info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
# info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
# XXX dev
info["but_cursus_mkup"] = ""
if info["sems"]:
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
if last_sem.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
info["but_cursus_mkup"] = render_template(
"but/cursus_etud.j2",
cursus=but_cursus,
scu=scu,
)
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table>
@ -486,7 +500,7 @@ def ficheEtud(etudid=None):
%(inscriptions_mkup)s
%(but_infos_mkup)s
%(but_cursus_mkup)s
<div class="ficheadmission">
%(adm_data)s
@ -524,7 +538,11 @@ def ficheEtud(etudid=None):
"""
header = html_sco_header.sco_header(
page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css"],
cssstyles=[
"libjs/jQuery-tagEditor/jquery.tag-editor.css",
"css/jury_but.css",
"css/cursus_but.css",
],
javascripts=[
"libjs/jinplace-1.2.1.min.js",
"js/ue_list.js",

View File

@ -200,7 +200,7 @@ def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small") ->
return abort(404, "etudiant inconnu")
etud = etuds[0]
else:
raise ValueError("etud_photo_html: either etud or etudid must be specified")
abort(404, "etud_photo_html: either etud or etudid must be specified")
photo_url = etud_photo_url(etud, size=size)
nom = etud.get("nomprenom", etud["nom_disp"])
if title is None:
@ -244,7 +244,7 @@ def photo_pathname(photo_filename: str, size="orig"):
elif size == "orig":
version = ""
else:
raise ValueError("invalid size parameter for photo")
abort(404, "invalid size parameter for photo")
if not photo_filename:
return False
path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT

View File

@ -1565,7 +1565,7 @@ class BasePreferences(object):
"initvalue": "",
"title": "e-mail copie bulletins",
"size": 40,
"explanation": "adresse recevant une copie des bulletins envoyés aux étudiants",
"explanation": "adresse(s) recevant une copie des bulletins envoyés aux étudiants (si plusieurs, les séparer par des virgules)",
"category": "bul_mail",
},
),

View File

@ -37,6 +37,7 @@ from flask import abort, url_for
from app import log
from app.but import bulletin_but
from app.comp import res_sem
from app.comp.res_common import ResultatsSemestre
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models.etudiants import Identite
@ -407,7 +408,7 @@ def gen_formsemestre_recapcomplet_html(
def _gen_formsemestre_recapcomplet_html(
formsemestre: FormSemestre,
res: NotesTableCompat,
res: ResultatsSemestre,
include_evaluations=False,
mode_jury=False,
filename: str = "",

View File

@ -967,31 +967,35 @@ def has_existing_decision(M, E, etudid):
# Nouveau formulaire saisie notes (2016)
def saisie_notes(evaluation_id, group_ids=[]):
def saisie_notes(evaluation_id: int, group_ids: list = None):
"""Formulaire saisie notes d'une évaluation pour un groupe"""
if not isinstance(evaluation_id, int):
raise ScoInvalidParamError()
group_ids = [int(group_id) for group_id in group_ids]
group_ids = [int(group_id) for group_id in (group_ids or [])]
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("évaluation inexistante")
E = evals[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"]
moduleimpl_status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=E["moduleimpl_id"],
)
# Check access
# (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(current_user, E["moduleimpl_id"]):
return (
html_sco_header.sco_header()
+ "<h2>Modification des notes impossible pour %s</h2>"
% current_user.user_name
+ """<p>(vérifiez que le semestre n'est pas verrouillé et que vous
return f"""
{html_sco_header.sco_header()}
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
<p>(vérifiez que le semestre n'est pas verrouillé et que vous
avez l'autorisation d'effectuer cette opération)</p>
<p><a href="moduleimpl_status?moduleimpl_id=%s">Continuer</a></p>
"""
% E["moduleimpl_id"]
+ html_sco_header.sco_footer()
)
<p><a href="{ moduleimpl_status_url }">Continuer</a>
</p>
{html_sco_header.sco_footer()}
"""
# Informations sur les groupes à afficher:
groups_infos = sco_groups_view.DisplayedGroupsInfos(
@ -1049,8 +1053,14 @@ def saisie_notes(evaluation_id, group_ids=[]):
alone=True,
)
)
H.append("""</td><td style="padding-left: 35px;"><button class="btn_masquer_DEM">Masquer les DEM</button></td></tr></table></div>""")
H.append("""<style>
H.append(
"""
</td>
<td style="padding-left: 35px;"><button class="btn_masquer_DEM">Masquer les DEM</button></td>
</tr>
</table>
</div>
<style>
.btn_masquer_DEM{
font-size: 12px;
}
@ -1061,19 +1071,14 @@ def saisie_notes(evaluation_id, group_ids=[]):
body.masquer_DEM .etud_dem{
display: none !important;
}
</style>""")
# Le formulaire de saisie des notes:
destination = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=E["moduleimpl_id"],
</style>
"""
)
form = _form_saisie_notes(E, M, groups_infos, destination=destination)
# Le formulaire de saisie des notes:
form = _form_saisie_notes(E, M, groups_infos, destination=moduleimpl_status_url)
if form is None:
log(f"redirecting to {destination}")
return flask.redirect(destination)
return flask.redirect(moduleimpl_status_url)
H.append(form)
#
H.append("</div>") # /saisie_notes
@ -1104,6 +1109,9 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
for etudid in etudids:
# infos identite etudiant
e = sco_etud.etudident_list(cnx, {"etudid": etudid})[0]
etud: Identite = Identite.query.get(etudid)
# TODO: refactor et eliminer etudident_list.
e["etud"] = etud # utilisé seulement pour le tri -- a refactorer
sco_etud.format_etud_ident(e)
etuds.append(e)
# infos inscription dans ce semestre
@ -1155,7 +1163,7 @@ def _get_sorted_etuds(eval_dict: dict, etudids: list, formsemestre_id: int):
e["val"] = "DEM"
e["explanation"] = "Démission"
etuds.sort(key=lambda x: (x["nom"], x["prenom"]))
etuds.sort(key=lambda x: x["etud"].sort_key)
return etuds
@ -1301,7 +1309,7 @@ def _form_saisie_notes(E, M, groups_infos, destination=""):
H = []
if nb_decisions > 0:
H.append(
"""<div class="saisie_warn">
f"""<div class="saisie_warn">
<ul class="tf-msg">
<li class="tf-msg">Attention: il y a déjà des <b>décisions de jury</b> enregistrées pour
{nb_decisions} étudiants. Après changement des notes, vérifiez la situation !</li>

View File

@ -1182,7 +1182,10 @@ def gen_row(
tr_id = (
f"""id="row_selected" """ if (row.get("etudid", "") == selected_etudid) else ""
)
return f"""<tr {tr_id} {tr_class}>{"".join([gen_cell(key, row, elt, with_col_class=with_col_classes) for key in keys if not key.startswith('_')])}</tr>"""
return f"""<tr {tr_id} {tr_class}>{
"".join([gen_cell(key, row, elt, with_col_class=with_col_classes)
for key in keys if not key.startswith('_')])
}</tr>"""
# Pour accès depuis les templates jinja

View File

@ -0,0 +1,42 @@
/* Affichage cursus BUT étudiant (sur sa fiche) */
.cursus_but {
margin-left: 32px;
display: inline-grid;
grid-template-columns: repeat(4, auto);
gap: 8px;
}
.cursus_but>* {
display: flex;
align-items: center;
padding-top: 0px;
padding-bottom: 0px;
padding-left: 16px;
padding-right: 0px;
background: #FFF;
border: 1px solid #aaa;
border-radius: 8px;
}
.cursus_but>div.cb_head {
background: rgb(242, 242, 238);
border: none;
border-radius: 0px;
border-bottom: 1px solid gray;
font-weight: bold;
}
div.cb_titre_competence {
background: #09c !important;
color: #FFF;
padding: 8px !important;
}
div.code_rcue {
padding-top: 8px;
padding-bottom: 8px;
position: relative;
}

View File

@ -1,24 +1,27 @@
:host{
:host {
font-family: Verdana;
background: #222;
background: rgb(14, 5, 73);
display: block;
padding: 12px 32px;
color: #FFF;
max-width: 1000px;
margin: auto;
}
h1{
h1 {
font-weight: 100;
}
/**********************/
/* Zone parcours */
/**********************/
.parcours{
.parcours {
display: flex;
gap: 4px;
padding-right: 4px;
}
.parcours>div{
.parcours>div {
background: #09c;
font-size: 18px;
text-align: center;
@ -29,65 +32,89 @@ h1{
transition: 0.1s;
opacity: 0.7;
}
.parcours>div:hover,
.competence>div:hover{
.competence>div:hover {
color: #ccc;
}
.parcours>.focus{
.parcours>.focus {
opacity: 1;
}
/**********************/
/* Zone compétences */
/**********************/
.competences{
display: grid;
.competences {
display: grid;
margin-top: 8px;
row-gap: 4px;
}
.competences>div{
.competences>div {
padding: 4px 8px;
border-radius: 4px;
border-radius: 4px;
cursor: pointer;
width: var(--competence-size);
margin-right: 4px;
}
.comp1{background:#a44}
.comp2{background:#84a}
.comp3{background:#a84}
.comp4{background:#8a4}
.comp5{background:#4a8}
.comp6{background:#48a}
.comp1 {
background: #a44
}
.competences>.focus{
.comp2 {
background: #84a
}
.comp3 {
background: #a84
}
.comp4 {
background: #8a4
}
.comp5 {
background: #4a8
}
.comp6 {
background: #48a
}
.competences>.focus {
outline: 2px solid;
}
/**********************/
/* Zone AC */
/**********************/
h2{
h2 {
display: table;
padding: 8px 16px;
font-size: 20px;
border-radius: 16px 0;
}
.ACs{
.ACs {
padding-right: 4px;
}
.AC li{
.AC li {
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
gap: 4px;
margin-bottom: 4px;
border-bottom: 1px solid;
border-bottom: 1px solid;
}
.AC li>div:nth-child(1){
.AC li>div:nth-child(1) {
padding: 2px 4px;
border-radius: 4px;
}
.AC li>div:nth-child(2){
.AC li>div:nth-child(2) {
padding-bottom: 2px;
}

View File

@ -26,8 +26,10 @@ function change_menu_code(elt) {
let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
"select.ue_rcue_" + elt.dataset.niveau_id);
ue_selects.forEach(select => {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
if (select.value != "ADM") {
select.value = "ADJR";
change_menu_code(select); // pour changer les styles
}
});
}
}

View File

@ -219,11 +219,11 @@ $(function () {
localStorage.setItem(order_info_key, order_info);
}
let etudids = [];
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
document.querySelectorAll("td.identite_court").forEach(e => {
etudids.push(e.dataset.etudid);
});
let noms = [];
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
document.querySelectorAll("td.identite_court").forEach(e => {
noms.push(e.dataset.nomprenom);
});
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);

View File

@ -0,0 +1,30 @@
{# Affichage cursus BUT fiche étudiant #}
<div class="cursus_but">
<div class="cb_head"></div>
<div class="cb_head">BUT 1</div>
<div class="cb_head">BUT 2</div>
<div class="cb_head">BUT 3</div>
{% for competence_id in cursus.to_dict() %}
<div class="cb_titre_competence">{{ cursus.competences[competence_id].titre }}</div>
{% for annee in ('BUT1', 'BUT2', 'BUT3') %}
{% set validation = cursus.validation_par_competence_et_annee.get(competence_id, {}).get(annee) %}
<div>
{% if validation %}
<div class="code_rcue with_scoplement">
<div class="code_jury">{{validation.code}}</div>
<div class="scoplement">
<div>{{validation.ue1.acronyme}} - {{validation.ue2.acronyme}}</div>
<div>Jury de {{validation.formsemestre.titre_annee()}}</div>
<div>enregistré le {{
validation.date.strftime("%d/%m/%Y à %H:%M")
}}</div>
</div>
</div>
{% else %}
-
{%endif%}
</div>
{% endfor %}
{% endfor %}
</div>

View File

@ -8,14 +8,22 @@
{% block app_content %}
<h2>Calcul automatique des décisions de jury annuelle BUT</h2>
<h2>Calcul automatique des décisions de jury du BUT</h2>
<ul>
<li>Seuls les étudiants qui valident l'année seront affectés:
tous les niveaux de compétences (RCUE) validables
(moyenne annuelle au dessus de 10);
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval" sur deux années.
</li>
<li>Ne modifie jamais de décisions déjà enregistrées.
</li>
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
</li>
<li>L'assiduité n'est <b>pas</b> prise en compte.
</li>
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
</ul>
<p>
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
notamment sur les UEs en dessous de 10.
</p>
<p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</p>

View File

@ -49,7 +49,8 @@
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
{% endif %}
parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun', true)|safe
- parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
true)|safe
}}</b>
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %}
({{mod.heures_cours|default("&nbsp;",true)|safe}}/{{mod.heures_td|default("&nbsp;",true)|safe}}/{{mod.heures_tp|default("&nbsp;",true)|safe}},

View File

@ -704,10 +704,15 @@ def formation_list(format=None, formation_id=None, args={}):
@scodoc
@permission_required(Permission.ScoView)
@scodoc7func
def formation_export(formation_id, export_ids=False, format=None):
def formation_export(
formation_id, export_ids=False, format=None, export_codes_apo=True
):
"Export de la formation au format indiqué (xml ou json)"
return sco_formations.formation_export(
formation_id, export_ids=export_ids, format=format
formation_id,
export_ids=export_ids,
format=format,
export_codes_apo=export_codes_apo,
)
@ -2350,7 +2355,7 @@ def formsemestre_validation_but(
etud: Identite = Identite.query.filter_by(
id=etudid, dept_id=g.scodoc_dept_id
).first_or_404()
nb_etuds = formsemestre.etuds.count()
# la route ne donne pas le type d'etudid pour pouvoir construire des URLs
# provisoires avec NEXT et PREV
try:
@ -2360,16 +2365,24 @@ def formsemestre_validation_but(
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
# --- Navigation
prev_lnk = f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
prev_lnk = (
f"""{scu.EMO_PREV_ARROW}&nbsp;<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="PREV"
)}" class="stdlink"">précédent</a>
"""
next_lnk = f"""<a href="{url_for(
if nb_etuds > 1
else ""
)
next_lnk = (
f"""<a href="{url_for(
"notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="NEXT"
)}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
"""
if nb_etuds > 1
else ""
)
navigation_div = f"""
<div class="but_navigation">
<div class="prev">
@ -2548,10 +2561,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST":
if not form.cancel.data:
nb_admis = jury_but_validation_auto.formsemestre_validation_auto_but(
nb_etud_modif = jury_but_validation_auto.formsemestre_validation_auto_but(
formsemestre
)
flash(f"Décisions enregistrées ({nb_admis} admis)")
flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect(
url_for(
"notes.formsemestre_saisie_jury",
@ -2563,7 +2576,7 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
"but/formsemestre_validation_auto_but.html",
form=form,
sco=ScoData(formsemestre=formsemestre),
title=f"Calcul automatique jury BUT",
title="Calcul automatique jury BUT",
)
@ -2641,7 +2654,17 @@ def formsemestre_validation_auto(formsemestre_id):
message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(),
)
formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404()
if formsemestre.formation.is_apc():
return redirect(
url_for(
"notes.formsemestre_validation_auto_but",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
)
return sco_formsemestre_validation.formsemestre_validation_auto(formsemestre_id)
@ -2821,8 +2844,9 @@ def formsemestre_jury_but_erase(
explanation=f"""Les validations d'UE et autorisations de passage
du semestre S{formsemestre.semestre_id} seront effacées."""
if only_one_sem
else """Les validations de toutes les UE, RCUE (compétences) et année seront effacées.
Les décisions de l'année scolaire précédente ne seront pas modifiées.
else """Ses validations de toutes les UE, RCUE (compétences) et année
issues de cette année scolaire seront effacées.
Les décisions des années scolaires précédentes ne seront pas modifiées.
""",
cancel_url=dest_url,
)

View File

@ -216,7 +216,7 @@ def edit_modules_ue_coefs():
</h2>
""",
render_template(
"pn/form_modules_ue_coefs.html",
"pn/form_modules_ue_coefs.j2",
formation=formation,
data_source=url_for(
"notes.table_modules_ue_coefs",

View File

@ -935,7 +935,7 @@ def partition_editor(formsemestre_id: int):
def create_partition_parcours(formsemestre_id):
"""Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS)
avec un groupe par parcours."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
formsemestre.setup_parcours_groups()
return flask.redirect(
url_for(

View File

@ -4,4 +4,5 @@ markers =
but_gb
lemans
lyon
test_test

View File

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

View File

@ -25,7 +25,7 @@ from app import models
from app.auth.models import User, Role, UserRole
from app.entreprises.models import entreprises_reset_database
from app.models import departements
from app.models import Departement, departements
from app.models import Formation, UniteEns, Matiere, Module
from app.models import FormSemestre, FormSemestreInscription
from app.models import GroupDescr
@ -33,6 +33,13 @@ from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models import Partition
from app.models import ScolarFormSemestreValidation
from app.models.but_refcomp import (
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcReferentielCompetences,
)
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.evaluations import Evaluation
from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission
@ -57,9 +64,16 @@ def make_shell_context():
from app.scodoc import sco_utils as scu
return {
"ApcCompetence": ApcCompetence,
"ApcNiveau": ApcNiveau,
"ApcParcours": ApcParcours,
"ApcReferentielCompetences": ApcReferentielCompetences,
"ApcValidationRCUE": ApcValidationRCUE,
"ApcValidationAnnee": ApcValidationAnnee,
"ctx": app.test_request_context(),
"current_app": flask.current_app,
"current_user": current_user,
"Departement": Departement,
"db": db,
"Evaluation": Evaluation,
"flask": flask,
@ -71,21 +85,21 @@ def make_shell_context():
"login_user": login_user,
"logout_user": logout_user,
"mapp": mapp,
"models": models,
"Matiere": Matiere,
"models": models,
"Module": Module,
"ModuleImpl": ModuleImpl,
"ModuleImplInscription": ModuleImplInscription,
"Partition": Partition,
"ndb": ndb,
"notes": notes,
"np": np,
"Partition": Partition,
"pd": pd,
"Permission": Permission,
"pp": pp,
"Role": Role,
"res_sem": res_sem,
"ResultatsSemestreBUT": ResultatsSemestreBUT,
"Role": Role,
"scolar": scolar,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews,

View File

@ -34,7 +34,11 @@ def test_list_users(api_admin_headers):
# Tous les utilisateurs, vus par SuperAdmin:
users = GET("/users/query", headers=admin_h)
assert len(users) > 2
# Les utilisateurs du dept. TAPI
users_TAPI = GET("/users/query?departement=TAPI", headers=admin_h)
nb_TAPI = len(users_TAPI)
assert nb_TAPI > 1
# Les utilisateurs de chaque département (+ ceux sans département)
all_users = []
for acronym in [dept["acronym"] for dept in depts] + [""]:
@ -59,9 +63,8 @@ def test_list_users(api_admin_headers):
for i, u in enumerate(u for u in u_users if u["dept"] != "TAPI"):
headers = get_auth_headers(u["user_name"], "test")
users_by_u = GET("/users/query", headers=headers)
assert len(users_by_u) == 4 + i
# explication: tous ont le droit de voir les 3 users de TAPI
# (test, other et u_TAPI)
assert len(users_by_u) == nb_TAPI + 1 + i
# explication: tous ont le droit de voir les users de TAPI
# plus l'utilisateur de chaque département jusqu'au leur
# (u_AA voit AA, u_BB voit AA et BB, etc)
@ -90,6 +93,10 @@ def test_edit_users(api_admin_headers):
)
assert user["dept"] == "TAPI"
assert user["active"] is False
user = GET(f"/user/{user['id']}", headers=admin_h)
assert user["nom"] == "Toto"
assert user["dept"] == "TAPI"
assert user["active"] is False
def test_roles(api_admin_headers):
@ -229,3 +236,10 @@ def test_modif_users_depts(api_admin_headers):
ok = True
assert ok
# Nettoyage:
# on ne peut pas supprimer l'utilisateur lambda, mais on
# le rend inactif et on le retire de son département
u = POST_JSON(
f"/user/{u_lambda['id']}/edit",
{"active": False, "dept": None},
headers=admin_h,
)

47
tests/api/test_test.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: UTF-8 -*
"""Unit tests for... tests
Ensure test DB is in the expected initial state.
Usage: pytest tests/unit/test_test.py
"""
import pytest
from tests.api.setup_test_api import (
api_headers,
GET,
)
@pytest.mark.test_test
def test_test_db(api_headers):
"""Check that we indeed have: 2 users, 1 dept, 3 formsemestres.
Juste après init, les ensembles seront ceux donnés ci-dessous.
Les autres tests peuvent ajouter des éléments, c'edt pourquoi on utilise issubset().
"""
headers = api_headers
assert {
"admin_api",
"admin",
"lecteur_api",
"other",
"test",
"u_AA",
"u_BB",
"u_CC",
"u_DD",
"u_TAPI",
}.issubset({u["user_name"] for u in GET("/users/query", headers=headers)})
assert {
"AA",
"BB",
"CC",
"DD",
"TAPI",
}.issubset({d["acronym"] for d in GET("/departements", headers=headers)})
assert 1 in (
formsemestre["semestre_id"]
for formsemestre in GET("/formsemestres/query", headers=headers)
)

View File

@ -129,7 +129,7 @@ FormSemestres:
Etudiants:
Aaaaa:
Aïaaa: # avec un i trema
prenom: Étudiant_SEE
civilite: M
formsemestres:
@ -196,7 +196,7 @@ Etudiants:
S3:
parcours: SEE
Bbbbb:
Azbbbb: # Az devrait être trié après Aï.
prenom: Étudiante_BMB
civilite: F
formsemestres:

View File

@ -276,3 +276,749 @@ Etudiants:
code_valide: AJ
moy_ue: 7.00
decision_annee: AJ
geii84:
prenom: etugeii84
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 11.95
"S1.2": 12.76
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 11.95
"UE12":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.76
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 7.83
"S2.2": 8.15
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.83
"UE22":
codes: [ "CMP", "..." ]
code_valide: CMP
moy_ue: 8.15
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.89
est_compensable: False
"UE12":
code_valide: CMP
decision_jury: CMP
rcue:
moy_rcue: 10.455 # ! attention à la précision
est_compensable: True
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 13.71
"S1.2": 9.50
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 13.71
"UE12":
codes: [ "AJ", "ADJ", "RAT", "DEF", "ABAN", "ADJR", "ATJ", "DEM", "UEBSL" ]
code_valide: AJ # c'est l'UE12 du S1 de l'année prec. qui est ADM
moy_ue: 9.5 # moyenne non capitalisée ici
moy_ue_with_cap: 12.76
# Pas de décisions RCUE
# "UE11": -- non applicable
# code_valide: ADM -- non applicable
# decision_jury: ADM -- non applicable
# rcue: -- non applicable
# moy_rcue: 10.94 -- non applicable
# est_compensable: False -- non applicable
# "UE12": -- non applicable
# code_valide: ADM -- non applicable
# decision_jury: ADM -- non applicable
# rcue: -- non applicable
# moy_rcue: 10.94 -- non applicable
# est_compensable: False -- non applicable
decision_annee: AJ
# Nouveaux cas RED (mardi 17/01/2023)
geii8bis:
prenom: "etugeii8 bis"
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 7.0000
"S1.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: AJ
moy_ue: 7.0000
"UE12":
code_valide: AJ # ne sera compensée qu'en fin de S2
moy_ue: 9.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 12.0000
"S2.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "ADM", "..." ]
code_valide: ADM
moy_ue: 12.0000
"UE22":
codes: [ "ADM", "..." ]
code_valide: ADM
moy_ue: 12.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
rcue:
moy_rcue: 9.5000
est_compensable: False
"UE12":
code_valide: CMP
decision_jury: CMP
rcue:
moy_rcue: 10.5000
est_compensable: True
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 9.5000
"S1.2": 7.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
decisions_ues:
"UE11":
codes: [ "CMP", "..." ]
code_valide: CMP
decision_jury: CMP
moy_ue: 9.5000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: CMP
decision_jury: CMP
rcue:
moy_rcue: 10.75
est_compensable: True
decision_annee: ADM
geii10:
prenom: etugeii10
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 9.0000
"S1.2": 7.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: AJ # en fin de S1, sera compensée en fin de S2
moy_ue: 9.0000
"UE12":
code_valide: AJ
moy_ue: 7.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 12.0000
"S2.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
"UE22":
code_valide: ADM
moy_ue: 12.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: CMP
rcue:
moy_rcue: 10.5000
est_compensable: True
"UE12":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.5000
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 12.0000
"S1.2": 7.5000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.5000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: ADM
decision_jury: ADM
rcue:
moy_rcue: 12.00
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.75
est_compensable: False
decision_annee: AJ
geii11:
prenom: etugeii11
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 7.0000
"S1.2": 7.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 12.0000
"S2.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
"UE22":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.5000
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.5000
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 9.0000
"S1.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: True
nb_competences: 2
nb_rcue_annee: 2
decisions_ues:
"UE11":
codes: [ "CMP", "..." ]
code_valide: CMP
decision_jury: CMP
moy_ue: 9.0000
"UE12":
codes: [ "CMP", "..." ]
code_valide: CMP
decision_jury: CMP
moy_ue: 9.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: CMP
decision_jury: CMP
rcue:
moy_rcue: 10.50
est_compensable: True
"UE12":
code_valide: CMP
decision_jury: CMP
rcue:
moy_rcue: 10.50
est_compensable: True
decision_annee: ADM
geii13:
prenom: etugeii13
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 9.0000
"S1.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
"UE12":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 9.0000
"S2.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
code_valide: AJ
moy_ue: 9.0000
"UE22":
code_valide: ADM
moy_ue: 12.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.0000
est_compensable: False
"UE12":
code_valide: ADM
decision_jury: ADM
rcue:
moy_rcue: 12.0000
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 12.0000
"S1.2": ATT
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: ADM
moy_ue: 12.0000
"UE12":
code_valide: AJ
# PAS DE RCUE car UE12 capitalisée mailleure qu'actuelle
decision_annee: AJ
geii20:
prenom: etugeii20
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 7.0000
"S1.2": 7.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: ADJR
moy_ue: 7.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 9.0000
"S2.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: ADJR
moy_ue: 9.0000
"UE22":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 8.0000
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: ADJ
rcue:
moy_rcue: 9.5000
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 12.0000
"S1.2": 4.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: false
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: ADM
moy_ue: 12.0000
"UE12":
code_valide: AJ
moy_ue: 4.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE12":
code_valide: ADJ
rcue:
moy_rcue: 8.00
est_compensable: 0
decision_annee: AJ
geii33:
prenom: etugeii33
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 12.0000
"S1.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 12.0000
"S2.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
"UE22":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: ADM
decision_jury: ADM
rcue:
moy_rcue: 12.0000
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.0000
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 5.0000
"S1.2": 12.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: AJ
moy_ue: 5. # LA MOYENNE COURANTE
moy_ue_with_cap: 12.0000
"UE12":
code_valide: ADM
decision_jury: ADM
moy_ue: 12.0000
# PAS DE RCUE ICI
decision_annee: AJ
geii43:
prenom: etugeii43
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 9.0000
"S1.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: AJ
decision_jury: ADJR
moy_ue: 9.0000
"UE12":
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 9.0000
"S2.2": 9.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
code_valide: AJ
decision_jury: ADJR
moy_ue: 9.0000
"UE22":
code_valide: AJ
decision_jury: AJ
moy_ue: 9.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.0000
est_compensable: False
"UE12":
code_valide: AJ
decision_jury: ADJ
rcue:
moy_rcue: 9.0000
est_compensable: False
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 11.0000
"S1.2": 7.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 11.0000
"UE12":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.0000
decision_annee: AJ
geii84bis:
prenom: "etugeii84 bis"
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 11.9500
"S1.2": 12.7600
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 11.9500
"UE12":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ADM
moy_ue: 12.7600
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 7.8300
"S2.2": 8.1500
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "RED", "..." ]
decisions_ues:
"UE21":
codes: [ "AJ", "..." ]
code_valide: AJ
decision_jury: AJ
moy_ue: 7.8300
"UE22":
codes: [ "CMP", "..." ]
code_valide: CMP
decision_jury: CMP
moy_ue: 8.1500
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ
decision_jury: AJ
rcue:
moy_rcue: 9.8900
est_compensable: False
"UE12":
code_valide: CMP
decision_jury: CMP
rcue:
moy_rcue: 10.4550
est_compensable: True
decision_annee: RED
S1-red:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 13.7100
"S1.2": 9.5000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False
nb_competences: 2
nb_rcue_annee: 0
decisions_ues:
"UE11":
code_valide: ADM
moy_ue: 13.7100
"UE12":
code_valide: AJ
moy_ue: 9.5000
moy_ue_with_cap: 12.7600
decision_annee: AJ

View File

@ -374,7 +374,7 @@ def setup_from_yaml(filename: str) -> dict:
def _check_codes_jury(codes: list[str], codes_att: list[str]):
"""Vérifie (assert) la liste des codes
l'ordre n'a pas d'importance ici.
Si codes_att contient un "...", on se contente de vérifie que
Si codes_att contient un "...", on se contente de vérifier que
les codes de codes_att sont tous présents dans codes.
"""
codes_set = set(codes)
@ -404,13 +404,18 @@ def _check_decisions_ues(
if "codes" in dec_ue_att:
_check_codes_jury(dec_ue.codes, dec_ue_att["codes"])
for attr in ("moy_ue", "moy_ue_with_cap", "explanation", "code_valide"):
for attr in ("explanation", "code_valide"):
if attr in dec_ue_att:
if getattr(dec_ue, attr) != dec_ue_att[attr]:
raise ValueError(
f"""Erreur: décision d'UE: {dec_ue.ue.acronyme
} : champs {attr}={getattr(dec_ue, attr)} != attendu {dec_ue_att[attr]}"""
)
for attr in ("moy_ue", "moy_ue_with_cap"):
if attr in dec_ue_att:
assert (
abs(getattr(dec_ue, attr) - dec_ue_att[attr]) < scu.NOTES_PRECISION
)
# Force décision de jury:
code_manuel = dec_ue_att.get("decision_jury")
if code_manuel is not None: