WIP: jury BUT: prise en compte des UE capitalisées dans les RCUEs

This commit is contained in:
Emmanuel Viennet 2022-11-24 00:11:59 +01:00
parent 06caacbb47
commit 386471a47f
10 changed files with 201 additions and 135 deletions

View File

@ -383,43 +383,47 @@ class DecisionsProposeesAnnee(DecisionsProposees):
#
def infos(self) -> str:
"informations, for debugging purpose"
return f"""<b>DecisionsProposeesAnnee</b>
"""informations, for debugging purpose."""
text = f"""<b>DecisionsProposeesAnnee</b>
<ul>
<li>Etudiant: <a href="{url_for("scolar.ficheEtud",
scodoc_dept=g.scodoc_dept, etudid=self.etud.id)
}">{self.etud.nomprenom}</a>
</li>
<li>formsemestre_impair: <a href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_impair.id)
}">{html.escape(str(self.formsemestre_impair))}</a>
<ul>
<li>Formation: <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
semestre_idx=self.formsemestre_impair.semestre_id,
formation_id=self.formsemestre_impair.formation.id)}">
{self.formsemestre_impair.formation.to_html()} ({self.formsemestre_impair.formation.id})</a>
</li>
</ul>
</li>
<li>formsemestre_pair: <a href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=self.formsemestre_pair.id)
}">{html.escape(str(self.formsemestre_pair))}</a>
<ul>
<li>Formation: <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
semestre_idx=self.formsemestre_pair.semestre_id,
formation_id=self.formsemestre_pair.formation.id)}">
{self.formsemestre_pair.formation.to_html()} ({self.formsemestre_pair.formation.id})</a>
</li>
</ul>
</li>
<li>RCUEs: {html.escape(str(self.rcues_annee))}</li>
<li>nb_competences: {getattr(self, "nb_competences", "-")}</li>
<li>nb_validables: {getattr(self, "nb_validables", "-")}</li>
<li>codes: {self.codes}</li>
<li>explanation: {self.explanation}</li>
</ul>
"""
for formsemestre, title in (
(self.formsemestre_impair, "formsemestre_impair"),
(self.formsemestre_pair, "formsemestre_pair"),
):
text += f"<li>{title}:"
if formsemestre is not None:
text += f"""
<a href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">{html.escape(str(formsemestre))}</a>
<ul>
<li>Formation: <a href="{url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept,
semestre_idx=formsemestre.semestre_id,
formation_id=formsemestre.formation.id)}">
{formsemestre.formation.to_html()} ({
formsemestre.formation.id})</a>
</li>
</ul>
"""
else:
text += " aucun."
text += "</li>"
text += f"""
<li>RCUEs: {html.escape(str(self.rcues_annee))}</li>
<li>nb_competences: {getattr(self, "nb_competences", "-")}</li>
<li>nb_validables: {getattr(self, "nb_validables", "-")}</li>
<li>codes: {self.codes}</li>
<li>explanation: {self.explanation}</li>
</ul>
"""
return text
def annee_scolaire(self) -> int:
"L'année de début de l'année scolaire"
@ -434,14 +438,19 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def comp_formsemestres(
self, formsemestre: FormSemestre
) -> tuple[FormSemestre, FormSemestre]:
"""les deux formsemestres de l'année scolaire à laquelle appartient formsemestre."""
"""Les deux formsemestres du niveau auquel appartient formsemestre.
Complète le niveau avec le formsemestre antérieur le plus récent.
L'"autre" formsemestre peut ainsi appartenir à l'année scolaire
antérieure (redoublants).
-> S_impair, S_pair
"""
if not formsemestre.formation.is_apc(): # garde fou
return None, None
if formsemestre.semestre_id % 2 == 0:
other_semestre_id = formsemestre.semestre_id - 1
else:
other_semestre_id = formsemestre.semestre_id + 1
annee_scolaire = formsemestre.annee_scolaire()
other_formsemestre = None
for inscr in self.etud.formsemestre_inscriptions:
if (
@ -452,8 +461,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
)
# L'autre semestre
and (inscr.formsemestre.semestre_id == other_semestre_id)
# de la même année scolaire:
and (inscr.formsemestre.annee_scolaire() == annee_scolaire)
# Antérieur
and inscr.formsemestre.date_debut < formsemestre.date_debut
# Et plus le récent possible
and (
(other_formsemestre is None)
or (other_formsemestre.date_debut < inscr.formsemestre.date_debut)
)
):
other_formsemestre = inscr.formsemestre
if formsemestre.semestre_id % 2 == 0:
@ -512,9 +526,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
rcue = RegroupementCoherentUE(
self.etud,
self.formsemestre_impair,
ue_impair,
self.decisions_ues[ue_impair.id],
self.formsemestre_pair,
ue_pair,
self.decisions_ues[ue_pair.id],
self.inscription_etat,
)
ues_impair_sans_rcue.discard(ue_impair.id)
@ -976,18 +990,23 @@ class DecisionsProposeesUE(DecisionsProposees):
self.codes = [
sco_codes.DEM if inscription_etat == scu.DEMISSION else sco_codes.DEF
]
self.moy_ue = np.NaN
self.moy_ue = self.moy_ue_with_cap = np.NaN
self.ue_status = {}
return
# Moyenne de l'UE ?
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre)
# Safety checks:
if not ue.id in res.etud_moy_ue:
self.explanation = "UE sans résultat"
return
if not etud.id in res.etud_moy_ue[ue.id]:
self.explanation = "Étudiant sans résultat dans cette UE"
return
self.moy_ue = res.etud_moy_ue[ue.id][etud.id]
ue_status = res.get_etud_ue_status(etud.id, ue.id)
self.moy_ue = ue_status["cur_moy_ue"]
self.moy_ue_with_cap = ue_status["moy"]
self.ue_status = ue_status
def set_rcue(self, rcue: RegroupementCoherentUE):
"""Rattache cette UE à un RCUE. Cela peut modifier les codes

View File

@ -58,15 +58,15 @@ def formsemestre_saisie_jury_but(
# DecisionsProposeesAnnee(etud, formsemestre2)
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
if formsemestre2.semestre_id % 2 != 0:
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
# XXX if formsemestre2.semestre_id % 2 != 0:
# raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
if formsemestre2.formation.referentiel_competence is None:
raise ScoValueError(
"""
<p>Pas de référentiel de compétences associé à la formation !</p>
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
de compétences"</em>
"""
)
@ -262,7 +262,7 @@ class RowCollector:
# --- Codes (seront cachés, mais exportés en excel)
self.add_cell("etudid", "etudid", etud.id, "codes")
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
# --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO)
# --- Identité étudiant (adapté de res_common/get_table_recap, à factoriser XXX TODO)
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
self["_nom_disp_order"] = etud.sort_key

View File

@ -82,8 +82,6 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_1,
deca.decisions_ues[dec_rcue.rcue.ue_1.id].moy_ue,
# dec_rcue.rcue.moy_ue_1,
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
disabled=read_only,
)
@ -92,8 +90,6 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
H.append(
_gen_but_niveau_ue(
dec_rcue.rcue.ue_2,
deca.decisions_ues[dec_rcue.rcue.ue_2.id].moy_ue,
# dec_rcue.rcue.moy_ue_2,
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
disabled=read_only,
)
@ -134,7 +130,7 @@ def _gen_but_select(
for code in codes
]
)
return f"""<select required name="{name}"
return f"""<select required name="{name}"
class="but_code {klass}"
onchange="change_menu_code(this);"
{"disabled" if disabled else ""}
@ -142,20 +138,40 @@ def _gen_but_select(
"""
def _gen_but_niveau_ue(
ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False
):
def _gen_but_niveau_ue(ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled=False):
if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]:
moy_ue_str = f"""<span class="ue_cap">{
scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>"""
scoplement = f"""<div class="scoplement">
<div>
<b>UE {ue.acronyme} capitalisée le
{dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")}
</b>
</div>
<div>UE en cours avec moyenne
{scu.fmt_note(dec_ue.moy_ue)}
</div>
</div>
"""
else:
moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>"""
scoplement = ""
return f"""<div class="but_niveau_ue {
'recorded' if dec_ue.code_valide is not None else ''}
">
<div title="{ue.titre}">{ue.acronyme}</div>
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
<div class="but_note with_scoplement">
<div>{moy_ue_str}</div>
{scoplement}
</div>
<div class="but_code">{
_gen_but_select("code_ue_"+str(ue.id),
dec_ue.codes,
dec_ue.code_valide, disabled=disabled
)
}</div>
</div>"""
@ -295,7 +311,6 @@ def jury_but_semestriel(
H.append(
_gen_but_niveau_ue(
ue,
dec_ue.moy_ue,
dec_ue,
disabled=read_only,
)

View File

@ -316,7 +316,7 @@ class ResultatsSemestre(ResultatsCache):
"""L'état de l'UE pour cet étudiant.
Result: dict, ou None si l'UE n'est pas dans ce semestre.
"""
ue = UniteEns.query.get(ue_id) # TODO cacher nos UEs ?
ue = UniteEns.query.get(ue_id)
if ue.type == UE_SPORT:
return {
"is_capitalized": False,

View File

@ -84,28 +84,28 @@ class RegroupementCoherentUE:
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
La moyenne (10/20) au RCU déclenche la compensation des UE.
La moyenne (10/20) au RCUE déclenche la compensation des UE.
"""
def __init__(
self,
etud: Identite,
formsemestre_1: FormSemestre,
ue_1: UniteEns,
dec_ue_1: "DecisionsProposeesUE",
formsemestre_2: FormSemestre,
ue_2: UniteEns,
dec_ue_2: "DecisionsProposeesUE",
inscription_etat: str,
):
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
# from app.but.jury_but import DecisionsProposeesUE
ue_1 = dec_ue_1.ue
ue_2 = dec_ue_2.ue
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
(
ue_2,
formsemestre_2,
),
(ue_2, formsemestre_2),
(ue_1, formsemestre_1),
)
assert formsemestre_1.semestre_id % 2 == 1
@ -125,21 +125,12 @@ class RegroupementCoherentUE:
self.moy_ue_1 = self.moy_ue_2 = "-"
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
return
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
else:
self.moy_ue_1 = None
self.moy_ue_1_val = 0.0
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
self.moy_ue_2_val = self.moy_ue_2
else:
self.moy_ue_2 = None
self.moy_ue_2_val = 0.0
# Calcul de la moyenne au RCUE
self.moy_ue_1 = dec_ue_1.moy_ue_with_cap
self.moy_ue_1_val = self.moy_ue_1 if self.moy_ue_1 is not None else 0.0
self.moy_ue_2 = dec_ue_2.moy_ue_with_cap
self.moy_ue_2_val = self.moy_ue_2 if self.moy_ue_2 is not None else 0.0
# Calcul de la moyenne au RCUE (utilise les moy d'UE capitalisées)
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
# Moyenne RCUE (les pondérations par défaut sont 1.)
self.moy_rcue = (
@ -149,7 +140,9 @@ class RegroupementCoherentUE:
self.moy_rcue = None
def __repr__(self) -> str:
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
return f"""<{self.__class__.__name__} {
self.ue_1.acronyme}({self.moy_ue_1}) {
self.ue_2.acronyme}({self.moy_ue_2})>"""
def query_validations(
self,
@ -218,62 +211,62 @@ class RegroupementCoherentUE:
# 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.
# 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.
# 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 []
# 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
# 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
# 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.query.get(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
# 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.query.get(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):

View File

@ -150,7 +150,7 @@ class FormSemestre(db.Model):
self.modalite = FormationModalite.DEFAULT_MODALITE
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
def to_dict(self, convert_objects=False) -> dict:
"""dict (compatible ScoDoc7).

View File

@ -25,7 +25,7 @@
#
##############################################################################
"""Feuille excel pour preparation des jurys
"""Feuille excel pour préparation des jurys classiques (non BUT)
"""
import time
@ -51,7 +51,9 @@ import sco_version
def feuille_preparation_jury(formsemestre_id):
"Feuille excel pour preparation des jurys"
"""Feuille excel pour préparation des jurys classiques.
Non adaptée pour le BUT.
"""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
etuds: Identite = nt.get_inscrits(order_by="moy") # tri par moy gen

View File

@ -52,6 +52,7 @@
.but_note {
border-right: 1px solid #aaa;
padding: 8px;
position: relative;
}
.but_annee select {

View File

@ -12,7 +12,6 @@ body {
font-size: 12pt;
}
@media print {
.noprint {
display: none;
@ -3168,7 +3167,8 @@ td.ue_cmp {
color: green;
}
td.ue_capitalized {
td.ue_capitalized,
.ue_cap {
text-decoration: underline;
}
@ -3330,6 +3330,42 @@ div.module_check_absences ul {
/* ----------------------------------------------- */
.scoplement {
pointer-events: none;
position: absolute;
bottom: 100%;
left: 0;
padding: 8px;
border-radius: 4px;
background: rgb(205, 228, 255);
color: #000;
border: 1px solid rgb(4, 16, 159);
opacity: 0;
display: grid !important;
grid-template-columns: 1fr;
gap: 0 !important;
column-gap: 4px !important;
}
.with_scoplement:hover .scoplement {
opacity: 1;
z-index: 1;
}
.scoplement>div {
text-align: left;
display: inline-block;
white-space: nowrap;
}
.scoplement>div:nth-child(1),
.scoplement>div:nth-child(7) {
margin-bottom: 8px;
}
/* ----------------------------------------------- */
/* ----------------------------- */
/* TABLES generees par gen_table */
/* ----------------------------- */

View File

@ -2658,7 +2658,7 @@ def formsemestre_saisie_jury(formsemestre_id: int, selected_etudid: int = None):
"""
read_only = not sco_permissions_check.can_validate_sem(formsemestre_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
if formsemestre.formation.is_apc(): # and formsemestre.semestre_id % 2 == 0:
return jury_but_recap.formsemestre_saisie_jury_but(
formsemestre, read_only, selected_etudid=selected_etudid
)