Jury BUT: cas avec redoublement en changeant de parcours. Fix #988 : détermination parcours, compte ECTS, DUT120.

This commit is contained in:
Emmanuel Viennet 2024-09-07 23:55:27 +02:00
parent 322870c7e9
commit 495aac82ae
8 changed files with 129 additions and 62 deletions

View File

@ -64,7 +64,7 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
Considère le parcours du semestre en cours (res).
"""
parcour_id = self.nt.etuds_parcour_id.get(self.etud.id)
return but_parcours_validated(self.etud.id, parcour_id)
return but_parcours_validated(self.etud, parcour_id)
def but_annee_validated(
@ -81,14 +81,14 @@ def but_annee_validated(
)
def but_parcours_validated(etudid: int, parcour_id: int | None) -> bool:
def but_parcours_validated(etud: Identite, parcour_id: int | None) -> bool:
"""Détermine si le parcours BUT est validé.
= 180 ECTS acquis dans les UEs du parcours.
"""
if parcour_id is None:
return False # étudiant non inscrit à un parcours
# Les ECTS
validations = but_validations_ues_parcours(etudid, parcour_id)
validations = but_validations_ues_parcours(etud, parcour_id)
ects_acquis = validations_count_ects(validations)
return ects_acquis >= CursusBUT.ECTS_DIPLOME
@ -113,20 +113,10 @@ class EtudCursusBUT:
#
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
self.parcour: ApcParcours = get_etud_parcours(
etud, formation.referentiel_competence_id
)
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
"{ annee:int : liste des niveaux à valider }"
@ -229,8 +219,12 @@ class EtudCursusBUT:
return annee in [n.annee for n in self.competences[competence_id].niveaux]
def get_ects_acquis(self) -> int:
"Nombre d'ECTS validés par etud dans le BUT de ce référentiel"
return but_ects_valides(self.etud, self.formation.referentiel_competence.id)
"Nombre d'ECTS validés par etud dans le parcours BUT de ce référentiel"
return but_ects_valides(
self.etud,
self.formation.referentiel_competence.id,
parcour_id=self.parcour.id if self.parcour is not None else None,
)
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
"""Cherche les validations de jury enregistrées pour chaque niveau
@ -403,17 +397,48 @@ class FormSemestreCursusBUT:
# "cache { competence_id : competence }"
def get_etud_parcours(
etud: Identite, referentiel_competence_id: int | None
) -> ApcParcours | None:
"""Le parcours de l'étudiant dans ce réf. de compétence, ou None.
= celui du DERNIER semestre suivi dans le référentiel de compétence
(peut être None si l'incription n'a pas de parcours)
"""
inscriptions = sorted(
[
ins
for ins in etud.formsemestre_inscriptions
if ins.formsemestre.formation.referentiel_competence
and (
ins.formsemestre.formation.referentiel_competence.id
== referentiel_competence_id
)
],
key=lambda s: (s.formsemestre.date_debut, s.formsemestre.semestre_id),
)
return inscriptions[-1].parcour if inscriptions else None
def but_ects_valides(
etud: Identite,
referentiel_competence_id: int,
referentiel_competence_id: int | None = None,
annees_but: None | Iterable[str] = None,
parcour_id: int | None = None,
) -> int:
"""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.
On peut spécifier soit le referentiel_competence_id, soit le parcour.
Si parcour est spécifié, ne prend que les UEs de ce parcours et du tronc commun.
Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années.
"""
validations = but_validations_ues(etud, referentiel_competence_id, annees_but)
validations = (
but_validations_ues_parcours(etud, parcour_id, annees_but)
if parcour_id is not None
else but_validations_ues(etud, referentiel_competence_id, annees_but)
)
return validations_count_ects(validations)
@ -472,7 +497,7 @@ def sorted_validations(validations) -> list[ScolarFormSemestreValidation]:
def but_validations_ues_parcours(
etudid: int, parcour_id: int
etud: Identite, parcour_id: int, annees_but: None | Iterable[str] = None
) -> list[ScolarFormSemestreValidation]:
"""Query les validations d'UEs pour cet étudiant
dans des UEs appartenant à ce parcours ou à son tronc commun.
@ -485,7 +510,7 @@ def but_validations_ues_parcours(
# Les validations d'UE de ce parcours ou du tronc commun pour cet étudiant:
validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None)
.join(UniteEns)
.filter(
@ -499,6 +524,11 @@ def but_validations_ues_parcours(
)
)
)
# restreint à certaines années (utile pour les ECTS du DUT120)
if annees_but:
validations = validations.join(ApcNiveau).filter(
ApcNiveau.annee.in_(annees_but)
)
return sorted_validations(validations)

View File

@ -11,8 +11,7 @@ from flask import g, request, url_for
from openpyxl.styles import Alignment
from app import log
from app.but import jury_but
from app.but.cursus_but import but_ects_valides
from app.but import cursus_but, jury_but
from app.models.but_validations import ValidationDUT120
from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre
@ -155,7 +154,14 @@ def pvjury_table_but(
except ScoValueError:
deca = None
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
parcour = cursus_but.get_etud_parcours(
etud, formsemestre.formation.referentiel_competence_id
)
ects_but_valides = (
cursus_but.but_ects_valides(etud, parcour_id=parcour.id)
if parcour
else cursus_but.but_ects_valides(etud, referentiel_competence_id)
)
has_diplome = deca.valide_diplome() if deca else False
diplome_lst = ["ADM"] if has_diplome else []
validation_dut120 = ValidationDUT120.query.filter_by(

View File

@ -27,12 +27,22 @@ from app.views import ScoData
def etud_valide_dut120(etud: Identite, referentiel_competence_id: int) -> bool:
"""Vrai si l'étudiant satisfait les conditions pour valider le DUT120"""
ects_but1_but2 = cursus_but.but_ects_valides(
etud, referentiel_competence_id, annees_but=("BUT1", "BUT2")
)
ects_but1_but2 = etud_ects_but1_but2(etud, referentiel_competence_id)
return ects_but1_but2 >= 120
def etud_ects_but1_but2(etud, referentiel_competence_id: int) -> float:
"""Les ECTS enregistré en BUT1 et BUT2 de ce ref. et du parcours"""
# parcours de la dernière inscription
parcour = cursus_but.get_etud_parcours(etud, referentiel_competence_id)
return cursus_but.but_ects_valides(
etud,
referentiel_competence_id,
parcour_id=parcour.id if parcour else None,
annees_but=("BUT1", "BUT2"),
)
class ValidationDUT120Form(FlaskForm):
"Formulaire validation DUT120"
submit = SubmitField("Enregistrer le diplôme DUT 120")
@ -61,16 +71,13 @@ def validate_dut120_etud(etudid: int, formsemestre_id: int):
formsemestre_id=formsemestre_id,
)
)
ects_but1_but2 = cursus_but.but_ects_valides(
etud, refcomp.id, annees_but=("BUT1", "BUT2")
)
ects_but1_but2 = etud_ects_but1_but2(etud, refcomp.id)
form = ValidationDUT120Form()
# Check if ValidationDUT120 instance already exists
existing_validation = ValidationDUT120.query.filter_by(
etudid=etud.id, referentiel_competence_id=refcomp.id
).first()
if existing_validation:
flash("DUT120 déjà validé", "info")
etud_can_validate_dut = False

View File

@ -9,7 +9,7 @@
from flask import render_template
from app import log
from app import db, log
from app.but import cursus_but
from app.models import (
ApcCompetence,
@ -20,10 +20,11 @@ from app.models import (
Formation,
FormSemestre,
Identite,
UniteEns,
# ScolarAutorisationInscription,
ScolarFormSemestreValidation,
UniteEns,
)
from app.models.ues import UEParcours
from app.scodoc import codes_cursus
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.views import ScoData
@ -49,7 +50,9 @@ def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = Fa
)
ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud)
rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud)
rcue_validation_by_niveau = get_rcue_validation_by_niveau(
refcomp, etud, None if parcour is None else parcour.id
)
ects_acquis = sum((v.ects() for v in ue_validation_by_niveau.values()))
return render_template(
@ -71,17 +74,18 @@ def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = Fa
def get_ue_validation_by_niveau(
refcomp: ApcReferentielCompetences, etud: Identite
) -> dict[tuple[int, str], ScolarFormSemestreValidation]:
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences
dans le parcours suivi par l'étudiant (celui de son semestre le plus récent
dans un semestre de ce référentiel).
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation.
"""
validations: list[ScolarFormSemestreValidation] = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.join(UniteEns)
.join(ApcNiveau)
.join(ApcCompetence)
.filter_by(referentiel_id=refcomp.id)
.all()
parcour = cursus_but.get_etud_parcours(etud, refcomp.id)
validations = (
cursus_but.but_validations_ues_parcours(etud, parcour.id)
if parcour is not None
else cursus_but.but_validations_ues(etud, refcomp.id)
)
# La meilleure validation pour chaque UE
ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation }
for validation in validations:
@ -104,10 +108,12 @@ def get_ue_validation_by_niveau(
def get_rcue_validation_by_niveau(
refcomp: ApcReferentielCompetences, etud: Identite
refcomp: ApcReferentielCompetences, etud: Identite, parcour_id: int | None
) -> dict[int, ApcValidationRCUE]:
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
Si parcour_id n'est pas None, restreint aux niveaux de ce parcours
et du tronc commun.
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation.
"""
validations: list[ApcValidationRCUE] = (
ApcValidationRCUE.query.filter_by(etudid=etud.id)
@ -115,6 +121,16 @@ def get_rcue_validation_by_niveau(
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
.join(ApcCompetence)
.filter_by(referentiel_id=refcomp.id)
.filter(
db.or_(
UniteEns.id.in_(
UEParcours.query.with_entities(UEParcours.ue_id).filter(
UEParcours.parcours_id == parcour_id
)
),
~UniteEns.id.in_(UEParcours.query.with_entities(UEParcours.ue_id)),
)
)
.all()
)
return {

View File

@ -113,7 +113,7 @@ class TableJury(TableRecap):
if res.is_apc and res.formsemestre.semestre_id == 6:
# on ne vérifie le diplôme que dans ce cas pour ne pas ralentir
if cursus_but.but_parcours_validated(
etud.id, res.etuds_parcour_id.get(etud.id)
etud, res.etuds_parcour_id.get(etud.id)
):
row.add_cell(
"autorisations_inscription",
@ -143,15 +143,20 @@ class TableJury(TableRecap):
group="jury_code_sem",
classes=["recorded_code"],
)
# ECTS acquis en BUT
# ECTS acquis en BUT (dans le parcours du semestre affiché)
if res.formsemestre.formation.referentiel_competence_id:
parcour_id = res.etuds_parcour_id.get(etud.id)
row.add_cell(
"ects_acquis",
"ECTS",
# res.get_etud_ects_valides(etud.id),
# cette recherche augmente de 10% le temps de construction de la table
cursus_but.but_ects_valides(
(
cursus_but.but_ects_valides(etud, parcour_id=parcour_id)
if parcour_id is not None
else cursus_but.but_ects_valides(
etud, res.formsemestre.formation.referentiel_competence_id
)
),
group="jury_code_sem",
classes=["recorded_code"],

View File

@ -10,7 +10,7 @@
<div class="bull_head jury_but">
<div>
<div class="titre_parcours">Validation du DUT en 120 ECTS dans un parcours BUT</div>
<h2>Validation du DUT en 120 ECTS dans un parcours BUT</h2>
<div class="nom_etud">{{etud.html_link_fiche()|safe}}</div>
</div>
<div class="bull_photo">
@ -20,7 +20,7 @@
</div>
</div>
<div class="help">
<div class="scobox explanation help">
<p>Les étudiants de BUT peuvent demander lattribution du <em>diplôme universitaire de technologie</em>
(DUT) au terme de lacquisition des 120 premiers crédits européens du cursus.
</p>
@ -29,12 +29,13 @@ une formation utilisant une autre version de référentiel, pensez à revalider
</p>
</div>
<div style="margin-top: 16px;">
<div class="scobox">
<div>
{{etud.html_link_fiche()|safe}} a acquis <b>{{ects_but1_but2}} ECTS</b> en BUT1 et BUT2.
{% if not validation %}
Son DUT n'est pas encore enregistré dans cette spécialité.
{% endif %}
</div>
</div>
{% if etud_can_validate_dut %}
<form method="POST" action="">
@ -65,4 +66,6 @@ une formation utilisant une autre version de référentiel, pensez à revalider
{% endif %}
{% endif %}
</div>
{% endblock %}

View File

@ -52,7 +52,7 @@
{% if parcour %}
parcours {{parcour.code}} « {{parcour.libelle}} »
{% else %}
non inscrit{{sco.etud.e}} à un parcours de la spécialité
<span class="warning">non inscrit{{sco.etud.e}} à un parcours de la spécialité</span>
{% endif %}
</div>
@ -164,7 +164,7 @@
</div>
{% endif %}
<div class="help">
<div class="scobox explanation help">
<p>Cette page montre les validations d'UEs et de niveaux de compétences (RCUEs)
de {{sco.etud.html_link_fiche()|safe}}

View File

@ -920,7 +920,7 @@ def jury_delete_manual(etudid: int):
@scodoc
@permission_required(Permission.ScoView)
def etud_bilan_ects(etudid: int):
"""Page bilan de tous els ECTS acquis par un étudiant.
"""Page bilan de tous les ECTS acquis par un étudiant.
Plusieurs formations (eg DUT, LP) peuvent être concernées.
"""
etud = Identite.get_etud(etudid)