diff --git a/app/comp/res_common.py b/app/comp/res_common.py index f1320126..7fa75c1b 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -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 + via cette page. + """ + ) + 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, diff --git a/app/models/moduleimpls.py b/app/models/moduleimpls.py index 700dec26..0aa74ef4 100644 --- a/app/models/moduleimpls.py +++ b/app/models/moduleimpls.py @@ -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) + ] + ) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index ca6748de..a5d84cf4 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -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"]) diff --git a/app/scodoc/sco_codes_parcours.py b/app/scodoc/sco_codes_parcours.py index f6ca2ff2..bcd6522b 100644 --- a/app/scodoc/sco_codes_parcours.py +++ b/app/scodoc/sco_codes_parcours.py @@ -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"]) ] diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py index 2c846515..4bfa7b72 100644 --- a/app/scodoc/sco_formsemestre_status.py +++ b/app/scodoc/sco_formsemestre_status.py @@ -1078,7 +1078,7 @@ def formsemestre_status(formsemestre_id=None): "

", ] - if use_ue_coefs: + if use_ue_coefs and not formsemestre.formation.is_apc(): H.append( """

utilise les coefficients d'UE pour calculer la moyenne générale.

diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py index 987dc962..c83e1cc4 100644 --- a/app/scodoc/sco_formsemestre_validation.py +++ b/app/scodoc/sco_formsemestre_validation.py @@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table( else: H.append('en cours') H.append('%s' % 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: diff --git a/app/scodoc/sco_parcours_dut.py b/app/scodoc/sco_parcours_dut.py index d6e244e7..fbf190c5 100644 --- a/app/scodoc/sco_parcours_dut.py +++ b/app/scodoc/sco_parcours_dut.py @@ -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: diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index b74eba0e..bd99c80c 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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; diff --git a/app/static/icons/scologo_img.png b/app/static/icons/scologo_img.png index fb0467ae..5c710a76 100644 Binary files a/app/static/icons/scologo_img.png and b/app/static/icons/scologo_img.png differ diff --git a/app/views/notes.py b/app/views/notes.py index cd211ca0..a8c387e4 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -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