forked from ScoDoc/ScoDoc
Compare commits
10 Commits
44cb716154
...
c0b750dcfb
Author | SHA1 | Date | |
---|---|---|---|
c0b750dcfb | |||
44d56f2493 | |||
2af2ca6c43 | |||
41e065f6ab | |||
60c157222b | |||
735100de60 | |||
f7a42646bc | |||
d666483530 | |||
7712de19a2 | |||
c928ccdcfe |
@ -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
|
||||
@ -792,13 +796,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||
)
|
||||
if mark_recorded:
|
||||
self.recorded = True
|
||||
if mark_recorded:
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
return True
|
||||
|
||||
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,21 +934,27 @@ 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,
|
||||
)
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=self.formsemestre.formation.formation_code)
|
||||
)
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
|
||||
# Efface éventuelles validations de semestre
|
||||
# (en principe inutilisées en BUT)
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x ue)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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")}
|
||||
"""
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -79,17 +79,23 @@ 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>
|
||||
: <b>{self.code}</b>
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
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")}
|
||||
"""
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -6,4 +6,8 @@ div.jury_decisions_list div {
|
||||
|
||||
div.jury_decisions_list form {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
span.parcours {
|
||||
color:blueviolet;
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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">
|
||||
|
@ -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}} - {{"%g"|format(ects_by_sem[semestre_idx]) | safe}} ECTS
|
||||
semestre {{semestre_idx}} - {{ects_by_sem[semestre_idx] | safe}} ECTS
|
||||
</div>
|
||||
<div class="formation_list_ues_content">
|
||||
<ul class="apc_ue_list">
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.4.88"
|
||||
SCOVERSION = "9.4.89"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user