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}", f"Mode test: mails redirigés vers {email_test_mode_address}",
category="warning", 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 import GroupDescr, Partition
from app.models.groups import group_membership from app.models.groups import group_membership
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu 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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group = query.first_or_404() 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}: 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") return json_error(404, "etud non inscrit au formsemestre du groupe")
groups = (
GroupDescr.query.filter_by(partition_id=group.partition.id) sco_groups.change_etud_group_in_partition(
.join(group_membership) etudid, group_id, group.partition.to_dict()
.filter_by(etudid=etudid)
) )
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}) 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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group = query.first_or_404() group = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if etud in group.etuds: if etud in group.etuds:
group.etuds.remove(etud) group.etuds.remove(etud)
db.session.commit() db.session.commit()
@ -232,6 +226,8 @@ def partition_remove_etud(partition_id: int, etudid: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition = query.first_or_404() partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
groups = ( groups = (
GroupDescr.query.filter_by(partition_id=partition_id) GroupDescr.query.filter_by(partition_id=partition_id)
.join(group_membership) .join(group_membership)
@ -262,8 +258,10 @@ def group_create(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.groups_editable: 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 data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name") group_name = data.get("group_name")
if group_name is None: 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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group: GroupDescr = query.first_or_404() group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.groups_editable: 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 formsemestre_id = group.partition.formsemestre_id
log(f"deleting {group}") log(f"deleting {group}")
db.session.delete(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) query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
) )
group: GroupDescr = query.first_or_404() group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not group.partition.groups_editable: 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 data = request.get_json(force=True) # may raise 400 Bad Request
group_name = data.get("group_name") group_name = data.get("group_name")
if group_name is not None: if group_name is not None:
@ -358,6 +360,8 @@ def partition_create(formsemestre_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_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 data = request.get_json(force=True) # may raise 400 Bad Request
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
if partition_name is None: if partition_name is None:
@ -406,6 +410,8 @@ def formsemestre_order_partitions(formsemestre_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_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 partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, int) and not all( if not isinstance(partition_ids, int) and not all(
isinstance(x, int) for x in partition_ids isinstance(x, int) for x in partition_ids
@ -443,6 +449,8 @@ def partition_order_groups(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() 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 group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, int) and not all( if not isinstance(group_ids, int) and not all(
isinstance(x, int) for x in group_ids isinstance(x, int) for x in group_ids
@ -484,6 +492,8 @@ def partition_edit(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() 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 data = request.get_json(force=True) # may raise 400 Bad Request
modified = False modified = False
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
@ -542,6 +552,8 @@ def partition_delete(partition_id: int):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404() partition: Partition = query.first_or_404()
if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé")
if not partition.partition_name: if not partition.partition_name:
return json_error(404, "ne peut pas supprimer la partition par défaut") return json_error(404, "ne peut pas supprimer la partition par défaut")
is_parcours = partition.is_parcours() is_parcours = partition.is_parcours()

View File

@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
avec la même interface. avec la même interface.
""" """
import collections
from typing import Union from typing import Union
from flask import g, url_for 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 import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu 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 from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res) super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT # Ajustements pour le BUT
@ -65,3 +67,117 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
def parcours_validated(self): def parcours_validated(self):
"True si le parcours est validé" "True si le parcours est validé"
return False # XXX TODO 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 from typing import Union
import numpy as np import numpy as np
from flask import g, url_for from flask import flash, g, url_for
from app import db from app import db
from app import log from app import log
@ -554,7 +554,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"""Liste des regroupements d'UE à considérer cette année. """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). 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. 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: if self.formsemestre_pair is None or self.formsemestre_impair is None:
return [] return []
@ -570,6 +569,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
not in CODES_UE_VALIDES not in CODES_UE_VALIDES
): ):
continue # ignore cette UE antérieure non capitalisée 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: if ue_pair.niveau_competence_id == ue_impair.niveau_competence_id:
rcue = RegroupementCoherentUE( rcue = RegroupementCoherentUE(
self.etud, self.etud,
@ -690,20 +693,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
db.session.commit() 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. """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 no_overwrite, ne fait rien si un code est déjà enregistré.
Si l'étudiant est DEM ou DEF, ne fait rien. Si l'étudiant est DEM ou DEF, ne fait rien.
""" """
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
return return False
if code and not code in self.codes: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
f"code annee <tt>{html.escape(code)}</tt> invalide pour formsemestre {html.escape(self.formsemestre)}" 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): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
db.session.commit() db.session.commit()
@ -743,9 +746,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
next_semestre_id, next_semestre_id,
) )
self.recorded = True
db.session.commit() db.session.commit()
self.recorded = True
self.invalidate_formsemestre_cache() self.invalidate_formsemestre_cache()
return True
def invalidate_formsemestre_cache(self): def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres" "invalide le résultats des deux formsemestres"
@ -756,13 +760,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
if self.formsemestre_pair is not None: if self.formsemestre_pair is not None:
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre_pair.id) 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, """Enregistre les codes qui n'ont pas été spécifiés par le formulaire,
et sont donc en mode "automatique". et sont donc en mode "automatique".
- Si "à cheval", ne modifie pas les codes UE de l'année scolaire précédente. - 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. - 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() annee_scolaire = self.formsemestre.annee_scolaire()
# UEs # UEs
for dec_ue in self.decisions_ues.values(): for dec_ue in self.decisions_ues.values():
@ -771,25 +782,40 @@ class DecisionsProposeesAnnee(DecisionsProposees):
) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire: ) and dec_ue.formsemestre.annee_scolaire() == annee_scolaire:
# rappel: le code par défaut est en tête # rappel: le code par défaut est en tête
code = dec_ue.codes[0] if dec_ue.codes else None 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 if (not only_validantes) or code in sco_codes.CODES_UE_VALIDES_DE_DROIT:
# (no_overwrite=True) sauf en mode test yaml # enregistre le code jury seulement s'il n'y a pas déjà de code
dec_ue.record(code, no_overwrite=no_overwrite) # (no_overwrite=True) sauf en mode test yaml
# RCUE : enregistre seulement si pas déjà validé "mieux" modif |= dec_ue.record(code, no_overwrite=no_overwrite)
# RCUE :
for dec_rcue in self.decisions_rcue_by_niveau.values(): for dec_rcue in self.decisions_rcue_by_niveau.values():
code = dec_rcue.codes[0] if dec_rcue.codes else None code = dec_rcue.codes[0] if dec_rcue.codes else None
if (not dec_rcue.recorded) and ( if (
(not dec_rcue.validation) (not dec_rcue.recorded)
or BUT_CODES_ORDERED.get(dec_rcue.validation.code, 0) and ( # enregistre seulement si pas déjà validé "mieux"
< BUT_CODES_ORDERED.get(code, 0) (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: # Année:
if not self.recorded: if not self.recorded:
# rappel: le code par défaut est en tête # rappel: le code par défaut est en tête
code = self.codes[0] if self.codes else None code = self.codes[0] if self.codes else None
# enregistre le code jury seulement s'il n'y a pas déjà de code # enregistre le code jury seulement s'il n'y a pas déjà de code
# (no_overwrite=True) sauf en mode test yaml # (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): def erase(self, only_one_sem=False):
"""Efface les décisions de jury de cet étudiant """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 return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
} codes={self.codes} explanation={self.explanation}""" } 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. """Enregistre le code RCUE.
Note: Note:
- si le RCUE est ADJ, les UE non validées sont passées à ADJ - 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 XXX on pourra imposer ici d'autres règles de cohérence
""" """
if self.rcue is None: if self.rcue is None:
return # pas de RCUE a enregistrer return False # pas de RCUE a enregistrer
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
return return False
if code and not code in self.codes: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
f"code UE invalide pour ue_id={self.ue.id}: {html.escape(code)}" 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): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
parcours_id = self.parcour.id if self.parcour is not None else None parcours_id = self.parcour.id if self.parcour is not None else None
if self.validation: if self.validation:
db.session.delete(self.validation) db.session.delete(self.validation)
@ -1051,7 +1077,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
dec_ue = deca.decisions_ues.get(ue_id) dec_ue = deca.decisions_ues.get(ue_id)
if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES: if dec_ue and dec_ue.code_valide not in CODES_UE_VALIDES:
log(f"rcue.record: force ADJR sur {dec_ue}") 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: if self.rcue.formsemestre_1 is not None:
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
@ -1063,6 +1095,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
) )
self.code_valide = code # mise à jour état self.code_valide = code # mise à jour état
self.recorded = True self.recorded = True
return True
def erase(self): def erase(self):
"""Efface la décision de jury de cet étudiant pour cet RCUE""" """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.codes = [sco_codes.AJ, sco_codes.ADJ] + self.codes
self.explanation = "notes insuffisantes" 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. """Enregistre le code jury pour cette UE.
Si no_overwrite, n'enregistre pas s'il y a déjà un code. 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: if code and not code in self.codes:
raise ScoValueError( raise ScoValueError(
@ -1204,7 +1238,7 @@ class DecisionsProposeesUE(DecisionsProposees):
) )
if code == self.code_valide or (self.code_valide is not None and no_overwrite): if code == self.code_valide or (self.code_valide is not None and no_overwrite):
self.recorded = True self.recorded = True
return # no change return False # no change
self.erase() self.erase()
if code is None: if code is None:
self.validation = None self.validation = None
@ -1235,6 +1269,7 @@ class DecisionsProposeesUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id) sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
self.code_valide = code # mise à jour self.code_valide = code # mise à jour
self.recorded = True self.recorded = True
return True
def erase(self): def erase(self):
"""Efface la décision de jury de cet étudiant pour cette UE""" """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["_nom_disp_order"] = etud.sort_key
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail") self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court") self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
self["_nom_short_data"] = {
"etudid": etud.id,
"nomprenom": etud.nomprenom,
}
if with_links: if with_links:
self["_nom_short_order"] = etud.sort_key self["_nom_short_order"] = etud.sort_key
self["_nom_short_target"] = url_for( self["_nom_short_target"] = url_for(
@ -368,10 +372,6 @@ class RowCollector:
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""), + ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
"col_rcue col_rcues_validables" + klass, "col_rcue col_rcues_validables" + klass,
) )
self["_rcues_validables_data"] = {
"etudid": deca.etud.id,
"nomprenom": deca.etud.nomprenom,
}
if len(deca.rcues_annee) > 0: if len(deca.rcues_annee) > 0:
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair # 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: 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( def formsemestre_validation_auto_but(
formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True formsemestre: FormSemestre, only_adm: bool = True, no_overwrite: bool = True
) -> int: ) -> int:
"""Calcul automatique des décisions de jury sur une année BUT. """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
- N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval". si on a des RCUE "à cheval".
Normalement, only_adm est True et on n'enregistre que les décisions ADM (de droit). - Ne modifie jamais de décisions déjà enregistrées (sauf si no_overwrite est faux,
Si only_adm est faux, on enregistre la première décision proposée par ScoDoc ce qui est utilisé pour certains tests unitaires).
(mode à n'utiliser que pour les tests) - 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 Returns: nombre d'étudiants pour lesquels on a enregistré au moins un code.
(utiliser faux pour certains tests)
Returns: nombre d'étudiants "admis"
""" """
if not formsemestre.formation.is_apc(): if not formsemestre.formation.is_apc():
raise ScoValueError("fonction réservée aux formations BUT") raise ScoValueError("fonction réservée aux formations BUT")
nb_admis = 0 nb_etud_modif = 0
with sco_cache.DeferredSemCacheManager(): with sco_cache.DeferredSemCacheManager():
for etudid in formsemestre.etuds_inscriptions: for etudid in formsemestre.etuds_inscriptions:
etud: Identite = Identite.query.get(etudid) etud: Identite = Identite.query.get(etudid)
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre) deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
if deca.admis: # année réussie nb_etud_modif += deca.record_all(
nb_admis += 1 no_overwrite=no_overwrite, only_validantes=only_adm
if deca.admis or not only_adm: )
deca.record_all(no_overwrite=no_overwrite)
db.session.commit() 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 <div>UE en cours
{ "sans notes" if np.isnan(dec_ue.moy_ue) { "sans notes" if np.isnan(dec_ue.moy_ue)
else else
("avec moyenne" + scu.fmt_note(dec_ue.moy_ue)) ("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>")
} }
</div> </div>
</div> </div>
@ -205,9 +205,10 @@ def _gen_but_niveau_ue(
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>""" moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
if dec_ue.code_valide: if dec_ue.code_valide:
scoplement = f"""<div class="scoplement"> 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")} à {dec_ue.validation.event_date.strftime("%Hh%M")}
</div> </div>
</div>
""" """
else: else:
scoplement = "" scoplement = ""

View File

@ -39,6 +39,7 @@ from dataclasses import dataclass
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import app
from app import db from app import db
from app.models import Evaluation, EvaluationUEPoids, ModuleImpl from app.models import Evaluation, EvaluationUEPoids, ModuleImpl
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -484,7 +485,8 @@ class ModuleImplResultsClassic(ModuleImplResults):
if nb_etuds == 0: if nb_etuds == 0:
return pd.Series() return pd.Series()
evals_coefs = self.get_evaluations_coefs(modimpl).reshape(-1) 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) evals_notes_20 = self.get_eval_notes_sur_20(modimpl)
# Les coefs des évals pour chaque étudiant: là où il a des notes # Les coefs des évals pour chaque étudiant: là où il a des notes
# non neutralisées # non neutralisées

View File

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

View File

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

View File

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

View File

@ -94,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return "" return ""
return self.version_orebut.split()[0] 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. """Représentation complète du ref. de comp.
comme un dict. comme un dict.
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
""" """
return { return {
"dept_id": self.dept_id, "dept_id": self.dept_id,
@ -111,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
if self.scodoc_date_loaded if self.scodoc_date_loaded
else "", else "",
"scodoc_orig_filename": self.scodoc_orig_filename, "scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences}, "competences": {
"parcours": {x.code: x.to_dict() for x in self.parcours}, 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( def get_niveaux_by_parcours(
@ -174,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
niveaux_by_parcours_no_tc["TC"] = niveaux_tc niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return parcours, niveaux_by_parcours_no_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): class ApcCompetence(db.Model, XMLModel):
"Compétence" "Compétence"
@ -215,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
def __repr__(self): def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre!r}>" 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" "repr dict recursive sur situations, composantes, niveaux"
return { return {
"id_orebut": self.id_orebut, "id_orebut": self.id_orebut,
@ -227,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
"composantes_essentielles": [ "composantes_essentielles": [
x.to_dict() for x in self.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: 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={ return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>""" self.annee!r} {self.competence!r}>"""
def to_dict(self): def to_dict(self, with_app_critiques=True):
"as a dict, recursif sur les AC" "as a dict, recursif (ou non) sur les AC"
return { return {
"libelle": self.libelle, "libelle": self.libelle,
"annee": self.annee, "annee": self.annee,
"ordre": self.ordre, "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): 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} d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
return d 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): class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True) 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")} <em>enregistrée le {self.date.strftime("%d/%m/%Y")}
à {self.date.strftime("%Hh%M")}</em>""" à {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: def niveau(self) -> ApcNiveau:
"""Le niveau de compétence associé à cet RCUE.""" """Le niveau de compétence associé à cet RCUE."""
# Par convention, il est donné par la seconde UE # Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence 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: def to_dict_bul(self) -> dict:
"Export dict pour bulletins: le code et le niveau de compétence" "Export dict pour bulletins: le code et le niveau de compétence"
niveau = self.niveau() niveau = self.niveau()

View File

@ -55,7 +55,8 @@ class Formation(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="formation") modules = db.relationship("Module", lazy="dynamic", backref="formation")
def __repr__(self): 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: def to_html(self) -> str:
"titre complet pour affichage" "titre complet pour affichage"

View File

@ -63,51 +63,51 @@ class FormSemestre(db.Model):
"False si verrouillé" "False si verrouillé"
modalite = db.Column( modalite = db.Column(
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") 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( gestion_compensation = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( bul_hide_xml = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( block_moyennes = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( block_moyenne_generale = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" db.Boolean(), nullable=False, default=False, server_default="false"
) )
"Si vrai, la moyenne générale indicative BUT n'est pas calculée" "Si vrai, la moyenne générale indicative BUT n'est pas calculée"
# semestres decales (pour gestion jurys):
gestion_semestrielle = db.Column( gestion_semestrielle = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( bul_bgcolor = db.Column(
db.String(SHORT_STR_LEN), db.String(SHORT_STR_LEN),
default="white", default="white",
server_default="white", server_default="white",
nullable=False, nullable=False,
) )
# autorise resp. a modifier semestre: "couleur fond bulletins HTML"
resp_can_edit = db.Column( resp_can_edit = db.Column(
db.Boolean(), nullable=False, default=False, server_default="false" 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( resp_can_change_ens = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true" 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( ens_can_edit_eval = db.Column(
db.Boolean(), nullable=False, default=False, server_default="False" 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 ! 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()) elt_annee_apo = db.Column(db.Text())
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
# Relations: # Relations:
etapes = db.relationship( etapes = db.relationship(

View File

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

View File

@ -111,6 +111,7 @@ class UniteEns(db.Model):
e["ects"] = e["ects"] e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0 e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None 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 with_module_ue_coefs:
if convert_objects: if convert_objects:
e["module_ue_coefs"] = [ e["module_ue_coefs"] = [
@ -219,6 +220,8 @@ class UniteEns(db.Model):
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_niveau_competence( {self}, {niveau} )") log(f"ue.set_niveau_competence( {self}, {niveau} )")
def set_parcour(self, parcour: ApcParcours): def set_parcour(self, parcour: ApcParcours):
@ -246,6 +249,8 @@ class UniteEns(db.Model):
self.niveau_competence = None self.niveau_competence = None
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
# Invalidation du cache
self.formation.invalidate_cached_sems()
log(f"ue.set_parcour( {self}, {parcour} )") 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) etud = self.get_cache_etudInfo_d_un_etudiant(etudid)
(_, parcours) = sco_report.get_codeparcoursetud(etud) (_, parcours) = sco_report.get_codeparcoursetud(etud)
if ( if (
len(set(sco_codes_parcours.CODES_SEM_REO.keys()) & set(parcours.values())) len(sco_codes_parcours.CODES_SEM_REO & set(parcours.values())) > 0
> 0
): # Eliminé car NAR apparait dans le parcours ): # Eliminé car NAR apparait dans le parcours
reponse = True reponse = True
if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2: if pe_tools.PE_DEBUG and pe_tools.PE_DEBUG >= 2:
@ -563,9 +562,8 @@ class JuryPE(object):
dec = nt.get_etud_decision_sem( dec = nt.get_etud_decision_sem(
etudid etudid
) # quelle est la décision du jury ? ) # quelle est la décision du jury ?
if dec and dec["code"] in list( if dec and (dec["code"] in sco_codes_parcours.CODES_SEM_VALIDES):
sco_codes_parcours.CODES_SEM_VALIDES.keys() # isinstance( sesMoyennes[i+1], float) and
): # isinstance( sesMoyennes[i+1], float) and
# mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide" # mT = sesMoyennes[i+1] # substitue la moyenne si le semestre suivant est "valide"
leFid = sem["formsemestre_id"] leFid = sem["formsemestre_id"]
else: else:

View File

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

View File

@ -187,16 +187,23 @@ CODES_EXPL = {
# Les codes de semestres: # Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} 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_VALIDES_DE_DROIT = {ADM, ADC}
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente 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: # Pour le BUT:
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL} CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
CODES_RCUE = {ADM, AJ, CMP}
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
@ -226,17 +233,17 @@ BUT_CODES_ORDERED = {
def code_semestre_validant(code: str) -> bool: def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre" "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: def code_semestre_attente(code: str) -> bool:
"Vrai si ce CODE est un code d'attente (semestre validable plus tard par jury ou compensation)" "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: def code_ue_validant(code: str) -> bool:
"Vrai si ce code d'UE est validant (ie attribue les ECTS)" "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 = { 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. 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). 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() cnx = ndb.GetDBConnexion()
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)

View File

@ -99,7 +99,7 @@ def html_edit_formation_apc(
H = [ H = [
render_template( render_template(
"pn/form_ues.html", "pn/form_ues.j2",
formation=formation, formation=formation,
semestre_ids=semestre_ids, semestre_ids=semestre_ids,
editable=editable, editable=editable,
@ -122,7 +122,7 @@ def html_edit_formation_apc(
).first() ).first()
H += [ H += [
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.j2",
formation=formation, formation=formation,
titre=f"Ressources du S{semestre_idx}", titre=f"Ressources du S{semestre_idx}",
create_element_msg="créer une nouvelle ressource", 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 if ues_by_sem[semestre_idx].count() > 0
else "", else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.j2",
formation=formation, formation=formation,
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}", titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
create_element_msg="créer une nouvelle SAÉ", 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 if ues_by_sem[semestre_idx].count() > 0
else "", else "",
render_template( render_template(
"pn/form_mods.html", "pn/form_mods.j2",
formation=formation, formation=formation,
titre=f"Autres modules (non BUT) du S{semestre_idx}", titre=f"Autres modules (non BUT) du S{semestre_idx}",
create_element_msg="créer un nouveau module", create_element_msg="créer un nouveau module",
@ -196,7 +196,7 @@ def html_ue_infos(ue):
and ue.matieres.count() == 0 and ue.matieres.count() == 0
) )
return render_template( return render_template(
"pn/ue_infos.html", "pn/ue_infos.j2",
titre=f"UE {ue.acronyme} {ue.titre}", titre=f"UE {ue.acronyme} {ue.titre}",
ue=ue, ue=ue,
formsemestres=formsemestres, 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", "libjs/jQuery-tagEditor/jquery.caret.min.js",
"js/module_tag_editor.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} f"""<h2>{formation.to_html()} {lockicon}
</h2> </h2>
@ -765,7 +765,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
# Description de la formation # Description de la formation
H.append( H.append(
render_template( render_template(
"pn/form_descr.html", "pn/form_descr.j2",
formation=formation, formation=formation,
parcours=parcours, parcours=parcours,
editable=editable, editable=editable,
@ -913,8 +913,12 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml') formation_id=formation_id, format='xml')
}">Export XML de la formation</a> }">Export XML de la formation</a> ou
(permet de la sauvegarder pour l'échanger avec un autre site) <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>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{

View File

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

View File

@ -1185,7 +1185,10 @@ def do_formsemestre_clone(
"""Clone a semestre: make copy, same modules, same options, same resps, same partitions. """Clone a semestre: make copy, same modules, same options, same resps, same partitions.
New dates, responsable_id 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) orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# 1- create sem # 1- create sem
@ -1196,7 +1199,8 @@ def do_formsemestre_clone(
args["date_fin"] = date_fin args["date_fin"] = date_fin
args["etat"] = 1 # non verrouillé args["etat"] = 1 # non verrouillé
formsemestre_id = sco_formsemestre.do_formsemestre_create(args) 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 # 2- create moduleimpls
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id) mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
for mod_orig in mods_orig: for mod_orig in mods_orig:
@ -1258,7 +1262,12 @@ def do_formsemestre_clone(
args["formsemestre_id"] = formsemestre_id args["formsemestre_id"] = formsemestre_id
_ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args) _ = 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: if clone_partitions:
sco_groups_copy.clone_partitions_and_groups( sco_groups_copy.clone_partitions_and_groups(
orig_formsemestre_id, formsemestre_id orig_formsemestre_id, formsemestre_id

View File

@ -648,12 +648,12 @@ def formsemestre_description_table(
titles = {title: title for title in columns_ids} titles = {title: title for title in columns_ids}
titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues}) titles.update({f"ue_{ue.id}": ue.acronyme for ue in ues})
titles["ects"] = "ECTS" titles["ects"] = "ECTS"
titles["jour"] = "Evaluation" titles["jour"] = "Évaluation"
titles["description"] = "" titles["description"] = ""
titles["coefficient"] = "Coef. éval." titles["coefficient"] = "Coef. éval."
titles["evalcomplete_str"] = "Complète" titles["evalcomplete_str"] = "Complète"
titles["parcours"] = "Parcours" 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()}" title = f"{parcours.SESSION_NAME.capitalize()} {formsemestre.titre_mois()}"
R = [] R = []
@ -732,6 +732,8 @@ def formsemestre_description_table(
evals.reverse() # ordre chronologique evals.reverse() # ordre chronologique
# Ajoute etat: # Ajoute etat:
for e in evals: 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"] = l["UE"]
e["_UE_td_attrs"] = l["_UE_td_attrs"] e["_UE_td_attrs"] = l["_UE_td_attrs"]
e["Code"] = l["Code"] e["Code"] = l["Code"]

View File

@ -781,8 +781,8 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
) )
# Choix code semestre: # Choix code semestre:
codes = list(sco_codes_parcours.CODES_JURY_SEM) codes = sorted(sco_codes_parcours.CODES_JURY_SEM)
codes.sort() # fortuitement, cet ordre convient bien ! # fortuitement, cet ordre convient bien !
H.append( H.append(
'<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>' '<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 return True
def change_etud_group_in_partition(etudid, group_id, partition=None): 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.""" """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)) log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id))
# 0- La partition # 0- La partition
@ -706,7 +708,7 @@ def change_etud_group_in_partition(etudid, group_id, partition=None):
cnx.commit() cnx.commit()
# 5- Update parcours # 5- Update parcours
formsemestre = FormSemestre.query.get(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
formsemestre.update_inscriptions_parcours_from_groups() formsemestre.update_inscriptions_parcours_from_groups()
# 6- invalidate cache # 6- invalidate cache
@ -1558,11 +1560,14 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
def do_evaluation_listeetuds_groups( def do_evaluation_listeetuds_groups(
evaluation_id, groups=None, getallstudents=False, include_demdef=False evaluation_id: int,
): groups=None,
"""Donne la liste des etudids inscrits a cette evaluation dans les 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. groupes indiqués.
Si getallstudents==True, donne tous les etudiants inscrits a cette Si getallstudents==True, donne tous les étudiants inscrits à cette
evaluation. evaluation.
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
(sinon, par défaut, seulement les 'I') (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.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.models import FormSemestre
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours 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) (la liste doit avoir été vérifiée au préalable)
En option: inscrit aux mêmes groupes que dans le semestre origine 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}") log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
for etudid in etudids: for etudid in etudids:
sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( 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 # du nom de la partition: évidemment, cela ne marche pas si on a les
# même noms de groupes dans des partitions différentes) # même noms de groupes dans des partitions différentes)
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] 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é!) # recherche le semestre origine (il serait plus propre de l'avoir conservé!)
if len(etud["sems"]) < 2: 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, prev_formsemestre["formsemestre_id"] if prev_formsemestre else None,
) )
cursem_groups_by_name = dict( cursem_groups_by_name = {
[ g["group_name"]: g
(g["group_name"], g) for g in sco_groups.get_sem_groups(sem["formsemestre_id"])
for g in sco_groups.get_sem_groups(sem["formsemestre_id"]) if g["group_name"]
if g["group_name"] }
]
)
# forme la liste des groupes présents dans les deux semestres: # forme la liste des groupes présents dans les deux semestres:
partition_groups = [] # [ partition+group ] (ds nouveau sem.) 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] new_group = cursem_groups_by_name[prev_group_name]
partition_groups.append(new_group) partition_groups.append(new_group)
# inscrit aux groupes # Inscrit aux groupes
for partition_group in partition_groups: for partition_group in partition_groups:
if partition_group["groups_editable"]: sco_groups.change_etud_group_in_partition(
sco_groups.change_etud_group_in_partition( etudid,
etudid, partition_group["group_id"],
partition_group["group_id"], partition_group,
partition_group, )
)
def do_desinscrit(sem, etudids): def do_desinscrit(sem, etudids):
@ -481,11 +480,12 @@ def build_page(
def formsemestre_inscr_passage_help(sem): def formsemestre_inscr_passage_help(sem):
return ( return f"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
"""<div class="pas_help"><h3><a name="help">Explications</a></h3>
<p>Cette page permet d'inscrire des étudiants dans le semestre destination <p>Cette page permet d'inscrire des étudiants dans le semestre destination
<a class="stdlink" <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. et d'en désincrire si besoin.
</p> </p>
<p>Les étudiants sont groupés par semestres d'origines. Ceux qui sont en caractères <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 <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> étudiants à inscrire dans le semestre destination.</p>
<p>Si vous -selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.</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> <p class="help">Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !</p>
</div>""" </div>
% sem """
)
def etuds_select_boxes( def etuds_select_boxes(
@ -574,13 +577,13 @@ def etuds_select_boxes(
if with_checkbox: if with_checkbox:
H.append( H.append(
""" (Select. """ (Select.
<a href="#" onclick="sem_select('%(id)s', true);">tous</a> <a href="#" class="stdlink" 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', false );">aucun</a>""" # "
% infos % infos
) )
if sel_inscrits: if sel_inscrits:
H.append( 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 % infos
) )
if with_checkbox or sel_inscrits: 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.scolog import logdb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue 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) 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( ues = sorted(
(UniteEns.query.get(ue_id) for ue_id in ue_ids), (UniteEns.query.get(ue_id) for ue_id in ue_ids),
key=lambda u: (u.numero or 0, u.acronyme), key=lambda u: (u.numero or 0, u.acronyme),

View File

@ -30,14 +30,15 @@
Fiche description d'un étudiant et de son parcours 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 from flask_login import current_user
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.but import jury_but_view from app.but import cursus_but, jury_but_view
from app.models.etudiants import make_etud_args 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 html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_archives_etud from app.scodoc import sco_archives_etud
@ -169,11 +170,12 @@ def ficheEtud(etudid=None):
if not etuds: if not etuds:
log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}") log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}")
raise ScoValueError("Étudiant inexistant !") raise ScoValueError("Étudiant inexistant !")
etud = etuds[0] etud_ = etuds[0] # transition: etud_ à éliminer et remplacer par etud
etudid = etud["etudid"] etudid = etud_["etudid"]
sco_etud.fill_etuds_info([etud]) etud = Identite.query.get(etudid)
sco_etud.fill_etuds_info([etud_])
# #
info = etud info = etud_
info["ScoURL"] = scu.ScoURL() info["ScoURL"] = scu.ScoURL()
info["authuser"] = authuser info["authuser"] = authuser
info["info_naissance"] = info["date_naissance"] info["info_naissance"] = info["date_naissance"]
@ -181,7 +183,7 @@ def ficheEtud(etudid=None):
info["info_naissance"] += " à " + info["lieu_naissance"] info["info_naissance"] += " à " + info["lieu_naissance"]
if info["dept_naissance"]: if info["dept_naissance"]:
info["info_naissance"] += f" ({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 ( if (
(not info["domicile"]) (not info["domicile"])
and (not info["codepostaldomicile"]) and (not info["codepostaldomicile"])
@ -206,7 +208,7 @@ def ficheEtud(etudid=None):
info["emaillink"] = ", ".join( info["emaillink"] = ", ".join(
[ [
'<a class="stdlink" href="mailto:%s">%s</a>' % (m, m) '<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 if m
] ]
) )
@ -277,7 +279,7 @@ def ficheEtud(etudid=None):
sem_info[sem["formsemestre_id"]] = grlink sem_info[sem["formsemestre_id"]] = grlink
if info["sems"]: 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( info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se, Se,
etudid, etudid,
@ -452,7 +454,19 @@ def ficheEtud(etudid=None):
info["bourse_span"] = "" info["bourse_span"] = ""
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche... # 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> tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table> <div class="ficheEtud" id="ficheEtud"><table>
@ -486,7 +500,7 @@ def ficheEtud(etudid=None):
%(inscriptions_mkup)s %(inscriptions_mkup)s
%(but_infos_mkup)s %(but_cursus_mkup)s
<div class="ficheadmission"> <div class="ficheadmission">
%(adm_data)s %(adm_data)s
@ -524,7 +538,11 @@ def ficheEtud(etudid=None):
""" """
header = html_sco_header.sco_header( header = html_sco_header.sco_header(
page_title="Fiche étudiant %(prenom)s %(nom)s" % info, 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=[ javascripts=[
"libjs/jinplace-1.2.1.min.js", "libjs/jinplace-1.2.1.min.js",
"js/ue_list.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") return abort(404, "etudiant inconnu")
etud = etuds[0] etud = etuds[0]
else: 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) photo_url = etud_photo_url(etud, size=size)
nom = etud.get("nomprenom", etud["nom_disp"]) nom = etud.get("nomprenom", etud["nom_disp"])
if title is None: if title is None:
@ -244,7 +244,7 @@ def photo_pathname(photo_filename: str, size="orig"):
elif size == "orig": elif size == "orig":
version = "" version = ""
else: else:
raise ValueError("invalid size parameter for photo") abort(404, "invalid size parameter for photo")
if not photo_filename: if not photo_filename:
return False return False
path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT path = os.path.join(PHOTO_DIR, photo_filename) + version + IMAGE_EXT

View File

@ -1565,7 +1565,7 @@ class BasePreferences(object):
"initvalue": "", "initvalue": "",
"title": "e-mail copie bulletins", "title": "e-mail copie bulletins",
"size": 40, "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", "category": "bul_mail",
}, },
), ),

View File

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

View File

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

View File

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

View File

@ -26,8 +26,10 @@ function change_menu_code(elt) {
let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll( let ue_selects = elt.parentElement.parentElement.parentElement.querySelectorAll(
"select.ue_rcue_" + elt.dataset.niveau_id); "select.ue_rcue_" + elt.dataset.niveau_id);
ue_selects.forEach(select => { ue_selects.forEach(select => {
select.value = "ADJR"; if (select.value != "ADM") {
change_menu_code(select); // pour changer les styles 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); localStorage.setItem(order_info_key, order_info);
} }
let etudids = []; let etudids = [];
document.querySelectorAll("td.col_rcues_validables").forEach(e => { document.querySelectorAll("td.identite_court").forEach(e => {
etudids.push(e.dataset.etudid); etudids.push(e.dataset.etudid);
}); });
let noms = []; let noms = [];
document.querySelectorAll("td.col_rcues_validables").forEach(e => { document.querySelectorAll("td.identite_court").forEach(e => {
noms.push(e.dataset.nomprenom); noms.push(e.dataset.nomprenom);
}); });
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]); 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 %} {% 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> <ul>
<li>Seuls les étudiants qui valident l'année seront affectés: <li>N'enregistre jamais de décisions de l'année scolaire précédente, même
tous les niveaux de compétences (RCUE) validables si on a des RCUE "à cheval" sur deux années.
(moyenne annuelle au dessus de 10); </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>
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
</ul> </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"> <p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure ! Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</p> </p>

View File

@ -49,7 +49,8 @@
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>, <span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
{% endif %} {% 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> }}</b>
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %} {% 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}}, ({{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 @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @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)" "Export de la formation au format indiqué (xml ou json)"
return sco_formations.formation_export( 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( etud: Identite = Identite.query.filter_by(
id=etudid, dept_id=g.scodoc_dept_id id=etudid, dept_id=g.scodoc_dept_id
).first_or_404() ).first_or_404()
nb_etuds = formsemestre.etuds.count()
# la route ne donne pas le type d'etudid pour pouvoir construire des URLs # la route ne donne pas le type d'etudid pour pouvoir construire des URLs
# provisoires avec NEXT et PREV # provisoires avec NEXT et PREV
try: try:
@ -2360,16 +2365,24 @@ def formsemestre_validation_but(
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id) read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
# --- Navigation # --- 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, "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="PREV" formsemestre_id=formsemestre_id, etudid="PREV"
)}" class="stdlink"">précédent</a> )}" 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, "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, etudid="NEXT" formsemestre_id=formsemestre_id, etudid="NEXT"
)}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW} )}" class="stdlink"">suivant</a>&nbsp;{scu.EMO_NEXT_ARROW}
""" """
if nb_etuds > 1
else ""
)
navigation_div = f""" navigation_div = f"""
<div class="but_navigation"> <div class="but_navigation">
<div class="prev"> <div class="prev">
@ -2548,10 +2561,10 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
form = jury_but_forms.FormSemestreValidationAutoBUTForm() form = jury_but_forms.FormSemestreValidationAutoBUTForm()
if request.method == "POST": if request.method == "POST":
if not form.cancel.data: 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 formsemestre
) )
flash(f"Décisions enregistrées ({nb_admis} admis)") flash(f"Décisions enregistrées ({nb_etud_modif} étudiants modifiés)")
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_saisie_jury", "notes.formsemestre_saisie_jury",
@ -2563,7 +2576,7 @@ def formsemestre_validation_auto_but(formsemestre_id: int = None):
"but/formsemestre_validation_auto_but.html", "but/formsemestre_validation_auto_but.html",
form=form, form=form,
sco=ScoData(formsemestre=formsemestre), 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, message="<p>Opération non autorisée pour %s</h2>" % current_user,
dest_url=scu.ScoURL(), 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) 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 explanation=f"""Les validations d'UE et autorisations de passage
du semestre S{formsemestre.semestre_id} seront effacées.""" du semestre S{formsemestre.semestre_id} seront effacées."""
if only_one_sem if only_one_sem
else """Les validations de toutes les UE, RCUE (compétences) et année seront effacées. else """Ses validations de toutes les UE, RCUE (compétences) et année
Les décisions de l'année scolaire précédente ne seront pas modifiées. 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, cancel_url=dest_url,
) )

View File

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

View File

@ -935,7 +935,7 @@ def partition_editor(formsemestre_id: int):
def create_partition_parcours(formsemestre_id): def create_partition_parcours(formsemestre_id):
"""Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS) """Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS)
avec un groupe par 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() formsemestre.setup_parcours_groups()
return flask.redirect( return flask.redirect(
url_for( url_for(

View File

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

View File

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

View File

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

View File

@ -34,7 +34,11 @@ def test_list_users(api_admin_headers):
# Tous les utilisateurs, vus par SuperAdmin: # Tous les utilisateurs, vus par SuperAdmin:
users = GET("/users/query", headers=admin_h) 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) # Les utilisateurs de chaque département (+ ceux sans département)
all_users = [] all_users = []
for acronym in [dept["acronym"] for dept in depts] + [""]: 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"): for i, u in enumerate(u for u in u_users if u["dept"] != "TAPI"):
headers = get_auth_headers(u["user_name"], "test") headers = get_auth_headers(u["user_name"], "test")
users_by_u = GET("/users/query", headers=headers) users_by_u = GET("/users/query", headers=headers)
assert len(users_by_u) == 4 + i assert len(users_by_u) == nb_TAPI + 1 + i
# explication: tous ont le droit de voir les 3 users de TAPI # explication: tous ont le droit de voir les users de TAPI
# (test, other et u_TAPI)
# plus l'utilisateur de chaque département jusqu'au leur # plus l'utilisateur de chaque département jusqu'au leur
# (u_AA voit AA, u_BB voit AA et BB, etc) # (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["dept"] == "TAPI"
assert user["active"] is False 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): def test_roles(api_admin_headers):
@ -229,3 +236,10 @@ def test_modif_users_depts(api_admin_headers):
ok = True ok = True
assert ok assert ok
# Nettoyage: # 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: Etudiants:
Aaaaa: Aïaaa: # avec un i trema
prenom: Étudiant_SEE prenom: Étudiant_SEE
civilite: M civilite: M
formsemestres: formsemestres:
@ -196,7 +196,7 @@ Etudiants:
S3: S3:
parcours: SEE parcours: SEE
Bbbbb: Azbbbb: # Az devrait être trié après Aï.
prenom: Étudiante_BMB prenom: Étudiante_BMB
civilite: F civilite: F
formsemestres: formsemestres:

View File

@ -276,3 +276,749 @@ Etudiants:
code_valide: AJ code_valide: AJ
moy_ue: 7.00 moy_ue: 7.00
decision_annee: AJ 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]): def _check_codes_jury(codes: list[str], codes_att: list[str]):
"""Vérifie (assert) la liste des codes """Vérifie (assert) la liste des codes
l'ordre n'a pas d'importance ici. 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. les codes de codes_att sont tous présents dans codes.
""" """
codes_set = set(codes) codes_set = set(codes)
@ -404,13 +404,18 @@ def _check_decisions_ues(
if "codes" in dec_ue_att: if "codes" in dec_ue_att:
_check_codes_jury(dec_ue.codes, dec_ue_att["codes"]) _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 attr in dec_ue_att:
if getattr(dec_ue, attr) != dec_ue_att[attr]: if getattr(dec_ue, attr) != dec_ue_att[attr]:
raise ValueError( raise ValueError(
f"""Erreur: décision d'UE: {dec_ue.ue.acronyme f"""Erreur: décision d'UE: {dec_ue.ue.acronyme
} : champs {attr}={getattr(dec_ue, attr)} != attendu {dec_ue_att[attr]}""" } : 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: # Force décision de jury:
code_manuel = dec_ue_att.get("decision_jury") code_manuel = dec_ue_att.get("decision_jury")
if code_manuel is not None: if code_manuel is not None: