Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
33 changed files with 1000 additions and 348 deletions
Showing only changes of commit e39260ab81 - Show all commits

View File

@ -114,16 +114,16 @@ def _validation_ue_delete(etudid: int, validation_id: int):
# rattachées à un formsemestre)
if not g.scodoc_dept: # accès API
if not current_user.has_permission(Permission.ScoEtudInscrit):
return json_error(403, "validation_delete: non autorise")
return json_error(403, "opération non autorisée (117)")
else:
if validation.formsemestre:
if (
validation.formsemestre.dept_id != g.scodoc_dept_id
) or not validation.formsemestre.can_edit_jury():
return json_error(403, "validation_delete: non autorise")
return json_error(403, "opération non autorisée (123)")
elif not current_user.has_permission(Permission.ScoEtudInscrit):
# Validation non rattachée à un semestre: on doit être chef
return json_error(403, "validation_delete: non autorise")
return json_error(403, "opération non autorisée (126)")
log(f"validation_ue_delete: etuid={etudid} {validation}")
db.session.delete(validation)

View File

@ -43,7 +43,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.models.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes
from app.scodoc.codes_cursus import code_ue_validant, RED, UE_STANDARD
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
@ -102,7 +102,7 @@ class EtudCursusBUT:
"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 = {}
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
"{ annee:int : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
@ -364,10 +364,33 @@ class FormSemestreCursusBUT:
"cache { competence_id : competence }"
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
Ne prend que les UE associées à des niveaux de compétences,
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
"""
validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None)
.join(UniteEns)
.join(ApcNiveau)
.join(ApcCompetence)
.filter_by(referentiel_id=referentiel_competence_id)
)
ects_dict = {}
for v in validations:
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
if v.code in CODES_UE_VALIDES:
ects_dict[key] = v.ue.ects
return sum(ects_dict.values()) if ects_dict else 0.0
def etud_ues_de_but1_non_validees(
etud: Identite, formation: Formation, parcour: ApcParcours
) -> list[UniteEns]:
"""Vrai si cet étudiant a validé toutes ses UEs de S1 et S2, dans son parcours"""
"""Liste des UEs de S1 et S2 non validées, dans son parcours"""
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
@ -377,9 +400,9 @@ def etud_ues_de_but1_non_validees(
.join(Formation)
.filter_by(formation_code=formation.formation_code)
)
codes_validations_by_ue = collections.defaultdict(list)
codes_validations_by_ue_code = collections.defaultdict(list)
for v in validations:
codes_validations_by_ue[v.ue_id].append(v.code)
codes_validations_by_ue_code[v.ue.ue_code].append(v.code)
# Les UEs du parcours en S1 et S2:
ues = formation.query_ues_parcour(parcour).filter(
@ -390,8 +413,11 @@ def etud_ues_de_but1_non_validees(
[
ue
for ue in ues
if any(
(not code_ue_validant(code) for code in codes_validations_by_ue[ue.id])
if not any(
(
code_ue_validant(code)
for code in codes_validations_by_ue_code[ue.ue_code]
)
)
],
key=attrgetter("numero", "acronyme"),

View File

@ -284,15 +284,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# ---- Décision année et autorisation
self.autorisations_recorded = False
"vrai si on a enregistré l'autorisation de passage"
self.validation = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=self.annee_but,
)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
.first()
)
self.validation = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=self.annee_but,
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
).first()
"Validation actuellement enregistrée pour cette année BUT"
self.code_valide = self.validation.code if self.validation is not None else None
"Le code jury annuel enregistré, ou None"
@ -346,21 +342,11 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Cas particulier du passage en BUT 3: nécessité d'avoir validé toutes les UEs du BUT 1.
if self.passage_de_droit and self.annee_but == 2:
inscription = formsemestre.etuds_inscriptions.get(etud.id)
if inscription:
ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
etud, self.formsemestre.formation, self.parcour
)
self.passage_de_droit = not ues_but1_non_validees
explanation += (
f"""UEs de BUT1 non validées: <b>{
', '.join(ue.acronyme for ue in ues_but1_non_validees)
}</b>. """
if ues_but1_non_validees
else ""
)
else:
if not inscription or inscription.etat != scu.INSCRIT:
# pas inscrit dans le semestre courant ???
self.passage_de_droit = False
else:
self.passage_de_droit, explanation = self.passage_de_droit_en_but3()
# Enfin calcule les codes des UEs:
for dec_ue in self.decisions_ues.values():
@ -427,6 +413,53 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
cursus: EtudCursusBUT = EtudCursusBUT(self.etud, self.formsemestre.formation)
niveaux_but1 = cursus.niveaux_by_annee[1]
niveaux_but1_non_valides = []
for niveau in niveaux_but1:
ok = False
validation_par_annee = cursus.validation_par_competence_et_annee.get(
niveau.competence_id
)
if validation_par_annee:
validation_niveau = validation_par_annee.get("BUT1")
if validation_niveau and validation_niveau.code in CODES_RCUE_VALIDES:
ok = True
if not ok:
niveaux_but1_non_valides.append(niveau)
# Les niveaux de BUT1 manquants passent-ils en ADSUP ?
# en vertu de l'article 4.3,
# "La validation des deux UE du niveau dune compétence emporte la validation de
# lensemble des UE du niveau inférieur de cette même compétence."
explanation = ""
ok = True
for niveau_but1 in niveaux_but1_non_valides:
niveau_but2 = niveau_but1.competence.niveaux.filter_by(annee="BUT2").first()
if niveau_but2:
rcue = self.rcue_by_niveau.get(niveau_but2.id)
if (rcue is None) or (
not rcue.est_validable() and not rcue.code_valide()
):
# le RCUE de BUT2 n'est ni validable (avec les notes en cours) ni déjà validé
ok = False
explanation += (
f"Compétence {niveau_but1} de BUT 1 non validée.<br> "
)
else:
explanation += (
f"Compétence {niveau_but1} de BUT 1 validée par ce BUT2.<br> "
)
else:
ok = False
explanation += f"""Compétence {
niveau_but1} de BUT 1 non validée et non existante en BUT2.<br> """
return ok, explanation
# WIP TODO XXX def get_moyenne_annuelle(self)
def infos(self) -> str:
@ -689,7 +722,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.validation = ApcValidationAnnee(
etudid=self.etud.id,
formsemestre=self.formsemestre_impair,
formation_id=self.formsemestre.formation_id,
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
ordre=self.annee_but,
annee_scolaire=self.annee_scolaire(),
code=code,
@ -852,13 +885,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
# Efface les validations concernant l'année BUT
# de ce semestre
validations = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=self.annee_but,
)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
validations = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=self.annee_but,
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
)
for validation in validations:
db.session.delete(validation)
@ -935,9 +965,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue = self.decisions_ues.get(ue.id)
if dec_ue:
if dec_ue.code_valide not in CODES_UE_VALIDES:
messages.append(
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
)
if (
dec_ue.ue_status
and dec_ue.ue_status["was_capitalized"]
):
messages.append(
f"Information: l'UE {ue.acronyme} capitalisée est utilisée pour un RCUE cette année"
)
else:
messages.append(
f"L'UE {ue.acronyme} n'est pas validée mais son RCUE l'est !"
)
else:
messages.append(
f"L'UE {ue.acronyme} n'a pas décision (???)"
@ -1207,6 +1245,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
ue1_id=ue1.id,
ue2_id=ue2.id,
code=sco_codes.ADSUP,
formsemestre_id=self.deca.formsemestre.id, # origine
)
db.session.add(validation_rcue)
db.session.commit()
@ -1233,13 +1272,16 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self, semestre_id: int, ordre_inferieur: int, competence: ApcCompetence
):
"""Au besoin, enregistre une validation d'UE ADSUP pour le niveau de compétence
semestre_id : l'indice du semestre concerné (le pair ou l'impair)
semestre_id : l'indice du semestre concerné (le pair ou l'impair du niveau courant)
"""
# Les validations d'UE impaires existantes pour ce niveau inférieur ?
semestre_id_inferieur = semestre_id - 2
if semestre_id_inferieur < 1:
return
# Les validations d'UE existantes pour ce niveau inférieur ?
validations_ues: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=self.etud.id)
.join(UniteEns)
.filter_by(semestre_idx=semestre_id)
.filter_by(semestre_idx=semestre_id_inferieur)
.join(ApcNiveau)
.filter_by(ordre=ordre_inferieur)
.join(ApcCompetence)
@ -1254,13 +1296,14 @@ class DecisionsProposeesRCUE(DecisionsProposees):
# Il faut créer une validation d'UE
# cherche l'UE de notre formation associée à ce niveau
# et warning si il n'y en a pas
ue = self._get_ue_inferieure(semestre_id, ordre_inferieur, competence)
ue = self._get_ue_inferieure(
semestre_id_inferieur, ordre_inferieur, competence
)
if not ue:
# programme incomplet ou mal paramétré
flash(
f"""Impossible de valider l'UE inférieure du niveau {
ordre_inferieur
} de la compétence {competence.titre}
f"""Impossible de valider l'UE inférieure de la compétence {
competence.titre} (niveau {ordre_inferieur})
car elle n'existe pas dans la formation
""",
"warning",
@ -1287,15 +1330,11 @@ class DecisionsProposeesRCUE(DecisionsProposees):
if annee_inferieure < 1:
return
# Garde-fou: Année déjà validée ?
validations_annee: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=annee_inferieure,
)
.join(Formation)
.filter_by(formation_code=self.deca.formsemestre.formation.formation_code)
.all()
)
validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
etudid=self.etud.id,
ordre=annee_inferieure,
referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id,
).all()
if len(validations_annee) > 1:
log(
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
@ -1332,8 +1371,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
validation_annee = ApcValidationAnnee(
etudid=self.etud.id,
ordre=annee_inferieure,
referentiel_competence_id=self.deca.formsemestre.formation.referentiel_competence_id,
code=sco_codes.ADSUP,
formation_id=self.deca.formsemestre.formation_id,
# met cette validation sur l'année scolaire actuelle, pas la précédente
annee_scolaire=self.deca.formsemestre.annee_scolaire(),
)
@ -1575,16 +1614,11 @@ class DecisionsProposeesUE(DecisionsProposees):
# def est_annee_validee(self, ordre: int) -> bool:
# """Vrai si l'année BUT ordre est validée"""
# # On cherche les validations d'annee avec le même
# # code formation que nous.
# return (
# ApcValidationAnnee.query.filter_by(
# etudid=self.etud.id,
# ordre=ordre,
# )
# .join(Formation)
# .filter(
# Formation.formation_code == self.formsemestre.formation.formation_code
# referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id
# )
# .count()
# > 0

View File

@ -12,6 +12,7 @@ from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
from app import log
from app.but import jury_but
from app.but.cursus_but import but_ects_valides
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
from app.scodoc.gen_tables import GenTable
@ -109,6 +110,11 @@ def pvjury_table_but(
"""
# remplace pour le BUT la fonction sco_pv_forms.pvjury_table
annee_but = (formsemestre.semestre_id + 1) // 2
referentiel_competence_id = formsemestre.formation.referentiel_competence_id
if referentiel_competence_id is None:
raise ScoValueError(
"pas de référentiel de compétences associé à la formation de ce semestre !"
)
titles = {
"nom": "Code" if anonymous else "Nom",
"cursus": "Cursus",
@ -153,7 +159,7 @@ def pvjury_table_but(
etudid=etud.id,
),
"cursus": _descr_cursus_but(etud),
"ects": f"{deca.ects_annee():g}",
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {but_ects_valides(etud, referentiel_competence_id ):g}""",
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
if deca

View File

@ -194,7 +194,7 @@ class BonusSportAdditif(BonusSport):
# les points au dessus du seuil sont comptés (defaut: seuil_moy_gen):
seuil_comptage = None
proportion_point = 0.05 # multiplie les points au dessus du seuil
bonux_max = 20.0 # le bonus ne peut dépasser 20 points
bonus_max = 20.0 # le bonus ne peut dépasser 20 points
bonus_min = 0.0 # et ne peut pas être négatif
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
@ -435,8 +435,11 @@ class BonusAmiens(BonusSportAdditif):
class BonusBesanconVesoul(BonusSportAdditif):
"""Bonus IUT Besançon - Vesoul pour les UE libres
<p>Toute note non nulle, peu importe sa valeur, entraine un bonus de 0,2 point
sur toutes les moyennes d'UE.
<p>Le bonus est compris entre 0 et 0,2 points.
et est reporté sur les moyennes d'UE.
</p>
<p>La valeur saisie doit être entre 0 et 0,2: toute valeur
supérieure à 0,2 entraine un bonus de 0,2.
</p>
"""
@ -444,7 +447,7 @@ class BonusBesanconVesoul(BonusSportAdditif):
displayed_name = "IUT de Besançon - Vesoul"
classic_use_bonus_ues = True # s'applique aux UEs en DUT et LP
seuil_moy_gen = 0.0 # tous les points sont comptés
proportion_point = 1e10 # infini
proportion_point = 1
bonus_max = 0.2
@ -1057,6 +1060,36 @@ class BonusLyon(BonusSportAdditif):
)
class BonusLyon3(BonusSportAdditif):
"""IUT de Lyon 3 (septembre 2022)
<p>Nous avons deux types de bonifications : sport et/ou culture
</p>
<p>
Pour chaque point au-dessus de 10 obtenu en sport ou en culture nous
ajoutons 0,03 points à toutes les moyennes dUE du semestre. Exemple : 16 en
sport ajoute 6*0,03 = 0,18 points à toutes les moyennes dUE du semestre.
</p>
<p>
Les bonifications sport et culture peuvent se cumuler dans la limite de 0,3
points ajoutés aux moyennes des UE. Exemple : 17 en sport et 16 en culture
conduisent au calcul (7 + 6)*0,03 = 0,39 qui dépasse 0,3. La bonification
dans ce cas ne sera que de 0,3 points ajoutés à toutes les moyennes dUE du
semestre.
</p>
<p>
Dans Scodoc on déclarera une UE Sport&Culture dans laquelle on aura un
module pour le Sport et un autre pour la Culture avec pour chaque module la
note sur 20 obtenue en sport ou en culture par létudiant.
</p>
"""
name = "bonus_lyon3"
displayed_name = "IUT de Lyon 3"
proportion_point = 0.03
bonus_max = 0.3
class BonusMantes(BonusSportAdditif):
"""Calcul bonus modules optionnels (investissement, ...), IUT de Mantes en Yvelines.

View File

@ -231,12 +231,11 @@ def erase_decisions_annee_formation(
.all()
)
# Année BUT
validations += (
ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee)
.join(Formation)
.filter_by(formation_code=formation.formation_code)
.all()
)
validations += ApcValidationAnnee.query.filter_by(
etudid=etud.id,
ordre=annee,
referentiel_competence_id=formation.referentiel_competence_id,
).all()
# Autorisations vers les semestres suivants ceux de l'année:
validations += (
ScolarAutorisationInscription.query.filter_by(

View File

@ -337,17 +337,15 @@ class ResultatsSemestreBUT(NotesTableCompat):
if self.validations_annee:
return self.validations_annee
annee_but = (self.formsemestre.semestre_id + 1) // 2
validations = (
ApcValidationAnnee.query.filter_by(ordre=annee_but)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
.join(
FormSemestreInscription,
db.and_(
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
),
)
validations = ApcValidationAnnee.query.filter_by(
ordre=annee_but,
referentiel_competence_id=self.formsemestre.formation.referentiel_competence_id,
).join(
FormSemestreInscription,
db.and_(
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
),
)
validation_by_etud = {}
for validation in validations:

View File

@ -94,6 +94,11 @@ class ApcReferentielCompetences(db.Model, XMLModel):
backref="referentiel_competence",
order_by="Formation.acronyme, Formation.version",
)
validations_annee = db.relationship(
"ApcValidationAnnee",
backref="referentiel_competence",
lazy="dynamic",
)
def __repr__(self):
return f"<ApcReferentielCompetences {self.id} {self.specialite!r} {self.departement!r}>"
@ -359,6 +364,9 @@ class ApcNiveau(db.Model, XMLModel):
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
def __str__(self):
return f"""{self.competence.titre} niveau {self.ordre}"""
def to_dict(self, with_app_critiques=True):
"as a dict, recursif (ou non) sur les AC"
return {

View File

@ -2,8 +2,6 @@
"""Décisions de jury (validations) des RCUE et années du BUT
"""
from typing import Union
from app import db
from app.models import CODE_STR_LEN
@ -38,7 +36,7 @@ class ApcValidationRCUE(db.Model):
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
"formsemestre pair du RCUE"
"formsemestre origine du RCUE (celui d'où a été émis la validation)"
# Les deux UE associées à ce niveau:
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
@ -106,73 +104,14 @@ class ApcValidationRCUE(db.Model):
}
# unused
# def find_rcues(
# formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
# ) -> list[RegroupementCoherentUE]:
# """Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
# ce semestre pour cette UE.
# Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
# En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
# Résultat: la liste peut être vide.
# """
# if (ue.niveau_competence is None) or (ue.semestre_idx is None):
# return []
# if ue.semestre_idx % 2: # S1, S3, S5
# other_semestre_idx = ue.semestre_idx + 1
# else:
# other_semestre_idx = ue.semestre_idx - 1
# cursor = db.session.execute(
# text(
# """SELECT
# ue.id, formsemestre.id
# FROM
# notes_ue ue,
# notes_formsemestre_inscription inscr,
# notes_formsemestre formsemestre
# WHERE
# inscr.etudid = :etudid
# AND inscr.formsemestre_id = formsemestre.id
# AND formsemestre.semestre_id = :other_semestre_idx
# AND ue.formation_id = formsemestre.formation_id
# AND ue.niveau_competence_id = :ue_niveau_competence_id
# AND ue.semestre_idx = :other_semestre_idx
# """
# ),
# {
# "etudid": etud.id,
# "other_semestre_idx": other_semestre_idx,
# "ue_niveau_competence_id": ue.niveau_competence_id,
# },
# )
# rcues = []
# for ue_id, formsemestre_id in cursor:
# other_ue = UniteEns.query.get(ue_id)
# other_formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# rcues.append(
# RegroupementCoherentUE(
# etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
# )
# )
# # safety check: 1 seul niveau de comp. concerné:
# assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
# return rcues
class ApcValidationAnnee(db.Model):
"""Validation des années du BUT"""
__tablename__ = "apc_validation_annee"
# Assure unicité de la décision:
__table_args__ = (
db.UniqueConstraint("etudid", "ordre", "formation_id"),
) # il aurait été plus intelligent de mettre ici le refcomp
db.UniqueConstraint("etudid", "ordre", "referentiel_competence_id"),
)
id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(
db.Integer,
@ -185,11 +124,9 @@ class ApcValidationAnnee(db.Model):
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
"le semestre IMPAIR (le 1er) de l'année"
formation_id = db.Column( # il aurait été plus intelligent de mettre ici le refcomp
db.Integer,
db.ForeignKey("notes_formations.id"),
nullable=False,
"le semestre origine, normalement l'IMPAIR (le 1er) de l'année"
referentiel_competence_id = db.Column(
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
)
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
@ -209,17 +146,30 @@ class ApcValidationAnnee(db.Model):
"dict pour bulletins"
return {
"annee_scolaire": self.annee_scolaire,
"date": self.date.isoformat(),
"date": self.date.isoformat() if self.date else "",
"code": self.code,
"ordre": self.ordre,
}
def html(self) -> str:
"Affichage html"
date_str = (
f"""le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}"""
if self.date
else "(sans date)"
)
link = (
self.formsemestre.html_link_status(
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
title=self.formsemestre.titre_annee(),
)
if self.formsemestre
else "externe/antérieure"
)
return f"""Validation <b>année BUT{self.ordre}</b> émise par
{self.formsemestre.html_link_status() if self.formsemestre else "-"}
{link}
: <b>{self.code}</b>
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
{date_str}
"""
@ -261,15 +211,11 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
decisions["descr_decisions_rcue"] = ""
decisions["descr_decisions_niveaux"] = ""
# --- Année: prend la validation pour l'année scolaire de ce semestre
validation = (
ApcValidationAnnee.query.filter_by(
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
)
.join(Formation)
.filter(Formation.formation_code == formsemestre.formation.formation_code)
.first()
)
validation = ApcValidationAnnee.query.filter_by(
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
).first()
if validation:
decisions["decision_annee"] = validation.to_dict_bul()
else:

View File

@ -165,12 +165,12 @@ class FormSemestre(db.Model):
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
def html_link_status(self) -> str:
def html_link_status(self, label=None, title=None) -> str:
"html link to status page"
return f"""<a class="stdlink" href="{
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
formsemestre_id=self.id,)
}">{self.titre_mois()}</a>
}" title="{title or ''}">{label or self.titre_mois()}</a>
"""
@classmethod
@ -528,6 +528,11 @@ class FormSemestre(db.Model):
return ""
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
def add_etape(self, etape_apo: str):
"Ajoute une étape"
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
db.session.add(etape)
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
formsemestre.
@ -873,15 +878,12 @@ class FormSemestre(db.Model):
.order_by(UniteEns.numero)
.all()
)
vals_annee = ( # issues de ce formsemestre seulement
vals_annee = ( # issues de cette année scolaire seulement
ApcValidationAnnee.query.filter_by(
etudid=etudid,
annee_scolaire=self.annee_scolaire(),
)
.join(ApcValidationAnnee.formsemestre)
.join(FormSemestre.formation)
.filter(Formation.formation_code == self.formation.formation_code)
.all()
referentiel_competence_id=self.formation.referentiel_competence_id,
).all()
)
H = []
for vals in (vals_sem, vals_ues, vals_rcues, vals_annee):

View File

@ -56,6 +56,7 @@ class ScolarFormSemestreValidation(db.Model):
)
ue = db.relationship("UniteEns", lazy="select", uselist=False)
etud = db.relationship("Identite", backref="validations")
formsemestre = db.relationship(
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
)
@ -94,6 +95,14 @@ class ScolarFormSemestreValidation(db.Model):
if self.moy_ue is not None
else ""
)
link = (
self.formsemestre.html_link_status(
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
title=self.formsemestre.titre_annee(),
)
if self.formsemestre
else "externe/antérieure"
)
return f"""Validation
{'<span class="redboldtext">externe</span>' if self.is_external else ""}
de l'UE <b>{self.ue.acronyme}</b>
@ -101,9 +110,7 @@ class ScolarFormSemestreValidation(db.Model):
+ ", ".join([p.code for p in self.ue.parcours]))
+ "</span>"
if self.ue.parcours else ""}
de {self.ue.formation.acronyme}
{("émise par " + self.formsemestre.html_link_status())
if self.formsemestre else "externe/antérieure"}
{("émise par " + link)}
: <b>{self.code}</b>{moyenne}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
"""
@ -149,10 +156,16 @@ class ScolarAutorisationInscription(db.Model):
def html(self) -> str:
"Affichage html"
link = (
self.origin_formsemestre.html_link_status(
label=f"{self.origin_formsemestre.titre_formation(with_sem_idx=1)}",
title=self.origin_formsemestre.titre_annee(),
)
if self.origin_formsemestre
else "externe/antérieure"
)
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
{self.origin_formsemestre.html_link_status()
if self.origin_formsemestre
else "-"}
{link}
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
"""

View File

@ -196,6 +196,8 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
CODES_SEM_REO = {NAR} # reorientation
# Les codes d'UEs
CODES_JURY_UE = {ADM, CMP, ADJ, ADJR, ADSUP, AJ, ATJ, RAT, DEF, ABAN, DEM, UEBSL}
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
"UE validée"

View File

@ -88,7 +88,7 @@ class DEFAULT_TABLE_PREFERENCES(object):
return self.values[k]
class GenTable(object):
class GenTable:
"""Simple 2D tables with export to HTML, PDF, Excel, CSV.
Can be sub-classed to generate fancy formats.
"""
@ -197,6 +197,9 @@ class GenTable(object):
def __repr__(self):
return f"<gen_table( nrows={self.get_nb_rows()}, ncols={self.get_nb_cols()} )>"
def __len__(self):
return len(self.rows)
def get_nb_cols(self):
return len(self.columns_ids)

View File

@ -51,7 +51,14 @@ from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite, ApcValidationAnnee
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
FormSemestre,
Identite,
ScolarFormSemestreValidation,
)
from app.models.config import ScoDocSiteConfig
from app.scodoc.sco_apogee_reader import (
APO_DECIMAL_SEP,
@ -64,6 +71,7 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.codes_cursus import code_semestre_validant
from app.scodoc.codes_cursus import (
ADSUP,
DEF,
DEM,
NAR,
@ -216,7 +224,12 @@ class ApoEtud(dict):
break
self.col_elts[code] = elt
if elt is None:
self.new_cols[col_id] = self.cols[col_id]
try:
self.new_cols[col_id] = self.cols[col_id]
except KeyError as exc:
raise ScoFormatError(
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
) from exc
else:
try:
self.new_cols[col_id] = sco_elts[code][
@ -323,14 +336,22 @@ class ApoEtud(dict):
x.strip() for x in ue["code_apogee"].split(",")
}:
if self.export_res_ues:
if decisions_ue and ue["ue_id"] in decisions_ue:
if (
decisions_ue and ue["ue_id"] in decisions_ue
) or self.export_res_sdj:
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
if decisions_ue and ue["ue_id"] in decisions_ue:
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
code_decision_ue
)
else:
code_decision_ue_apo = ""
return dict(
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
B=20,
J="",
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
R=code_decision_ue_apo,
M="",
)
else:
@ -343,14 +364,17 @@ class ApoEtud(dict):
module_code_found = False
for modimpl in modimpls:
module = modimpl["module"]
if module["code_apogee"] and code in {
x.strip() for x in module["code_apogee"].split(",")
}:
if (
res.modimpl_inscr_df[modimpl["moduleimpl_id"]][etudid]
and module["code_apogee"]
and code in {x.strip() for x in module["code_apogee"].split(",")}
):
n = res.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
if n != "NI" and self.export_res_modules:
return dict(N=self.fmt_note(n), B=20, J="", R="")
else:
module_code_found = True
if module_code_found:
return VOID_APO_RES
#
@ -491,15 +515,11 @@ class ApoEtud(dict):
# ne trouve pas de semestre impair
self.validation_annee_but = None
return
self.validation_annee_but: ApcValidationAnnee = (
ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
formation_id=self.cur_sem[
"formation_id"
], # XXX utiliser formation_code
).first()
)
self.validation_annee_but: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
formsemestre_id=formsemestre.id,
etudid=self.etud["etudid"],
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
).first()
self.is_nar = (
self.validation_annee_but and self.validation_annee_but.code == NAR
)
@ -899,6 +919,75 @@ class ApoData:
)
return T
def build_adsup_table(self):
"""Construit une table listant les ADSUP émis depuis les formsemestres
NIP nom prenom nom_formsemestre etape UE
"""
validations_ues, validations_rcue = self.list_adsup()
rows = [
{
"code_nip": v.etud.code_nip,
"nom": v.etud.nom,
"prenom": v.etud.prenom,
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
"etape": v.formsemestre.etapes_apo_str(),
"ue": v.ue.acronyme,
}
for v in validations_ues
]
rows += [
{
"code_nip": v.etud.code_nip,
"nom": v.etud.nom,
"prenom": v.etud.prenom,
"formsemestre": v.formsemestre.titre_formation(with_sem_idx=1),
"etape": "", # on ne sait pas à quel étape rattacher le RCUE
"rcue": f"{v.ue1.acronyme}/{v.ue2.acronyme}",
}
for v in validations_rcue
]
return GenTable(
columns_ids=(
"code_nip",
"nom",
"prenom",
"formsemestre",
"etape",
"ue",
"rcue",
),
titles={
"code_nip": "NIP",
"nom": "Nom",
"prenom": "Prénom",
"formsemestre": "Semestre",
"etape": "Etape",
"ue": "UE",
"rcue": "RCUE",
},
rows=rows,
xls_sheet_name="ADSUPs",
)
def list_adsup(
self,
) -> tuple[list[ScolarFormSemestreValidation], list[ApcValidationRCUE]]:
"""Liste les validations ADSUP émises par des formsemestres de cet ensemble"""
validations_ues = (
ScolarFormSemestreValidation.query.filter_by(code=ADSUP)
.filter(ScolarFormSemestreValidation.ue_id != None)
.filter(
ScolarFormSemestreValidation.formsemestre_id.in_(
self.etape_formsemestre_ids
)
)
)
validations_rcue = ApcValidationRCUE.query.filter_by(code=ADSUP).filter(
ApcValidationRCUE.formsemestre_id.in_(self.etape_formsemestre_ids)
)
return validations_ues, validations_rcue
def comp_apo_sems(etape_apogee, annee_scolaire: int) -> list[dict]:
"""
@ -1025,6 +1114,10 @@ def export_csv_to_apogee(
cr_table = apo_data.build_cr_table()
cr_xls = cr_table.excel()
# ADSUPs
adsup_table = apo_data.build_adsup_table()
adsup_xls = adsup_table.excel() if len(adsup_table) else None
# Create ZIP
if not dest_zip:
data = io.BytesIO()
@ -1050,6 +1143,7 @@ def export_csv_to_apogee(
log_filename = "scodoc-" + basename + ".log.txt"
nar_filename = basename + "-nar" + scu.XLSX_SUFFIX
cr_filename = basename + "-decisions" + scu.XLSX_SUFFIX
adsup_filename = f"{basename}-adsups{scu.XLSX_SUFFIX}"
logf = io.StringIO()
logf.write(f"export_to_apogee du {time.ctime()}\n\n")
@ -1086,6 +1180,8 @@ def export_csv_to_apogee(
"\n\nElements Apogee inconnus dans ces semestres ScoDoc:\n"
+ "\n".join(apo_data.list_unknown_elements())
)
if adsup_xls:
logf.write(f"\n\nADSUP générés: {len(adsup_table)}\n")
log(logf.getvalue()) # sortie aussi sur le log ScoDoc
# Write data to ZIP
@ -1094,6 +1190,8 @@ def export_csv_to_apogee(
if nar_xls:
dest_zip.writestr(nar_filename, nar_xls)
dest_zip.writestr(cr_filename, cr_xls)
if adsup_xls:
dest_zip.writestr(adsup_filename, adsup_xls)
if my_zip:
dest_zip.close()

View File

@ -295,8 +295,15 @@ class ApoCSVReadWrite:
filename=self.get_filename(),
)
cols = {} # { col_id : value }
for i, field in enumerate(fields):
cols[self.col_ids[i]] = field
try:
for i, field in enumerate(fields):
cols[self.col_ids[i]] = field
except IndexError as exc:
raise
raise ScoFormatError(
f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
filename=self.get_filename(),
) from exc
etud_tuples.append(
ApoEtudTuple(
nip=fields[0], # id etudiant

View File

@ -398,7 +398,7 @@ def formsemestre_validation_etud(
selected_choice = choice
break
if not selected_choice:
raise ValueError("code choix invalide ! (%s)" % codechoice)
raise ValueError(f"code choix invalide ! ({codechoice})")
#
Se.valide_decision(selected_choice) # enregistre
return _redirect_valid_choice(
@ -1132,6 +1132,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
},
)
)
ue_codes = sorted(codes_cursus.CODES_JURY_UE)
form_descr += [
(
"date",
@ -1152,6 +1153,18 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
"title": "Moyenne (/20) obtenue dans cette UE:",
},
),
(
"code_jury",
{
"input_type": "menu",
"title": "Code jury",
"explanation": " code donné par le jury (ADM si validée normalement)",
"allow_null": True,
"allowed_values": [""] + ue_codes,
"labels": ["-"] + ue_codes,
"default": ADM,
},
),
]
tf = TrivialFormulator(
request.base_url,
@ -1173,17 +1186,20 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
de {etud.html_link_fiche()}
</h2>
<p class="help">Utiliser cette page pour enregistrer une UE validée antérieurement,
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
<em>dans un semestre hors ScoDoc</em>.</p>
<p class="expl"><b>Les UE validées dans ScoDoc sont déjà
automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré
<b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre
(<em>pour les semestres précédents gérés avec ScoDoc,
passer par la page jury normale)</em>).
<p class="expl"><b>Les UE validées dans ScoDoc sont
automatiquement prises en compte</b>.
</p>
<p>Cette page est surtout utile pour les étudiants ayant
suivi un début de cursus dans <b>un autre établissement</b>, ou qui
ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
</p>
<p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
</p>
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
l'attribution des ECTS si le code jury est validant (ADM).
</p>
<p>Notez que l'UE est validée (ADM), avec enregistrement immédiat de la décision et
l'attribution des ECTS.</p>
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
{_get_etud_ue_cap_html(etud, formsemestre)}
@ -1221,12 +1237,16 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
else:
semestre_id = None
if tf[2]["code_jury"] not in CODES_JURY_UE:
flash("Code UE invalide")
return flask.redirect(dest_url)
do_formsemestre_validate_previous_ue(
formsemestre,
etud.id,
tf[2]["ue_id"],
tf[2]["moy_ue"],
tf[2]["date"],
code=tf[2]["code_jury"],
semestre_id=semestre_id,
)
flash("Validation d'UE enregistrée")
@ -1258,7 +1278,7 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
sur des semestres ou déclarées comme "antérieures" (externes).
</div>
<ul>"""
<ul class="liste_validations">"""
]
for validation in validations:
if validation.formsemestre_id is None:
@ -1267,17 +1287,20 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
origine = f", du semestre {formsemestre.html_link_status()}"
if validation.semestre_id is not None:
origine += f" (<b>S{validation.semestre_id}</b>)"
H.append(
f"""
<li>{validation.html()}
H.append(f"""<li>{validation.html()}""")
if validation.formsemestre.can_edit_jury():
H.append(
f"""
<form class="inline-form">
<button
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
>effacer</button>
</form>
</li>
""",
)
""",
)
else:
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
H.append("</li>")
H.append("</ul></div>")
return "\n".join(H)
@ -1300,7 +1323,7 @@ def do_formsemestre_validate_previous_ue(
ue: UniteEns = UniteEns.query.get_or_404(ue_id)
cnx = ndb.GetDBConnexion()
if ue_coefficient != None:
if ue_coefficient is not None:
sco_formsemestre.do_formsemestre_uecoef_edit_or_create(
cnx, formsemestre.id, ue_id, ue_coefficient
)

View File

@ -59,11 +59,13 @@ from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_t
from app.scodoc.sco_permissions import Permission
def _menu_scolarite(authuser, sem: dict, etudid: int):
def _menu_scolarite(
authuser, formsemestre: FormSemestre, etudid: int, etat_inscription: str
):
"""HTML pour menu "scolarite" pour un etudiant dans un semestre.
Le contenu du menu depend des droits de l'utilisateur et de l'état de l'étudiant.
"""
locked = not sem["etat"]
locked = not formsemestre.etat
if locked:
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
return lockicon # no menu
@ -71,10 +73,10 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
Permission.ScoEtudInscrit
) and not authuser.has_permission(Permission.ScoEtudChangeGroups):
return "" # no menu
ins = sem["ins"]
args = {"etudid": etudid, "formsemestre_id": ins["formsemestre_id"]}
if ins["etat"] != "D":
args = {"etudid": etudid, "formsemestre_id": formsemestre.id}
if etat_inscription != scu.DEMISSION:
dem_title = "Démission"
dem_url = "scolar.form_dem"
else:
@ -82,14 +84,14 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
dem_url = "scolar.do_cancel_dem"
# Note: seul un etudiant inscrit (I) peut devenir défaillant.
if ins["etat"] != codes_cursus.DEF:
if etat_inscription != codes_cursus.DEF:
def_title = "Déclarer défaillance"
def_url = "scolar.form_def"
elif ins["etat"] == codes_cursus.DEF:
elif etat_inscription == codes_cursus.DEF:
def_title = "Annuler la défaillance"
def_url = "scolar.do_cancel_def"
def_enabled = (
(ins["etat"] != "D")
(etat_inscription != scu.DEMISSION)
and authuser.has_permission(Permission.ScoEtudInscrit)
and not locked
)
@ -128,6 +130,12 @@ def _menu_scolarite(authuser, sem: dict, etudid: int):
"enabled": authuser.has_permission(Permission.ScoEtudInscrit)
and not locked,
},
{
"title": "Gérer les validations d'UEs antérieures",
"endpoint": "notes.formsemestre_validate_previous_ue",
"args": args,
"enabled": formsemestre.can_edit_jury(),
},
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
@ -250,8 +258,8 @@ def ficheEtud(etudid=None):
info["last_formsemestre_id"] = ""
sem_info = {}
for sem in info["sems"]:
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
if sem["ins"]["etat"] != scu.INSCRIT:
formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
descr, _ = etud_descr_situation_semestre(
etudid,
formsemestre,
@ -283,7 +291,7 @@ def ficheEtud(etudid=None):
)
grlink = ", ".join(grlinks)
# infos ajoutées au semestre dans le parcours (groupe, menu)
menu = _menu_scolarite(authuser, sem, etudid)
menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"])
if menu:
sem_info[sem["formsemestre_id"]] = (
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"

View File

@ -42,6 +42,7 @@ sem_set_list()
import flask
from flask import g, url_for
from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
@ -52,7 +53,6 @@ from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc.gen_tables import GenTable
from app import log
from app.scodoc.sco_etape_bilan import EtapeBilan
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_vdi import ApoEtapeVDI

View File

@ -1,9 +1,12 @@
div.jury_decisions_list div {
font-size: 120%;
font-weight: bold;
}
span.parcours {
color:blueviolet;
color: blueviolet;
}
div.ue_list_etud_validations ul.liste_validations li {
margin-bottom: 8px;
}

View File

@ -11,27 +11,30 @@ document.addEventListener("DOMContentLoaded", () => {
// Handle button click event here
event.preventDefault();
const etudid = event.target.dataset.etudid;
const v_id = event.target.dataset.v_id;
const validation_id = event.target.dataset.v_id;
const validation_type = event.target.dataset.type;
if (confirm("Supprimer cette validation ?")) {
fetch(
`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${v_id}/delete`,
{
method: "POST",
}
).then((response) => {
// Handle the response
if (response.ok) {
location.reload();
} else {
throw new Error("Request failed");
}
});
delete_validation(etudid, validation_type, validation_id);
}
});
});
});
async function delete_validation(etudid, validation_type, validation_id) {
const response = await fetch(
`${SCO_URL}/../api/etudiant/${etudid}/jury/${validation_type}/${validation_id}/delete`,
{
method: "POST",
}
);
if (response.ok) {
location.reload();
} else {
const data = await response.json();
sco_error_message("erreur: " + data.message);
}
}
function update_ue_list() {
var ue_id = $("#tf_ue_id")[0].value;
if (ue_id) {

View File

@ -9,38 +9,41 @@
{% block app_content %}
<div class="sco_help">
<h2>Calcul automatique des décisions de jury du BUT</h2>
<ul>
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval" sur deux années.
</li>
<li><b>Attention: peut modifier des décisions déjà enregistrées</b>, si la
validation de droit est calculée. Par exemple, vous aviez saisi <b>RAT</b>
pour un étudiant dont les moyennes d'UE dépassent 10 mais qui pour une
raison particulière ne valide pas son année. Le calcul automatique peut
remplacer ce <b>RAT</b> par un <b>ADM</b>, ScoDoc considérant que les
conditions sont satisfaites. On peut éviter cela en laissant une note de
l'étudiant en ATTente.
</li>
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
</li>
<li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
</li>
<li>L'assiduité n'est <b>pas</b> prise en compte. </li>
</ul>
<p>
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
notamment sur les UEs en dessous de 10.
</p>
<div class="warning">
<h2>Calcul automatique des décisions de jury du BUT</h2>
<ul>
<li>Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
(verrouiller le semestre ensuite)
</li>
<li>Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !</li>
</div>
<li>N'enregistre jamais de décisions de l'année scolaire précédente, même
si on a des RCUE "à cheval" sur deux années.
</li>
<li><b>Attention: peut modifier des décisions déjà enregistrées</b>, si la
validation de droit est calculée.
Ce calcul <b>n'utilise que les notes, et pas les décisions manuelles déjà saisies.</b>
<br>
Par exemple, vous aviez saisi <b>ATJ</b> ou <b>RAT</b>
pour un étudiant dont les moyennes d'UE dépassent 10 mais qui pour une
raison particulière ne valide pas son année. Le calcul automatique peut
remplacer ce <b>RAT</b> par un <b>ADM</b>, ScoDoc considérant que les
conditions sont satisfaites. On peut éviter cela en laissant une note de
l'étudiant en ATTente.
</li>
<li>N'enregistre que les décisions <b>validantes de droit: ADM ou CMP</b>.
</li>
<li>N'enregistre pas de décision si l'étudiant a une ou plusieurs notes en ATTente.
</li>
<li>L'assiduité n'est <b>pas</b> prise en compte. </li>
</ul>
<p>
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
notamment sur les UEs en dessous de 10.
</p>
<div class="warning">
<ul>
<li>Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
(verrouiller le semestre ensuite)
</li>
<li>Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !</li>
</div>
</div>

View File

@ -4,8 +4,8 @@
{% if not validations %}
<p>Aucune validation de jury enregistrée pour <b>{{etud.html_link_fiche()|safe}}</b>
sur <b>l'année {{annee}}</b>
de la formation <em>{{ formation.html() }}</em>
sur <b>l'année {{annee}}</b>
de la formation <em>{{ formation.html() }}</em>
</p>
<div style="margin-top: 16px;">
@ -16,7 +16,7 @@ de la formation <em>{{ formation.html() }}</em>
<h2>Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()|safe}} ?</h2>
<p class="help">Affectera toutes les décisions concernant l'année {{annee}} de la formation,
quelle que soit leur origine.</p>
quelle que soit leur origine.</p>
<p>Les décisions concernées sont:</p>
<ul>
@ -34,8 +34,34 @@ quelle que soit leur origine.</p>
{% endif %}
</form>
</div>
{% endif %}
<div class="sco_box">
<div class="sco_box_title">Autres actions:</div>
<ul>
<li><a class="stdlink" href="{{
url_for('notes.jury_delete_manual',
scodoc_dept=g.scodoc_dept,
etudid=etud.id
)
}}">effacer les décisions une à une</a>
</li>
{% if formsemestre_origine is not none %}
<li><a class="stdlink" href="{{
url_for('notes.formsemestre_jury_but_erase',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_origine.id,
etudid=etud.id, only_one_sem=1)
}}">
effacer seulement les décisions émises par le semestre
{{formsemestre_origine.titre_formation(with_sem_idx=1)|safe}}
(efface aussi la décision annuelle)
</a>
</li>
{% endif %}
</ul>
</div>
{% endblock %}

View File

@ -2534,21 +2534,20 @@ def formsemestre_validation_but(
</div>"""
)
else:
erase_span = f"""<a href="{
url_for("notes.formsemestre_jury_but_erase",
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre.id,
etudid=deca.etud.id)}" class="stdlink"
title="efface décisions issues des jurys de cette année"
>effacer décisions de ce jury</a>
erase_span = f"""
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.erase_decisions_annee_formation",
scodoc_dept=g.scodoc_dept, formation_id=deca.formsemestre.formation.id,
etudid=deca.etud.id, annee=deca.annee_but)}"
title="efface toutes décisions concernant le BUT{deca.annee_but}
de cet étudiant (même extérieures ou issues d'un redoublement)"
>effacer toutes ses décisions de BUT{deca.annee_but}</a>
etudid=deca.etud.id, annee=deca.annee_but, formsemestre_id=formsemestre_id)}"
>effacer des décisions de jury</a>
<a style="margin-left: 16px;" class="stdlink"
href="{
url_for("notes.formsemestre_validate_previous_ue",
scodoc_dept=g.scodoc_dept,
etudid=deca.etud.id, formsemestre_id=formsemestre_id)}"
>enregistrer des UEs antérieures</a>
"""
H.append(
f"""<div class="but_settings">
@ -2966,6 +2965,12 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
)
)
validations = jury.erase_decisions_annee_formation(etud, formation, annee)
formsemestre_origine_id = request.args.get("formsemestre_id")
formsemestre_origine = (
FormSemestre.query.get_or_404(formsemestre_origine_id)
if formsemestre_origine_id
else None
)
return render_template(
"jury/erase_decisions_annee_formation.j2",
annee=annee,
@ -2974,6 +2979,7 @@ def erase_decisions_annee_formation(etudid: int, formation_id: int, annee: int):
),
etud=etud,
formation=formation,
formsemestre_origine=formsemestre_origine,
validations=validations,
sco=ScoData(),
title=f"Effacer décisions de jury {etud.nom} - année {annee}",

View File

@ -7,7 +7,7 @@ Create Date: 2023-06-28 09:47:16.591028
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "829683efddc4"
@ -15,30 +15,96 @@ down_revision = "c701224fa255"
branch_labels = None
depends_on = None
Session = sessionmaker()
# Voir https://stackoverflow.com/questions/24082542/check-if-a-table-column-exists-in-the-database-using-sqlalchemy-and-alembic
from sqlalchemy import inspect
def column_exists(table_name, column_name):
bind = op.get_context().bind
insp = inspect(bind)
columns = insp.get_columns(table_name)
return any(c["name"] == column_name for c in columns)
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
if column_exists("apc_validation_annee", "referentiel_competence_id"):
return # utile durant developpement
# Enleve la contrainte erronée
with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
batch_op.drop_constraint(
"apc_validation_annee_etudid_annee_scolaire_ordre_key", type_="unique"
)
# batch_op.create_unique_constraint(
# "apc_validation_annee_etudid_formation_ordre_key",
# ["etudid", "ordre", "formation_id"],
# )
# Ajoute colonne referentiel, nullable pour l'instant
batch_op.add_column(
sa.Column("referentiel_competence_id", sa.Integer(), nullable=True)
)
# ### end Alembic commands ###
# Affecte le referentiel des anciennes validations
bind = op.get_bind()
session = Session(bind=bind)
session.execute(
sa.text(
"""
UPDATE apc_validation_annee AS a
SET referentiel_competence_id = (
SELECT f.referentiel_competence_id
FROM notes_formations f
WHERE f.id = a.formation_id
)
"""
)
)
# En principe, on n'a pas pu entrer de validation sur des formations sans referentiel
# par prudence, on les supprime avant d'ajouter la contrainte
session.execute(
sa.text(
"DELETE FROM apc_validation_annee WHERE referentiel_competence_id is NULL"
)
)
op.alter_column(
"apc_validation_annee",
"referentiel_competence_id",
nullable=False,
)
op.create_foreign_key(
"apc_validation_annee_refcomp_fkey",
"apc_validation_annee",
"apc_referentiel_competences",
["referentiel_competence_id"],
["id"],
)
# Efface les validations d'année dupliquées
# (garde la validation la plus récente)
session.execute(
sa.text(
"""
DELETE FROM apc_validation_annee t1
WHERE t1.id <> (SELECT max(t2.id)
FROM apc_validation_annee t2
WHERE t1.etudid = t2.etudid
AND t1.referentiel_competence_id = t2.referentiel_competence_id
AND t1.ordre = t2.ordre
)
"""
)
)
# Et ajoute la contrainte unicité de décision année par étudiant/ref. comp.:
op.create_unique_constraint(
"apc_validation_annee_etudid_ordre_refcomp_key",
"apc_validation_annee",
["etudid", "ordre", "referentiel_competence_id"],
)
op.drop_column("apc_validation_annee", "formation_id")
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
# Se contente de ré-ajouter la colonne formation_id sans re-générer son contenu
with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
# batch_op.drop_constraint(
# "apc_validation_annee_etudid_formation_ordre_key", type_="unique"
# "apc_validation_annee_etudid_ordre_refcomp_key", type_="unique"
# )
batch_op.create_unique_constraint(
"apc_validation_annee_etudid_annee_scolaire_ordre_key",
["etudid", "annee_scolaire", "ordre"],
)
# ### end Alembic commands ###
# batch_op.drop_column("referentiel_competence_id")
batch_op.add_column(sa.Column("formation_id", sa.Integer(), nullable=True))

View File

@ -1,6 +1,7 @@
[pytest]
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
apo
but_gb
but_gccd
but_mlt

View File

@ -1,13 +1,24 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.4.94"
SCOVERSION = "9.4.96"
SCONAME = "ScoDoc"
SCONEWS = """
<h4>Année 2023</h4>
<ul>
<li>ScoDoc 9.6 (juillet 2023)</li>
<ul>
<li>Nouvelle gestion des absences et assiduité</li>
</ul>
<li>ScoDoc 9.5 (juillet 2023)</li>
<ul>
<li>Version de maintenance (sécurité et correctifs critiques) sur Debian 11: fin de vie: 1/11/2023</li>
</ul>
<li>ScoDoc 9.4</li>
<ul>
<li>Connexion avec service CAS</li>

View File

@ -0,0 +1,76 @@
XX-APO_TITRES-XX
apoC_annee 2021/2022
apoC_cod_dip DIPTIS2
apoC_Cod_Exp 2
apoC_cod_vdi 17
apoC_Fichier_Exp export.txt
apoC_lib_dip BUT INFO TEST
apoC_Titre1 Maquette pour tests unitaires sur un BUT Info S2
apoC_Titre2
XX-APO_TYP_RES-XX
10 AB1 AB2 ABI ABJ ADM AJ AJRO C1 DEF DIF
18 AB1 AB2 ABI ABJ ADM ADMC ADMD AJ AJAC AJAR AJRO ATT B1 C1 COMP DEF DIF NAR
45 ABI ABJ ADAC ADM ADMC ADMD AIR AJ AJAR AJCP AJRO AJS ATT B1 B2 C1 COMP CRED DEF DES DETT DIF ENER ENRA EXC INFA INFO INST LC MACS N1 N2 NAR NON NSUI NVAL OUI SUIV SUPS TELE TOEF TOIE VAL VALC VALR
10 ABI ABJ ADMC COMP DEF DIS NVAL VAL VALC VALR
AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis AJ : Ajourné AJRO : Ajourné - Réorientation Obligatoire C1 : Niveau C1 DEF : Défaillant DIF : Décision différée
AB1 : Ajourné en B2 mais admis en B1 AB2 : ADMIS en B1 mais ajourné en B2 ABI : Absence ABJ : Absence justifiée ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AJ : Ajourné AJAC : Ajourné mais accès autorisé à étape sup. AJAR : Ajourné et Admis A Redoubler AJRO : Ajourné - Réorientation Obligatoire ATT : En attente de décison B1 : Niveau B1 C1 : Niveau C1 COMP : Compensé DEF : Défaillant DIF : Décision différée NAR : Ajourné non admis à redoubler
ABI : Absence ABJ : Absence justifiée ADAC : Admis avant choix ADM : Admis ADMC : Admis avec compensation ADMD : Admis (passage avec dette) AIR : Ingénieur spécialité Informatique appr AJ : Ajourné AJAR : Ajourné et Admis A Redoubler AJCP : Ajourné mais autorisé à compenser AJRO : Ajourné - Réorientation Obligatoire AJS : Ajourné (note éliminatoire) ATT : En attente de décison B1 : Niveau B1 B2 : Niveau B2 C1 : Niveau C1 COMP : Compensé CRED : Eléments en crédits DEF : Défaillant DES : Désistement DETT : Eléments en dettes DIF : Décision différée ENER : Ingénieur spécialité Energétique ENRA : Ingénieur spécialité Energétique appr EXC : Exclu INFA : Ingénieur spécialité Informatique appr INFO : Ingénieur spécialié Informatique INST : Ingénieur spécialité Instrumentation LC : Liste complémentaire MACS : Ingénieur spécialité MACS N1 : Compétences CLES N2 : Niveau N2 NAR : Ajourné non admis à redoubler NON : Non NSUI : Non suivi(e) NVAL : Non Validé(e) OUI : Oui SUIV : Suivi(e) SUPS : Supérieur au seuil TELE : Ingénieur spéciailté Télécommunications TOEF : TOEFL TOIE : TOEIC VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement
ABI : Absence ABJ : Absence justifiée ADMC : Admis avec compensation COMP : Compensé DEF : Défaillant DIS : Dispense examen NVAL : Non Validé(e) VAL : Validé(e) VALC : Validé(e) par compensation VALR : Validé(e) Retrospectivement
XX-APO_COLONNES-XX
apoL_a01_code Type Objet Code Version Année Session Admission/Admissibilité Type Rés. Etudiant Numéro
apoL_a02_nom Nom
apoL_a03_prenom Prénom
apoL_a04_naissance Session Admissibilité Naissance
APO_COL_VAL_DEB
apoL_c0001 ELP V1INFU21 2021 0 1 N V1INFU21 - UE 2.1 Réaliser 0 1 Note
apoL_c0002 ELP V1INFU21 2021 0 1 B 0 1 Barème
apoL_c0003 ELP V1INFU21 2021 0 1 J 0 1 Pts Jury
apoL_c0004 ELP V1INFU21 2021 0 1 R 0 1 Résultat
apoL_c0005 ELP V1INFU22 2021 0 1 N V1INFU22 - UE 2.2 Optimiser 0 1 Note
apoL_c0006 ELP V1INFU22 2021 0 1 B 0 1 Barème
apoL_c0007 ELP V1INFU22 2021 0 1 J 0 1 Pts Jury
apoL_c0008 ELP V1INFU22 2021 0 1 R 0 1 Résultat
apoL_c0009 ELP V1INFU23 2021 0 1 N V1INFU23 - UE 2.3 Administrer 0 1 Note
apoL_c0010 ELP V1INFU23 2021 0 1 B 0 1 Barème
apoL_c0011 ELP V1INFU23 2021 0 1 J 0 1 Pts Jury
apoL_c0012 ELP V1INFU23 2021 0 1 R 0 1 Résultat
apoL_c0013 ELP V1INFU24 2021 0 1 N V1INFU24 - UE 2.4 Gérer 0 1 Note
apoL_c0014 ELP V1INFU24 2021 0 1 B 0 1 Barème
apoL_c0015 ELP V1INFU24 2021 0 1 J 0 1 Pts Jury
apoL_c0016 ELP V1INFU24 2021 0 1 R 0 1 Résultat
apoL_c0017 ELP V1INFU25 2021 0 1 N V1INFU25 - UE 2.5 Conduire 0 1 Note
apoL_c0018 ELP V1INFU25 2021 0 1 B 0 1 Barème
apoL_c0019 ELP V1INFU25 2021 0 1 J 0 1 Pts Jury
apoL_c0020 ELP V1INFU25 2021 0 1 R 0 1 Résultat
apoL_c0021 ELP V1INFU26 2021 0 1 N V1INFU26 - UE 2.6 Travailler 0 1 Note
apoL_c0022 ELP V1INFU26 2021 0 1 B 0 1 Barème
apoL_c0023 ELP V1INFU26 2021 0 1 J 0 1 Pts Jury
apoL_c0024 ELP V1INFU26 2021 0 1 R 0 1 Résultat
apoL_c0025 ELP VINFR201 2021 0 1 N VINFR201 - Développement orienté objets 0 1 Note
apoL_c0026 ELP VINFR201 2021 0 1 B 0 1 Barème
apoL_c0027 ELP VINFR207 2021 0 1 N VINFR207 - Graphes 0 1 Note
apoL_c0028 ELP VINFR207 2021 0 1 B 0 1 Barème
apoL_c0029 ELP VINFPOR2 2021 0 1 N VINFPOR2 - Portfolio 0 1 Note
apoL_c0030 ELP VINFPOR2 2021 0 1 B 0 1 Barème
apoL_c0031 ELP TIRW2 2021 0 1 N TIRW2 - Semestre 2 BUT INFO 2 0 1 Note
apoL_c0032 ELP TIRW2 2021 0 1 B 0 1 Barème
apoL_c0033 ELP TIRW2 2021 0 1 J 0 1 Pts Jury
apoL_c0034 ELP TIRW2 2021 0 1 R 0 1 Résultat
apoL_c0035 ELP TIRO 2021 0 1 N TIRO - Année BUT 1 RT 0 1 Note
apoL_c0036 ELP TIRO 2021 0 1 B 0 1 Barème
apoL_c0037 VET TI1 117 2021 0 1 N TI1 - BUT INFO an1 0 1 Note
apoL_c0038 VET TI1 117 2021 0 1 B 0 1 Barème
apoL_c0039 VET TI1 117 2021 0 1 J 0 1 Pts Jury
apoL_c0040 VET TI1 117 2021 0 1 R 0 1 Résultat
APO_COL_VAL_FIN
apoL_c0041 APO_COL_VAL_FIN
XX-APO_VALEURS-XX
apoL_a01_code apoL_a02_nom apoL_a03_prenom apoL_a04_naissance apoL_c0001 apoL_c0002 apoL_c0003 apoL_c0004 apoL_c0005 apoL_c0006 apoL_c0007 apoL_c0008 apoL_c0009 apoL_c0010 apoL_c0011 apoL_c0012 apoL_c0013 apoL_c0014 apoL_c0015 apoL_c0016 apoL_c0017 apoL_c0018 apoL_c0019 apoL_c0020 apoL_c0021 apoL_c0022 apoL_c0023 apoL_c0024 apoL_c0025 apoL_c0026 apoL_c0027 apoL_c0028 apoL_c0029 apoL_c0030 apoL_c0031 apoL_c0032 apoL_c0033 apoL_c0034 apoL_c0035 apoL_c0036 apoL_c0037 apoL_c0038 apoL_c0039 apoL_c0040
1001 ex_a1 Jean 10/01/2003
1002 ex_a2 Lucie 11/01/2003
1003 ex_b1 Hélène 11/01/2003
1004 ex_b2 Rose 11/01/2003

View File

@ -109,6 +109,16 @@ FormSemestres:
idx: 1
date_debut: 2022-09-02
date_fin: 2023-01-12
S3:
idx: 3
codes_parcours: ['AII']
date_debut: 2022-09-01
date_fin: 2023-01-15
S4:
idx: 4
codes_parcours: ['AII']
date_debut: 2023-01-16
date_fin: 2023-07-10
Etudiants:
geii8:
@ -1265,3 +1275,135 @@ Etudiants:
moy_ue: 13.5000
# decisions_rcues: aucun RCUE en S1-red
decision_annee: AJ
geii89:
prenom: etugeii89
civilite: M
formsemestres:
S1:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 13.5000
"S1.2": 13.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: ATJ # A cause des absences
moy_ue: 13.5000
"UE12":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ATJ # A cause des absences
moy_ue: 13.0000
S2:
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 14.5000
"S2.2": 14.0000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: True # d'apres les notes, on *pourrait* passer
autorisations_inscription: [2] # et le jury manuel nous fait passer
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "ATJ", "..." ]
decisions_ues:
"UE21":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ATJ
moy_ue: 14.5000
"UE22":
codes: [ "ADM", "..." ]
code_valide: ADM
decision_jury: ATJ
moy_ue: 14.0000
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: ADM # le code proposé en auto
decision_jury: ATJ # le code forcé manuellement par le jury
rcue:
# moy_rcue: 14.0000 # Pas de moyenne calculée
est_compensable: False
"UE12":
code_valide: ADM # le code proposé en auto
decision_jury: ATJ # le code forcé manuellement par le jury
rcue:
# moy_rcue: 13.5000 # Pas de moyenne calculée
est_compensable: False
decision_annee: ATJ # Passage tout de même en S3
#
# ----------------------- geii90 : ADSUP envoyés par BUT2 vers BUT1
#
geii90:
prenom: etugeii90
civilite: M
code_nip: geii90
formsemestres:
S1: # 2 UEs, les deux en AJ
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S1.1": 9.5000
"S1.2": 8.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: [ "AJ", "..." ]
"UE12":
codes: [ "AJ", "..." ]
S2: # pareil, mais le jury le fait passer en S3
notes_modules: # on joue avec les SAE seulement car elles sont "diagonales"
"S2.1": 9.8000
"S2.2": 9.9000
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False # d'apres les notes, on ne peut pas passer
autorisations_inscription: [2] # et le jury manuel nous fait passer
nb_competences: 2
nb_rcue_annee: 2
valide_moitie_rcue: False
codes: [ "ADJ", "ATJ", "RED", "..." ]
code_valide: RED # le code proposé en auto
decisions_ues:
"UE21":
codes: [ "AJ", "..." ]
code_valide: AJ
moy_ue: 9.8
"UE22":
code_valide: AJ
moy_ue: 9.9
decisions_rcues: # on repère ici les RCUE par l'acronyme de leur 1ere UE (donc du S1)
"UE11":
code_valide: AJ # le code proposé en auto
rcue:
# moy_rcue: 14.0000 # Pas de moyenne calculée
est_compensable: False
"UE12":
code_valide: AJ # le code proposé en auto
rcue:
# moy_rcue: 13.5000 # Pas de moyenne calculée
est_compensable: False
decision_annee: ADJ # Passage tout de même en S3 !
S3: # le S3 avec 4 niveaux
parcours: AII
notes_modules: # combinaison pour avoir ADM AJ AJ AJ
"AII3": 9
"ER3": 10.75
"AU3": 8
attendu: # les codes jury que l'on doit vérifier
deca:
passage_de_droit: False # d'apres les notes, on ne peut pas passer
autorisations_inscription: [4] # passe en S4
nb_competences: 4
S4: # le S4 avec 4 niveaux
parcours: AII
notes_modules: # combinaison pour avoir ADM ADM ADM AJ
"PF4": 12
"SAE4AII": 8

View File

@ -1,4 +1,4 @@
# Tests unitaires
# Tests unitaires
# Le BUT Info a 4 parcours qui partagent certains niveaux de compétences
# mais à ces niveaux sont associés des UEs dont les coefficients des ressources
# varient selon le parcours.
@ -14,58 +14,58 @@ Formation:
# nota: les associations UE/Niveaux sont déjà données dans ce fichier XML.
ues:
# S1
'UE11':
"UE11":
annee: BUT1
'UE12':
"UE12":
annee: BUT1
'UE13':
"UE13":
annee: BUT1
'UE14':
"UE14":
annee: BUT1
'UE15':
"UE15":
annee: BUT1
'UE16':
"UE16":
annee: BUT1
# S2
'UE21':
"UE21":
annee: BUT1
'UE22':
"UE22":
annee: BUT1
'UE23':
"UE23":
annee: BUT1
'UE24':
"UE24":
annee: BUT1
'UE25':
"UE25":
annee: BUT1
'UE26':
"UE26":
annee: BUT1
# S3
'UE31':
"UE31":
annee: BUT2
'UE32':
"UE32":
annee: BUT2
'UE33':
"UE33":
annee: BUT2
'UE34':
"UE34":
annee: BUT2
'UE35':
"UE35":
annee: BUT2
'UE36':
"UE36":
annee: BUT2
# S4
'UE41-A': # UE pour le parcours A
"UE41-A": # UE pour le parcours A
annee: BUT2
'UE41-B': # UE pour le parcours B (même contenu, coefs différents)
"UE41-B": # UE pour le parcours B (même contenu, coefs différents)
annee: BUT2
'UE42':
"UE42":
annee: BUT2
'UE43':
"UE43":
annee: BUT2
'UE44':
"UE44":
annee: BUT2
'UE45':
"UE45":
annee: BUT2
'UE46':
"UE46":
annee: BUT2
FormSemestres:
@ -74,37 +74,41 @@ FormSemestres:
idx: 1
date_debut: 2021-09-01
date_fin: 2022-01-15
codes_parcours: ['A', 'B']
codes_parcours: ["A", "B"]
S2:
idx: 2
date_debut: 2022-01-16
date_fin: 2022-06-30
codes_parcours: ['A', 'B']
codes_parcours: ["A", "B"]
elt_sem_apo: TIRW2
elt_annee_apo: TIRO
etape_apo: TI1!117
S3:
idx: 3
date_debut: 2022-09-01
date_fin: 2023-01-15
codes_parcours: ['A', 'B']
codes_parcours: ["A", "B"]
S4:
idx: 4
date_debut: 2023-01-16
date_fin: 2023-06-30
codes_parcours: ['A', 'B']
codes_parcours: ["A", "B"]
S5:
idx: 5
date_debut: 2023-09-01
date_fin: 2024-01-15
codes_parcours: ['A', 'B']
codes_parcours: ["A", "B"]
S6:
idx: 6
date_debut: 2024-01-16
date_fin: 2024-06-30
codes_parcours: ['A', 'B']
codes_parcours: ["A", "B"]
Etudiants:
ex_a1: # cursus S1 -> S6, valide tout
prenom: Jean
civilite: M
code_nip: 1001
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@ -115,6 +119,7 @@ Etudiants:
parcours: A
notes_modules:
"P2": 12
"R2.04-A": 16
S3:
parcours: A
notes_modules:
@ -135,6 +140,7 @@ Etudiants:
ex_a2: # cursus S1 -> S6, valide tout sauf S5
prenom: Lucie
civilite: F
code_nip: 1002
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@ -145,6 +151,7 @@ Etudiants:
parcours: A
notes_modules:
"P2": 12
"R2.04-A": 17
S3:
parcours: A
notes_modules:
@ -161,10 +168,11 @@ Etudiants:
parcours: A
notes_modules:
"P6-A": 16
ex_b1: # cursus S1 -> S6, valide tout
prenom: Hélène
civilite: F
code_nip: 1003
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@ -175,6 +183,7 @@ Etudiants:
parcours: B
notes_modules:
"P2": 12
"R2.04-B": 18
S3:
parcours: B
notes_modules:
@ -191,10 +200,11 @@ Etudiants:
parcours: B
notes_modules:
"P6-B": 16
ex_b2: # cursus S1 -> S6, valide tout sauf S6
prenom: Rose
civilite: F
code_nip: 1004
formsemestres:
# on ne note que le portfolio, qui affecte toutes les UEs
S1:
@ -205,6 +215,7 @@ Etudiants:
parcours: B
notes_modules:
"P2": 12
"R2.04-B": 19
S3:
parcours: B
notes_modules:

View File

@ -0,0 +1,54 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
""" Test export Apogéee
Ces tests sont généralement lents (construction de la base),
et donc marqués par `@pytest.mark.slow`.
Certains sont aussi marqués par @pytest.mark.lemans ou @pytest.mark.lyon
pour lancer certains tests spécifiques seulement.
Exemple utilisation spécifique:
# test sur "apo" seulement:
pytest --pdb -m apo tests/unit/test_apogee_export.py
Elements Apogée simulés:
- UEs : TIU2x
- Ressources: R2.xy : TIRxy (VRETR201 -> TIR201)
"""
import pytest
from tests.unit import yaml_setup, yaml_setup_but
import app
from app.but.jury_but_validation_auto import formsemestre_validation_auto_but
from app.models import Formation, FormSemestre, UniteEns
from config import TestConfig
DEPT = TestConfig.DEPT_TEST
@pytest.mark.skip # Ce "test" est utilisé comme setup pour développer, pas comme test unitaire routinier
@pytest.mark.slow
@pytest.mark.apo
def test_refcomp_niveaux_info(test_client):
"""Test niveaux / parcours / UE pour un BUT INFO
avec parcours A et B, même compétences mais coefs différents
selon le parcours.
"""
# WIP
# pour le moment juste le chargement de la formation, du ref. comp, et des UE du S4.
app.set_sco_dept(DEPT)
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/cursus_but_info.yaml"
)
for formsemestre_titre in formsemestre_titres:
formsemestre = yaml_setup.create_formsemestre_with_etuds(
doc, formation, formsemestre_titre
)
#

View File

@ -31,7 +31,13 @@ def test_cursus_but_jury_gb(test_client):
app.set_sco_dept(DEPT)
# login_user(User.query.filter_by(user_name="admin").first()) # XXX pour tests manuels
# ctx.push() # XXX
doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_gb.yaml")
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/cursus_but_gb.yaml"
)
for formsemestre_titre in formsemestre_titres:
formsemestre = yaml_setup.create_formsemestre_with_etuds(
doc, formation, formsemestre_titre
)
formsemestre: FormSemestre = FormSemestre.query.filter_by(titre="S3").first()
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
cursus = FormSemestreCursusBUT(res)
@ -72,7 +78,13 @@ def test_refcomp_niveaux_info(test_client):
# WIP
# pour le moment juste le chargement de la formation, du ref. comp, et des UE du S4.
app.set_sco_dept(DEPT)
doc = yaml_setup.setup_from_yaml("tests/ressources/yaml/cursus_but_info.yaml")
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/cursus_but_info.yaml"
)
for formsemestre_titre in formsemestre_titres:
formsemestre = yaml_setup.create_formsemestre_with_etuds(
doc, formation, formsemestre_titre
)
formsemestre: FormSemestre = FormSemestre.query.filter_by(titre="S4").first()
assert formsemestre
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)

