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). Considère le parcours du semestre en cours (res).
""" """
parcour_id = self.nt.etuds_parcour_id.get(self.etud.id) 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( 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é. """Détermine si le parcours BUT est validé.
= 180 ECTS acquis dans les UEs du parcours. = 180 ECTS acquis dans les UEs du parcours.
""" """
if parcour_id is None: if parcour_id is None:
return False # étudiant non inscrit à un parcours return False # étudiant non inscrit à un parcours
# Les ECTS # Les ECTS
validations = but_validations_ues_parcours(etudid, parcour_id) validations = but_validations_ues_parcours(etud, parcour_id)
ects_acquis = validations_count_ects(validations) ects_acquis = validations_count_ects(validations)
return ects_acquis >= CursusBUT.ECTS_DIPLOME return ects_acquis >= CursusBUT.ECTS_DIPLOME
@ -113,20 +113,10 @@ class EtudCursusBUT:
# #
self.etud = etud self.etud = etud
self.formation = formation 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" "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)" "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {} self.niveaux_by_annee: dict[int, list[ApcNiveau]] = {}
"{ annee:int : liste des niveaux à valider }" "{ 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] return annee in [n.annee for n in self.competences[competence_id].niveaux]
def get_ects_acquis(self) -> int: def get_ects_acquis(self) -> int:
"Nombre d'ECTS validés par etud dans le BUT de ce référentiel" "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) 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]]: def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
"""Cherche les validations de jury enregistrées pour chaque niveau """Cherche les validations de jury enregistrées pour chaque niveau
@ -403,17 +397,48 @@ class FormSemestreCursusBUT:
# "cache { competence_id : competence }" # "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( def but_ects_valides(
etud: Identite, etud: Identite,
referentiel_competence_id: int, referentiel_competence_id: int | None = None,
annees_but: None | Iterable[str] = None, annees_but: None | Iterable[str] = None,
parcour_id: int | None = None,
) -> int: ) -> int:
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué. """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, 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. 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. 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) return validations_count_ects(validations)
@ -472,7 +497,7 @@ def sorted_validations(validations) -> list[ScolarFormSemestreValidation]:
def but_validations_ues_parcours( def but_validations_ues_parcours(
etudid: int, parcour_id: int etud: Identite, parcour_id: int, annees_but: None | Iterable[str] = None
) -> list[ScolarFormSemestreValidation]: ) -> list[ScolarFormSemestreValidation]:
"""Query les validations d'UEs pour cet étudiant """Query les validations d'UEs pour cet étudiant
dans des UEs appartenant à ce parcours ou à son tronc commun. 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: # Les validations d'UE de ce parcours ou du tronc commun pour cet étudiant:
validations = ( validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etudid) ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None) .filter(ScolarFormSemestreValidation.ue_id != None)
.join(UniteEns) .join(UniteEns)
.filter( .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) return sorted_validations(validations)

View File

@ -11,8 +11,7 @@ from flask import g, request, url_for
from openpyxl.styles import Alignment from openpyxl.styles import Alignment
from app import log from app import log
from app.but import jury_but from app.but import cursus_but, jury_but
from app.but.cursus_but import but_ects_valides
from app.models.but_validations import ValidationDUT120 from app.models.but_validations import ValidationDUT120
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestre
@ -155,7 +154,14 @@ def pvjury_table_but(
except ScoValueError: except ScoValueError:
deca = None 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 has_diplome = deca.valide_diplome() if deca else False
diplome_lst = ["ADM"] if has_diplome else [] diplome_lst = ["ADM"] if has_diplome else []
validation_dut120 = ValidationDUT120.query.filter_by( 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: def etud_valide_dut120(etud: Identite, referentiel_competence_id: int) -> bool:
"""Vrai si l'étudiant satisfait les conditions pour valider le DUT120""" """Vrai si l'étudiant satisfait les conditions pour valider le DUT120"""
ects_but1_but2 = cursus_but.but_ects_valides( ects_but1_but2 = etud_ects_but1_but2(etud, referentiel_competence_id)
etud, referentiel_competence_id, annees_but=("BUT1", "BUT2")
)
return ects_but1_but2 >= 120 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): class ValidationDUT120Form(FlaskForm):
"Formulaire validation DUT120" "Formulaire validation DUT120"
submit = SubmitField("Enregistrer le diplôme DUT 120") 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, formsemestre_id=formsemestre_id,
) )
) )
ects_but1_but2 = etud_ects_but1_but2(etud, refcomp.id)
ects_but1_but2 = cursus_but.but_ects_valides(
etud, refcomp.id, annees_but=("BUT1", "BUT2")
)
form = ValidationDUT120Form() form = ValidationDUT120Form()
# Check if ValidationDUT120 instance already exists # Check if ValidationDUT120 instance already exists
existing_validation = ValidationDUT120.query.filter_by( existing_validation = ValidationDUT120.query.filter_by(
etudid=etud.id, referentiel_competence_id=refcomp.id etudid=etud.id, referentiel_competence_id=refcomp.id
).first() ).first()
if existing_validation: if existing_validation:
flash("DUT120 déjà validé", "info") flash("DUT120 déjà validé", "info")
etud_can_validate_dut = False etud_can_validate_dut = False

View File

@ -9,7 +9,7 @@
from flask import render_template from flask import render_template
from app import log from app import db, log
from app.but import cursus_but from app.but import cursus_but
from app.models import ( from app.models import (
ApcCompetence, ApcCompetence,
@ -20,10 +20,11 @@ from app.models import (
Formation, Formation,
FormSemestre, FormSemestre,
Identite, Identite,
UniteEns,
# ScolarAutorisationInscription, # ScolarAutorisationInscription,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
UniteEns,
) )
from app.models.ues import UEParcours
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.views import ScoData 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) 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())) ects_acquis = sum((v.ects() for v in ue_validation_by_niveau.values()))
return render_template( return render_template(
@ -71,17 +74,18 @@ def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = Fa
def get_ue_validation_by_niveau( def get_ue_validation_by_niveau(
refcomp: ApcReferentielCompetences, etud: Identite refcomp: ApcReferentielCompetences, etud: Identite
) -> dict[tuple[int, str], ScolarFormSemestreValidation]: ) -> dict[tuple[int, str], ScolarFormSemestreValidation]:
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences. """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 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] = ( parcour = cursus_but.get_etud_parcours(etud, refcomp.id)
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) validations = (
.join(UniteEns) cursus_but.but_validations_ues_parcours(etud, parcour.id)
.join(ApcNiveau) if parcour is not None
.join(ApcCompetence) else cursus_but.but_validations_ues(etud, refcomp.id)
.filter_by(referentiel_id=refcomp.id)
.all()
) )
# La meilleure validation pour chaque UE # La meilleure validation pour chaque UE
ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation } ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation }
for validation in validations: for validation in validations:
@ -104,10 +108,12 @@ def get_ue_validation_by_niveau(
def get_rcue_validation_by_niveau( def get_rcue_validation_by_niveau(
refcomp: ApcReferentielCompetences, etud: Identite refcomp: ApcReferentielCompetences, etud: Identite, parcour_id: int | None
) -> dict[int, ApcValidationRCUE]: ) -> dict[int, ApcValidationRCUE]:
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences. """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] = ( validations: list[ApcValidationRCUE] = (
ApcValidationRCUE.query.filter_by(etudid=etud.id) 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(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
.join(ApcCompetence) .join(ApcCompetence)
.filter_by(referentiel_id=refcomp.id) .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() .all()
) )
return { return {

View File

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

View File

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

View File

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

View File

@ -920,7 +920,7 @@ def jury_delete_manual(etudid: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def etud_bilan_ects(etudid: int): 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. Plusieurs formations (eg DUT, LP) peuvent être concernées.
""" """
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)