Compare commits

..

10 Commits

15 changed files with 139 additions and 43 deletions

View File

@ -206,6 +206,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
sco_codes.DEF, sco_codes.DEF,
sco_codes.DEM, sco_codes.DEM,
sco_codes.EXCLU, sco_codes.EXCLU,
sco_codes.NAR,
] ]
def __init__( def __init__(
@ -256,6 +257,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.annee_but = (formsemestre_last.semestre_id + 1) // 2 self.annee_but = (formsemestre_last.semestre_id + 1) // 2
"le rang de l'année dans le BUT: 1, 2, 3" "le rang de l'année dans le BUT: 1, 2, 3"
assert self.annee_but in (1, 2, 3) assert self.annee_but in (1, 2, 3)
self.autorisations_recorded = False
"vrai si on a enregistré l'autorisation de passage"
self.rcues_annee = [] self.rcues_annee = []
"""RCUEs de l'année """RCUEs de l'année
(peuvent concerner l'année scolaire antérieur pour les redoublants (peuvent concerner l'année scolaire antérieur pour les redoublants
@ -444,6 +447,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
+ '</div><div class="warning">'.join(messages) + '</div><div class="warning">'.join(messages)
+ "</div>" + "</div>"
) )
self.codes = [self.codes[0]] + sorted(self.codes[1:])
# WIP TODO XXX def get_moyenne_annuelle(self) # WIP TODO XXX def get_moyenne_annuelle(self)
@ -749,7 +753,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.record(code) dec_ue.record(code)
for dec_rcue, code in codes_rcues: for dec_rcue, code in codes_rcues:
dec_rcue.record(code) dec_rcue.record(code)
self.record(code_annee, mark_recorded=False) self.record(code_annee) # XXX , mark_recorded=False)
self.record_autorisation_inscription(code_annee) self.record_autorisation_inscription(code_annee)
self.record_all() self.record_all()
self.recorded = True self.recorded = True
@ -792,13 +796,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
etudid=self.etud.id, etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: {code}", msg=f"Validation année BUT{self.annee_but}: {code}",
) )
if mark_recorded: if mark_recorded:
self.recorded = True self.recorded = True
self.invalidate_formsemestre_cache() self.invalidate_formsemestre_cache()
return True return True
def record_autorisation_inscription(self, code: str): def record_autorisation_inscription(self, code: str):
"""Autorisation d'inscription dans semestre suivant""" """Autorisation d'inscription dans semestre suivant"""
if self.autorisations_recorded:
return
if self.inscription_etat != scu.INSCRIT: if self.inscription_etat != scu.INSCRIT:
# les dem et DEF ne continuent jamais # les dem et DEF ne continuent jamais
return return
@ -813,6 +819,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.formsemestre.id, self.formsemestre.id,
next_semestre_id, next_semestre_id,
) )
self.autorisations_recorded = True
def invalidate_formsemestre_cache(self): def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres" "invalide le résultats des deux formsemestres"
@ -902,6 +909,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
Efface même si étudiant DEM ou DEF. Efface même si étudiant DEM ou DEF.
Si à cheval ou only_one_sem, n'efface que les décisions UE et les Si à cheval ou only_one_sem, n'efface que les décisions UE et les
autorisations de passage du semestre d'origine du deca. autorisations de passage du semestre d'origine du deca.
Dans tous les cas, efface les validations de l'année en cours.
(commite la session.) (commite la session.)
""" """
if only_one_sem or self.a_cheval: if only_one_sem or self.a_cheval:
@ -916,8 +925,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
else: else:
for dec_ue in self.decisions_ues.values(): for dec_ue in self.decisions_ues.values():
dec_ue.erase() dec_ue.erase()
for dec_rcue in self.decisions_rcue_by_niveau.values():
dec_rcue.erase()
if self.formsemestre_impair: if self.formsemestre_impair:
ScolarAutorisationInscription.delete_autorisation_etud( ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_impair.id self.etud.id, self.formsemestre_impair.id
@ -926,21 +934,27 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ScolarAutorisationInscription.delete_autorisation_etud( ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_pair.id self.etud.id, self.formsemestre_pair.id
) )
validations = ApcValidationAnnee.query.filter_by( # Efface les RCUEs
for dec_rcue in self.decisions_rcue_by_niveau.values():
dec_rcue.erase()
# Efface les validations concernant l'année BUT
# de ce semestre
validations = (
ApcValidationAnnee.query.filter_by(
etudid=self.etud.id, etudid=self.etud.id,
# XXX efface les validations émise depuis ce semestre
# et pas toutes celles concernant cette l'année...
# (utiliser formation_id pour changer cette politique)
formsemestre_id=self.formsemestre_impair.id,
ordre=self.annee_but, ordre=self.annee_but,
) )
for validation in validations: .join(Formation)
db.session.delete(validation) .filter_by(formation_code=self.formsemestre.formation.formation_code)
Scolog.logdb( )
"jury_but", for validation in validations:
etudid=self.etud.id, db.session.delete(validation)
msg=f"Validation année BUT{self.annee_but}: effacée", Scolog.logdb(
) "jury_but",
etudid=self.etud.id,
msg=f"Validation année BUT{self.annee_but}: effacée",
)
# Efface éventuelles validations de semestre # Efface éventuelles validations de semestre
# (en principe inutilisées en BUT) # (en principe inutilisées en BUT)
@ -1113,6 +1127,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self.codes.insert(0, self.code_valide) self.codes.insert(0, self.code_valide)
else: else:
self.codes.insert(1, self.code_valide) self.codes.insert(1, self.code_valide)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
@ -1286,7 +1301,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=validation_rcue.formsemestre_id formsemestre_id=validation_rcue.formsemestre_id
) )
else: elif ue1 and ue2:
# Crée nouvelle validation # Crée nouvelle validation
validation_rcue = ApcValidationRCUE( validation_rcue = ApcValidationRCUE(
etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
@ -1380,20 +1395,20 @@ class DecisionsProposeesRCUE(DecisionsProposees):
"Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'", "Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
"warning", "warning",
) )
return return [], None, None
ues_impaires = [ue for ue in ues if ue.semestre_idx % 2] ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
if len(ues_impaires) != 1: if len(ues_impaires) != 1:
flash( flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée" "Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
) )
return return [], None, None
ue1 = ues_impaires[0] ue1 = ues_impaires[0]
ues_paires = [ue for ue in ues if not ue.semestre_idx % 2] ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
if len(ues_paires) != 1: if len(ues_paires) != 1:
flash( flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée" "Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
) )
return return [], None, None
ue2 = ues_paires[0] ue2 = ues_paires[0]
return ues, ue1, ue2 return ues, ue1, ue2
@ -1476,6 +1491,7 @@ class DecisionsProposeesUE(DecisionsProposees):
self.moy_ue = ue_status["cur_moy_ue"] self.moy_ue = ue_status["cur_moy_ue"]
self.moy_ue_with_cap = ue_status["moy"] self.moy_ue_with_cap = ue_status["moy"]
self.ue_status = ue_status self.ue_status = ue_status
self.codes = [self.codes[0]] + sorted(self.codes[1:])
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide

View File

@ -9,16 +9,15 @@
Non spécifique au BUT. Non spécifique au BUT.
""" """
import flask from flask import flash, render_template
from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
import sqlalchemy as sa
from app import db from app import db
from app.models import ( from app.models import (
ApcValidationAnnee, ApcValidationAnnee,
ApcValidationRCUE, ApcValidationRCUE,
FormSemestre,
Identite, Identite,
UniteEns, UniteEns,
ScolarAutorisationInscription, ScolarAutorisationInscription,
@ -38,7 +37,12 @@ def jury_delete_manual(etud: Identite):
ue_vals = ( ue_vals = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.join(UniteEns) .join(UniteEns)
.order_by(ScolarFormSemestreValidation.event_date, UniteEns.numero) .order_by(
sa.extract("year", ScolarFormSemestreValidation.event_date),
UniteEns.semestre_idx,
UniteEns.numero,
UniteEns.acronyme,
)
) )
autorisations = ScolarAutorisationInscription.query.filter_by( autorisations = ScolarAutorisationInscription.query.filter_by(
etudid=etud.id etudid=etud.id

View File

@ -322,7 +322,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
modimpl = ModuleImpl.query.get(self.moduleimpl_id) modimpl = ModuleImpl.query.get(self.moduleimpl_id)
nb_etuds, nb_evals = self.evals_notes.shape nb_etuds, nb_evals = self.evals_notes.shape
nb_ues = evals_poids_df.shape[1] nb_ues = evals_poids_df.shape[1]
assert evals_poids_df.shape[0] == nb_evals # compat notes/poids if evals_poids_df.shape[0] != nb_evals:
# compat notes/poids: race condition ?
app.critical_error(
f"""compute_module_moy: evals_poids_df.shape[0] != nb_evals ({
evals_poids_df.shape[0]} != {nb_evals})
"""
)
if nb_etuds == 0: if nb_etuds == 0:
return pd.DataFrame(index=[], columns=evals_poids_df.columns) return pd.DataFrame(index=[], columns=evals_poids_df.columns)
if nb_ues == 0: if nb_ues == 0:

View File

@ -30,6 +30,7 @@
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import app
from app import db from app import db
from app import models from app import models
from app.models import ( from app.models import (
@ -167,8 +168,14 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
""" """
assert len(modimpls_notes) assert len(modimpls_notes)
modimpls_notes_arr = [df.values for df in modimpls_notes] modimpls_notes_arr = [df.values for df in modimpls_notes]
modimpls_notes = np.stack(modimpls_notes_arr) try:
# passe de (mod x etud x ue) à (etud x mod x ue) modimpls_notes = np.stack(modimpls_notes_arr)
# passe de (mod x etud x ue) à (etud x mod x ue)
except ValueError:
app.critical_error(
f"""notes_sem_assemble_cube: shapes {
", ".join([x.shape for x in modimpls_notes_arr])}"""
)
return modimpls_notes.swapaxes(0, 1) return modimpls_notes.swapaxes(0, 1)

View File

@ -9,6 +9,7 @@ from datetime import datetime
import functools import functools
from operator import attrgetter from operator import attrgetter
from flask import g
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper from sqlalchemy.orm import class_mapper
import sqlalchemy import sqlalchemy
@ -412,6 +413,20 @@ class ApcNiveau(db.Model, XMLModel):
(dans ce cas, spécifier referentiel_competence) (dans ce cas, spécifier referentiel_competence)
Si competence est indiquée, filtre les niveaux de cette compétence. Si competence est indiquée, filtre les niveaux de cette compétence.
""" """
key = (
parcour.id if parcour else None,
annee,
referentiel_competence.id if referentiel_competence else None,
competence.id if competence else None,
)
_cache = getattr(g, "_niveaux_annee_de_parcours_cache", None)
if _cache:
result = g._niveaux_annee_de_parcours_cache.get(key, False)
if result is not False:
return result
else:
g._niveaux_annee_de_parcours_cache = {}
_cache = g._niveaux_annee_de_parcours_cache
if annee not in {1, 2, 3}: if annee not in {1, 2, 3}:
raise ValueError("annee invalide pour un parcours BUT") raise ValueError("annee invalide pour un parcours BUT")
referentiel_competence = ( referentiel_competence = (
@ -428,10 +443,13 @@ class ApcNiveau(db.Model, XMLModel):
) )
if competence is not None: if competence is not None:
query = query.filter(ApcCompetence.id == competence.id) query = query.filter(ApcCompetence.id == competence.id)
return query.all() result = query.all()
_cache[key] = result
return result
annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first() annee_parcour: ApcAnneeParcours = parcour.annees.filter_by(ordre=annee).first()
if not annee_parcour: if not annee_parcour:
_cache[key] = []
return [] return []
if competence is None: if competence is None:
@ -446,6 +464,7 @@ class ApcNiveau(db.Model, XMLModel):
niveaux: list[ApcNiveau] = competence.niveaux.filter_by( niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
annee=f"BUT{int(annee)}" annee=f"BUT{int(annee)}"
).all() ).all()
_cache[key] = niveaux
return niveaux return niveaux

View File

@ -352,6 +352,7 @@ class ApcValidationAnnee(db.Model):
"Affichage html" "Affichage html"
return f"""Validation <b>année BUT{self.ordre}</b> émise par return f"""Validation <b>année BUT{self.ordre}</b> émise par
{self.formsemestre.html_link_status() if self.formsemestre else "-"} {self.formsemestre.html_link_status() if self.formsemestre else "-"}
: <b>{self.code}</b>
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")} le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
""" """

View File

@ -1,6 +1,7 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE) """ScoDoc 9 models : Unités d'Enseignement (UE)
""" """
from flask import g
import pandas as pd import pandas as pd
from app import db, log from app import db, log
@ -8,7 +9,6 @@ from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.but_refcomp import ApcNiveau, ApcParcours from app.models.but_refcomp import ApcNiveau, ApcParcours
from app.models.modules import Module from app.models.modules import Module
from app.scodoc.sco_exceptions import ScoFormationConflict
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -107,6 +107,17 @@ class UniteEns(db.Model):
If convert_objects, convert all attributes to native types If convert_objects, convert all attributes to native types
(suitable for json encoding). (suitable for json encoding).
""" """
# cache car très utilisé par anciens codes
key = (self.id, convert_objects, with_module_ue_coefs)
_cache = getattr(g, "_ue_to_dict_cache", None)
if _cache:
result = g._ue_to_dict_cache.get(key, False)
if result is not False:
return result
else:
g._ue_to_dict_cache = {}
_cache = g._ue_to_dict_cache
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
e.pop("evaluation_ue_poids", None) e.pop("evaluation_ue_poids", None)
@ -133,6 +144,7 @@ class UniteEns(db.Model):
] ]
else: else:
e.pop("module_ue_coefs", None) e.pop("module_ue_coefs", None)
_cache[key] = e
return e return e
def annee(self) -> int: def annee(self) -> int:
@ -180,12 +192,23 @@ class UniteEns(db.Model):
le parcours indiqué. le parcours indiqué.
""" """
if parcour is not None: if parcour is not None:
key = (parcour.id, self.id, only_parcours)
ue_ects_cache = getattr(g, "_ue_ects_cache", None)
if ue_ects_cache:
ects = g._ue_ects_cache.get(key, False)
if ects is not False:
return ects
else:
g._ue_ects_cache = {}
ue_ects_cache = g._ue_ects_cache
ue_parcour = UEParcours.query.filter_by( ue_parcour = UEParcours.query.filter_by(
ue_id=self.id, parcours_id=parcour.id ue_id=self.id, parcours_id=parcour.id
).first() ).first()
if ue_parcour is not None and ue_parcour.ects is not None: if ue_parcour is not None and ue_parcour.ects is not None:
ue_ects_cache[key] = ue_parcour.ects
return ue_parcour.ects return ue_parcour.ects
if only_parcours: if only_parcours:
ue_ects_cache[key] = None
return None return None
return self.ects return self.ects

View File

@ -79,17 +79,23 @@ class ScolarFormSemestreValidation(db.Model):
def html(self, detail=False) -> str: def html(self, detail=False) -> str:
"Affichage html" "Affichage html"
if self.ue_id is not None: if self.ue_id is not None:
return f"""Validation de l'UE {self.ue.acronyme} de {self.ue.formation.acronyme} return f"""Validation de l'UE <b>{self.ue.acronyme}</b>
{('parcours <span class="parcours">'
+ ", ".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()) {("émise par " + self.formsemestre.html_link_status())
if self.formsemestre else ""} if self.formsemestre else ""}
:<b>{self.code}</b> : <b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")} le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
""" """
else: else:
return f"""Validation du semestre S{ return f"""Validation du semestre S{
self.formsemestre.semestre_id if self.formsemestre else "?"} self.formsemestre.semestre_id if self.formsemestre else "?"}
(<b>{self.code}</b> {self.formsemestre.html_link_status() if self.formsemestre else ""}
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}) : <b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
""" """

View File

@ -82,7 +82,7 @@ def html_edit_formation_apc(
if None in ects: if None in ects:
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>' ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
else: else:
ects_by_sem[semestre_idx] = sum(ects) ects_by_sem[semestre_idx] = f"{sum(ects):g}"
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags() arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()

View File

@ -6,4 +6,8 @@ div.jury_decisions_list div {
div.jury_decisions_list form { div.jury_decisions_list form {
display: inline-block; display: inline-block;
} }
span.parcours {
color:blueviolet;
}

View File

@ -551,6 +551,7 @@ class RowRecap(tb.Row):
"etud_codes": "Codes", "etud_codes": "Codes",
"identite_detail": "", "identite_detail": "",
"identite_court": "", "identite_court": "",
"rang": "",
} }
) )
# --- Codes (seront cachés, mais exportés en excel) # --- Codes (seront cachés, mais exportés en excel)

View File

@ -26,9 +26,13 @@
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>, En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
notamment sur les UEs en dessous de 10. notamment sur les UEs en dessous de 10.
</p> </p>
<p class="warning"> <div class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure ! <ul>
</p> <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 class="row"> <div class="row">

View File

@ -2,7 +2,7 @@
{% for semestre_idx in semestre_ids %} {% for semestre_idx in semestre_ids %}
<div class="formation_list_ues"> <div class="formation_list_ues">
<div class="formation_list_ues_titre">Unités d'Enseignement <div class="formation_list_ues_titre">Unités d'Enseignement
semestre {{semestre_idx}} &nbsp;-&nbsp; {{"%g"|format(ects_by_sem[semestre_idx]) | safe}} ECTS semestre {{semestre_idx}} &nbsp;-&nbsp; {{ects_by_sem[semestre_idx] | safe}} ECTS
</div> </div>
<div class="formation_list_ues_content"> <div class="formation_list_ues_content">
<ul class="apc_ue_list"> <ul class="apc_ue_list">

View File

@ -2497,7 +2497,7 @@ def formsemestre_validation_but(
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id, scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
etudid=deca.etud.id)}" class="stdlink" etudid=deca.etud.id)}" class="stdlink"
title="efface décisions issues des jurys de cette année" title="efface décisions issues des jurys de cette année"
>effacer décisions</a> >effacer décisions de ce jury</a>
<a style="margin-left: 16px;" class="stdlink" <a style="margin-left: 16px;" class="stdlink"
href="{ href="{
@ -2898,7 +2898,12 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
) )
+ """ + """
<p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p> <p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
<div class="warning">Cette opération est irréversible !</div> <p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
même si elles ont été acquises ailleurs.
</p>
<div class="warning">Cette opération est irréversible !
A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
</div>
""", """,
cancel_url=dest_url, cancel_url=dest_url,
) )

View File

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