View File

@ -44,9 +44,18 @@ def test_formsemestres_associate_new_version(test_client):
app.set_sco_dept(DEPT)
# Construit la base de test GB une seule fois
# puis lance les tests de jury
yaml_setup.setup_from_yaml("tests/ressources/yaml/simple_formsemestres.yaml")
formation = Formation.query.filter_by(acronyme="BUT GEII", version=1).first()
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/simple_formsemestres.yaml"
)
for formsemestre_titre in formsemestre_titres:
formsemestre = yaml_setup.create_formsemestre_with_etuds(
doc, formation, formsemestre_titre
)
assert formsemestre
formation_geii = Formation.query.filter_by(acronyme="BUT GEII", version=1).first()
assert formation_geii.id == formation.id
formsemestres = formation.formsemestres.all()
assert len(formsemestres) == len(formsemestre_titres)
# On a deux S1:
assert len(formsemestres) == 2
assert {s.semestre_id for s in formsemestres} == {1}
@ -70,7 +79,14 @@ def test_formsemestre_misc_views(test_client):
Note: les anciennes vues renvoient souvent des str au lieu de Response.
"""
app.set_sco_dept(DEPT)
yaml_setup.setup_from_yaml("tests/ressources/yaml/simple_formsemestres.yaml")
doc, formation, formsemestre_titres = yaml_setup.setup_from_yaml(
"tests/ressources/yaml/simple_formsemestres.yaml"
)
for formsemestre_titre in formsemestre_titres:
formsemestre = yaml_setup.create_formsemestre_with_etuds(
doc, formation, formsemestre_titre
)
assert formsemestre
formsemestre: FormSemestre = FormSemestre.query.first()
# ----- MENU SEMESTRE

View File

@ -99,6 +99,9 @@ def create_formsemestre(
titre: str,
date_debut: str,
date_fin: str,
elt_sem_apo: str = None,
elt_annee_apo: str = None,
etape_apo: str = None,
) -> FormSemestre:
"Création d'un formsemestre, avec ses modimpls et évaluations"
assert formation.is_apc() or not parcours # parcours seulement si APC
@ -110,11 +113,15 @@ def create_formsemestre(
semestre_id=semestre_id,
date_debut=date_debut,
date_fin=date_fin,
elt_sem_apo=elt_sem_apo,
elt_annee_apo=elt_annee_apo,
)
# set responsable (list)
a_user = User.query.first()
formsemestre.responsables = [a_user]
db.session.add(formsemestre)
db.session.flush()
formsemestre.add_etape(etape_apo)
# Ajoute tous les modules du semestre sans parcours OU avec l'un des parcours indiqués
sem_parcours_ids = {p.id for p in parcours}
modules = [
@ -228,6 +235,10 @@ def setup_formsemestre(
assert parcour is not None
parcours.append(parcour)
elt_sem_apo = infos.get("elt_sem_apo")
elt_annee_apo = infos.get("elt_annee_apo")
etape_apo = infos.get("etape_apo")
formsemestre = create_formsemestre(
formation,
parcours,
@ -235,6 +246,9 @@ def setup_formsemestre(
formsemestre_titre,
infos["date_debut"],
infos["date_fin"],
elt_sem_apo=elt_sem_apo,
elt_annee_apo=elt_annee_apo,
etape_apo=etape_apo,
)
db.session.flush()
@ -257,6 +271,7 @@ def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""):
# Création des étudiants (sauf si déjà existants)
prenom = infos.get("prenom", "prénom")
civilite = infos.get("civilite", "X")
code_nip = infos.get("code_nip", None)
etud = Identite.query.filter_by(
nom=nom, prenom=prenom, civilite=civilite
).first()
@ -266,6 +281,7 @@ def inscrit_les_etudiants(doc: dict, formsemestre_titre: str = ""):
nom=nom,
prenom=prenom,
civilite=civilite,
code_nip=code_nip,
)
db.session.add(etud)
db.session.commit()