Prise en compte UE capitalisées lorsque non inscrit dans le sem. courant. Affichage sur bulletins classiques. Capitalisation en BUT avec ECTS.

This commit is contained in:
Emmanuel Viennet 2022-02-27 20:12:20 +01:00
parent 6b8410e43b
commit 29b5d54d22
10 changed files with 108 additions and 41 deletions

View File

@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np
import pandas as pd
from flask import g, url_for
from app import log
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models import FormSemestre, FormSemestreUECoef
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -206,12 +210,18 @@ class ResultatsSemestre(ResultatsCache):
0.0, min(self.etud_moy_gen[etudid], 20.0)
)
def _get_etud_ue_cap(self, etudid, ue):
""""""
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
Résultat:
Si pas capitalisée: None
Si capitalisée: un dict, avec les colonnes de validation.
"""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
if ue_cap.empty:
return None
if isinstance(ue_cap, pd.DataFrame):
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
@ -219,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
return None
# converti la Series en dict, afin que les np.int64 reviennent en int
return ue_cap.to_dict()
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
@ -253,17 +264,41 @@ class ResultatsSemestre(ResultatsCache):
)
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if (
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
# Coef l'UE dans le semestre courant:
if self.is_apc:
# utilise les ECTS comme coef.
coef_ue = ue.ects
else:
# formations classiques
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
if self.is_apc:
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
coef_ue = ue_capitalized.ects
if coef_ue is None:
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
raise ScoValueError(
f"""L'UE capitalisée {ue_capitalized.acronyme}
du semestre {orig_sem.titre_annee()}
n'a pas d'indication d'ECTS.
Corrigez ou faite corriger le programme
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
"""
)
else:
# Coefs de l'UE capitalisée en formation classique:
# va chercher le coef dans le semestre d'origine
coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue(
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
)
return {
"is_capitalized": is_capitalized,

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls
"""
import pandas as pd
import flask_sqlalchemy
from app import db
from app.comp import df_cache
@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
)
@classmethod
def nb_inscriptions_dans_ue(
def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
) -> flask_sqlalchemy.BaseQuery:
"""moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id,
Module.ue_id == ue_id,
).count()
)
@classmethod
def nb_inscriptions_dans_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()
@classmethod
def sum_coefs_modimpl_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> float:
"""Somme des coefficients des modules auxquels l'étudiant est inscrit
dans l'UE du semestre indiqué.
N'utilise que les coefficients, donc inadapté aux formations APC.
"""
return sum(
[
inscr.modimpl.module.coefficient
for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
]
)

View File

@ -291,15 +291,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
):
) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])

View File

@ -282,7 +282,7 @@ class TypeParcours(object):
return [
ue_status
for ue_status in ues_status
if ue_status["coef_ue"] > 0
if ue_status["coef_ue"]
and isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
]

View File

@ -1078,7 +1078,7 @@ def formsemestre_status(formsemestre_id=None):
"</p>",
]
if use_ue_coefs:
if use_ue_coefs and not formsemestre.formation.is_apc():
H.append(
"""
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>

View File

@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table(
else:
H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit:
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [
ue
for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
]
for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"]
else:
code = ""
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
ue_status = etud_ue_status[ue["ue_id"]]
moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings
if code == ADM:

View File

@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object):
# pour le DUT, le dernier est toujours S4.
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
# (licences et autres formations en 1 seule session))
self.semestre_non_terminal = (
self.sem["semestre_id"] != self.parcours.NB_SEM
) # True | False
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant:

View File

@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */
}
div.logo-logo {
margin-left: -5px;
text-align: center ;
}
div.logo-logo img {
box-sizing: content-box;
margin-top: -10px;
width: 128px;
margin-top: 10px; /* -10px */
width: 135px; /* 128px */
padding-right: 5px;
margin-left: -75px;
}
div.sidebar-bottom {
margin-top: 10px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -35,7 +35,7 @@ from operator import itemgetter
from xml.etree import ElementTree
import flask
from flask import flash, jsonify, render_template, url_for
from flask import abort, flash, jsonify, render_template, url_for
from flask import current_app, g, request
from flask_login import current_user
from werkzeug.utils import redirect
@ -68,10 +68,14 @@ from app.scodoc import sco_utils as scu
from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoException,
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header
from app.pe import pe_view
from app.scodoc import sco_abs
@ -2672,12 +2676,15 @@ def check_integrity_all():
def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
):
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
try:
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom