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 numpy as np
import pandas as pd import pandas as pd
from flask import g, url_for
from app import log from app import log
from app.comp.aux_stats import StatsMoyenne from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl from app.models import FormSemestre, FormSemestreUECoef
from app.models import FormSemestreUECoef from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer # Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis): # - 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) 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] capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame): if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code] 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 # si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax() cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx] ue_cap = ue_cap.iloc[cap_idx]
@ -219,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code: if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations ue_cap = capitalisations
else: else:
ue_cap = None return None
return ue_cap # 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: def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant. """L'état de l'UE pour cet étudiant.
@ -253,17 +264,41 @@ class ResultatsSemestre(ResultatsCache):
) )
if etudid in self.validations.ue_capitalisees.index: if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue) ue_cap = self._get_etud_ue_cap(etudid, ue)
if ( if ue_cap and not np.isnan(ue_cap["moy_ue"]):
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
was_capitalized = True was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"] moy_ue = ue_cap["moy_ue"]
is_capitalized = True is_capitalized = True
# 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] 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 { return {
"is_capitalized": is_capitalized, "is_capitalized": is_capitalized,

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls """ScoDoc models: moduleimpls
""" """
import pandas as pd import pandas as pd
import flask_sqlalchemy
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
) )
@classmethod @classmethod
def nb_inscriptions_dans_ue( def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int: ) -> flask_sqlalchemy.BaseQuery:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit""" """moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter( return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid, ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id, ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id, ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id, ModuleImpl.module_id == Module.id,
Module.ue_id == ue_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"] = {}
I["matieres_modules_capitalized"] = {} I["matieres_modules_capitalized"] = {}
for ue in ues: for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ( if (
ModuleImplInscription.nb_inscriptions_dans_ue( ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"] formsemestre_id, etudid, ue["ue_id"]
) )
== 0 == 0
): ) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...} u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT: if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"]) u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])

View File

@ -282,7 +282,7 @@ class TypeParcours(object):
return [ return [
ue_status ue_status
for ue_status in ues_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 isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"]) 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>", "</p>",
] ]
if use_ue_coefs: if use_ue_coefs and not formsemestre.formation.is_apc():
H.append( H.append(
""" """
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p> <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: else:
H.append('<td colspan="%d"><em>en cours</em></td>') H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit: # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
ues = nt.get_ues_stat_dict(filter_sport=True) ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [ ues = [
ue ue
for ue in ues for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"]) 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: for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"] code = decisions_ue[ue["ue_id"]]["code"]
else: else:
code = "" 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 "" moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings explanation_ue = [] # list of strings
if code == ADM: if code == ADM:

View File

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

View File

@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */ width: 75px; /* la marge fait 130px */
} }
div.logo-logo { div.logo-logo {
margin-left: -5px;
text-align: center ; text-align: center ;
} }
div.logo-logo img { div.logo-logo img {
box-sizing: content-box; box-sizing: content-box;
margin-top: -10px; margin-top: 10px; /* -10px */
width: 128px; width: 135px; /* 128px */
padding-right: 5px; padding-right: 5px;
margin-left: -75px;
} }
div.sidebar-bottom { div.sidebar-bottom {
margin-top: 10px; 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 from xml.etree import ElementTree
import flask 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 import current_app, g, request
from flask_login import current_user from flask_login import current_user
from werkzeug.utils import redirect 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.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb 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.scodoc import html_sco_header
from app.pe import pe_view from app.pe import pe_view
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -2672,12 +2676,15 @@ def check_integrity_all():
def moduleimpl_list( def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json" moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
): ):
try:
data = sco_moduleimpl.moduleimpl_list( data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id, moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
module_id=module_id, module_id=module_id,
) )
return scu.sendResult(data, format=format) return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom @bp.route("/do_moduleimpl_withmodule_list") # ancien nom