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.DEM,
sco_codes.EXCLU,
sco_codes.NAR,
]
def __init__(
@ -256,6 +257,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.annee_but = (formsemestre_last.semestre_id + 1) // 2
"le rang de l'année dans le BUT: 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 = []
"""RCUEs de l'année
(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>"
)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
# WIP TODO XXX def get_moyenne_annuelle(self)
@ -749,7 +753,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
dec_ue.record(code)
for dec_rcue, code in codes_rcues:
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_all()
self.recorded = True
@ -799,6 +803,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
def record_autorisation_inscription(self, code: str):
"""Autorisation d'inscription dans semestre suivant"""
if self.autorisations_recorded:
return
if self.inscription_etat != scu.INSCRIT:
# les dem et DEF ne continuent jamais
return
@ -813,6 +819,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.formsemestre.id,
next_semestre_id,
)
self.autorisations_recorded = True
def invalidate_formsemestre_cache(self):
"invalide le résultats des deux formsemestres"
@ -902,6 +909,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
Efface même si étudiant DEM ou DEF.
Si à cheval ou only_one_sem, n'efface que les décisions UE et les
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.)
"""
if only_one_sem or self.a_cheval:
@ -916,8 +925,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
else:
for dec_ue in self.decisions_ues.values():
dec_ue.erase()
for dec_rcue in self.decisions_rcue_by_niveau.values():
dec_rcue.erase()
if self.formsemestre_impair:
ScolarAutorisationInscription.delete_autorisation_etud(
self.etud.id, self.formsemestre_impair.id
@ -926,14 +934,20 @@ class DecisionsProposeesAnnee(DecisionsProposees):
ScolarAutorisationInscription.delete_autorisation_etud(
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,
# 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,
)
.join(Formation)
.filter_by(formation_code=self.formsemestre.formation.formation_code)
)
for validation in validations:
db.session.delete(validation)
Scolog.logdb(
@ -1113,6 +1127,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
self.codes.insert(0, self.code_valide)
else:
self.codes.insert(1, self.code_valide)
self.codes = [self.codes[0]] + sorted(self.codes[1:])
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} rcue={self.rcue} valid={self.code_valide
@ -1286,7 +1301,7 @@ class DecisionsProposeesRCUE(DecisionsProposees):
sco_cache.invalidate_formsemestre(
formsemestre_id=validation_rcue.formsemestre_id
)
else:
elif ue1 and ue2:
# Crée nouvelle validation
validation_rcue = ApcValidationRCUE(
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'",
"warning",
)
return
return [], None, None
ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
if len(ues_impaires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
)
return
return [], None, None
ue1 = ues_impaires[0]
ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
if len(ues_paires) != 1:
flash(
"Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
)
return
return [], None, None
ue2 = ues_paires[0]
return ues, ue1, ue2
@ -1476,6 +1491,7 @@ class DecisionsProposeesUE(DecisionsProposees):
self.moy_ue = ue_status["cur_moy_ue"]
self.moy_ue_with_cap = ue_status["moy"]
self.ue_status = ue_status
self.codes = [self.codes[0]] + sorted(self.codes[1:])
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} ue={self.ue.acronyme} valid={self.code_valide

View File

@ -9,16 +9,15 @@
Non spécifique au BUT.
"""
import flask
from flask import flash, render_template, url_for
from flask import flash, render_template
from flask import g, request
import sqlalchemy as sa
from app import db
from app.models import (
ApcValidationAnnee,
ApcValidationRCUE,
FormSemestre,
Identite,
UniteEns,
ScolarAutorisationInscription,
@ -38,7 +37,12 @@ def jury_delete_manual(etud: Identite):
ue_vals = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.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(
etudid=etud.id

View File

@ -322,7 +322,13 @@ class ModuleImplResultsAPC(ModuleImplResults):
modimpl = ModuleImpl.query.get(self.moduleimpl_id)
nb_etuds, nb_evals = self.evals_notes.shape
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:
return pd.DataFrame(index=[], columns=evals_poids_df.columns)
if nb_ues == 0:

View File

@ -30,6 +30,7 @@
import numpy as np
import pandas as pd
import app
from app import db
from app import models
from app.models import (
@ -167,8 +168,14 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
"""
assert len(modimpls_notes)
modimpls_notes_arr = [df.values for df in modimpls_notes]
try:
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)

View File

@ -9,6 +9,7 @@ from datetime import datetime
import functools
from operator import attrgetter
from flask import g
from flask_sqlalchemy.query import Query
from sqlalchemy.orm import class_mapper
import sqlalchemy
@ -412,6 +413,20 @@ class ApcNiveau(db.Model, XMLModel):
(dans ce cas, spécifier referentiel_competence)
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}:
raise ValueError("annee invalide pour un parcours BUT")
referentiel_competence = (
@ -428,10 +443,13 @@ class ApcNiveau(db.Model, XMLModel):
)
if competence is not None:
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()
if not annee_parcour:
_cache[key] = []
return []
if competence is None:
@ -446,6 +464,7 @@ class ApcNiveau(db.Model, XMLModel):
niveaux: list[ApcNiveau] = competence.niveaux.filter_by(
annee=f"BUT{int(annee)}"
).all()
_cache[key] = niveaux
return niveaux

View File

@ -352,6 +352,7 @@ class ApcValidationAnnee(db.Model):
"Affichage html"
return f"""Validation <b>année BUT{self.ordre}</b> émise par
{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")}
"""

View File

@ -1,6 +1,7 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE)
"""
from flask import g
import pandas as pd
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.but_refcomp import ApcNiveau, ApcParcours
from app.models.modules import Module
from app.scodoc.sco_exceptions import ScoFormationConflict
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
(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.pop("_sa_instance_state", None)
e.pop("evaluation_ue_poids", None)
@ -133,6 +144,7 @@ class UniteEns(db.Model):
]
else:
e.pop("module_ue_coefs", None)
_cache[key] = e
return e
def annee(self) -> int:
@ -180,12 +192,23 @@ class UniteEns(db.Model):
le parcours indiqué.
"""
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_id=self.id, parcours_id=parcour.id
).first()
if ue_parcour is not None and ue_parcour.ects is not None:
ue_ects_cache[key] = ue_parcour.ects
return ue_parcour.ects
if only_parcours:
ue_ects_cache[key] = None
return None
return self.ects

View File

@ -79,7 +79,12 @@ class ScolarFormSemestreValidation(db.Model):
def html(self, detail=False) -> str:
"Affichage html"
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())
if self.formsemestre else ""}
: <b>{self.code}</b>
@ -88,8 +93,9 @@ class ScolarFormSemestreValidation(db.Model):
else:
return f"""Validation du semestre S{
self.formsemestre.semestre_id if self.formsemestre else "?"}
(<b>{self.code}</b>
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")})
{self.formsemestre.html_link_status() if self.formsemestre else ""}
: <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:
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
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()

View File

@ -7,3 +7,7 @@ div.jury_decisions_list div {
div.jury_decisions_list form {
display: inline-block;
}
span.parcours {
color:blueviolet;
}

View File

@ -551,6 +551,7 @@ class RowRecap(tb.Row):
"etud_codes": "Codes",
"identite_detail": "",
"identite_court": "",
"rang": "",
}
)
# --- 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>,
notamment sur les UEs en dessous de 10.
</p>
<p class="warning">
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
</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 class="row">

View File

@ -2,7 +2,7 @@
{% for semestre_idx in semestre_ids %}
<div class="formation_list_ues">
<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 class="formation_list_ues_content">
<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,
etudid=deca.etud.id)}" class="stdlink"
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"
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>
<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,
)

View File

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