BUT: dispenses d'UE capitalisées. Voir #537.

This commit is contained in:
Emmanuel Viennet 2022-12-01 13:00:14 +01:00 committed by iziram
parent ae9aad0619
commit a87dbd9927
13 changed files with 247 additions and 70 deletions

View File

@ -80,6 +80,9 @@ class BulletinBUT:
""" """
res = self.res res = self.res
if (etud.id, ue.id) in self.res.dispense_ues:
return {}
if ue.type == UE_SPORT: if ue.type == UE_SPORT:
modimpls_spo = [ modimpls_spo = [
modimpl modimpl

View File

@ -32,9 +32,17 @@ import pandas as pd
from app import db from app import db
from app import models from app import models
from app.models import UniteEns, Module, ModuleImpl, ModuleUECoef from app.models import (
DispenseUE,
FormSemestre,
FormSemestreInscription,
Identite,
Module,
ModuleImpl,
ModuleUECoef,
UniteEns,
)
from app.comp import moy_mod from app.comp import moy_mod
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
@ -140,7 +148,8 @@ def df_load_modimpl_coefs(
mod_coef.ue_id mod_coef.ue_id
] = mod_coef.coef ] = mod_coef.coef
except IndexError: except IndexError:
# il peut y avoir en base des coefs sur des modules ou UE qui ont depuis été retirés de la formation # il peut y avoir en base des coefs sur des modules ou UE
# qui ont depuis été retirés de la formation
pass pass
# Initialisation des poids non fixés: # Initialisation des poids non fixés:
# 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse # 0 pour modules normaux, 1. pour bonus (car par défaut, on veut qu'un bonus agisse
@ -199,7 +208,7 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
modimpls_results[modimpl.id] = mod_results modimpls_results[modimpl.id] = mod_results
modimpls_evals_poids[modimpl.id] = evals_poids modimpls_evals_poids[modimpl.id] = evals_poids
modimpls_notes.append(etuds_moy_module) modimpls_notes.append(etuds_moy_module)
if len(modimpls_notes): if len(modimpls_notes) > 0:
cube = notes_sem_assemble_cube(modimpls_notes) cube = notes_sem_assemble_cube(modimpls_notes)
else: else:
nb_etuds = formsemestre.etuds.count() nb_etuds = formsemestre.etuds.count()
@ -211,14 +220,39 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
) )
def load_dispense_ues(
formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns]
) -> set[tuple[int, int]]:
"""Construit l'ensemble des
etudids = modimpl_inscr_df.index, # les etudids
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
Résultat: set de (etudid, ue_id).
"""
dispense_ues = set()
ue_sem_by_code = {ue.ue_code: ue for ue in ues}
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
# puis filtre sur inscrits et code d'UE UE
for dispense_ue in DispenseUE.query.join(
Identite, FormSemestreInscription
).filter_by(formsemestre_id=formsemestre.id):
if dispense_ue.etudid in etudids:
# UE dans le semestre avec même code ?
ue = ue_sem_by_code.get(dispense_ue.ue.ue_code)
if ue is not None:
dispense_ues.add((dispense_ue.etudid, ue.id))
return dispense_ues
def compute_ue_moys_apc( def compute_ue_moys_apc(
sem_cube: np.array, sem_cube: np.array,
etuds: list, etuds: list,
modimpls: list, modimpls: list,
ues: list,
modimpl_inscr_df: pd.DataFrame, modimpl_inscr_df: pd.DataFrame,
modimpl_coefs_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame,
modimpl_mask: np.array, modimpl_mask: np.array,
dispense_ues: set[tuple[int, int]],
block: bool = False, block: bool = False,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Calcul de la moyenne d'UE en mode APC (BUT). """Calcul de la moyenne d'UE en mode APC (BUT).
@ -230,7 +264,7 @@ def compute_ue_moys_apc(
etuds : liste des étudiants (dim. 0 du cube) etuds : liste des étudiants (dim. 0 du cube)
modimpls : liste des module_impl (dim. 1 du cube) modimpls : liste des module_impl (dim. 1 du cube)
ues : liste des UE (dim. 2 du cube) ues : liste des UE (dim. 2 du cube)
modimpl_inscr_df: matrice d'inscription du semestre (etud x modimpl) modimpl_inscr_df: matrice d'inscription aux modules du semestre (etud x modimpl)
modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport modimpl_coefs_df: matrice coefficients (UE x modimpl), sans UEs bonus sport
modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas. modimpl_mask: liste de booléens, indiquants le module doit être pris ou pas.
(utilisé pour éliminer les bonus, et pourra servir à cacluler (utilisé pour éliminer les bonus, et pourra servir à cacluler
@ -239,7 +273,6 @@ def compute_ue_moys_apc(
Résultat: DataFrame columns UE (sans bonus), rows etudid Résultat: DataFrame columns UE (sans bonus), rows etudid
""" """
nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape
nb_ues_tot = len(ues)
assert len(modimpls) == nb_modules assert len(modimpls) == nb_modules
if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0: if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0:
return pd.DataFrame( return pd.DataFrame(
@ -278,11 +311,16 @@ def compute_ue_moys_apc(
etud_moy_ue = np.sum( etud_moy_ue = np.sum(
modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
return pd.DataFrame( etud_moy_ue_df = pd.DataFrame(
etud_moy_ue, etud_moy_ue,
index=modimpl_inscr_df.index, # les etudids index=modimpl_inscr_df.index, # les etudids
columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport columns=modimpl_coefs_df.index, # les UE sans les UE bonus sport
) )
# Les "dispenses" sont très peu nombreuses et traitées en python:
for dispense_ue in dispense_ues:
etud_moy_ue_df[dispense_ue[1]][dispense_ue[0]] = 0.0
return etud_moy_ue_df
def compute_ue_moys_classic( def compute_ue_moys_classic(
@ -435,7 +473,7 @@ def compute_mat_moys_classic(
Résultat: Résultat:
- moyennes: pd.Series, index etudid - moyennes: pd.Series, index etudid
""" """
if (not len(modimpl_mask)) or ( if (0 == len(modimpl_mask)) or (
sem_matrix.shape[0] == 0 sem_matrix.shape[0] == 0
): # aucun module ou aucun étudiant ): # aucun module ou aucun étudiant
# etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df # etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df

View File

@ -39,6 +39,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
"""ndarray (etuds x modimpl x ue)""" """ndarray (etuds x modimpl x ue)"""
self.etuds_parcour_id = None self.etuds_parcour_id = None
"""Parcours de chaque étudiant { etudid : parcour_id }""" """Parcours de chaque étudiant { etudid : parcour_id }"""
if not self.load_cached(): if not self.load_cached():
t0 = time.time() t0 = time.time()
self.compute() self.compute()
@ -71,14 +72,17 @@ class ResultatsSemestreBUT(NotesTableCompat):
modimpl.module.ue.type != UE_SPORT modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted for modimpl in self.formsemestre.modimpls_sorted
] ]
self.dispense_ues = moy_ue.load_dispense_ues(
self.formsemestre, self.modimpl_inscr_df.index, self.ues
)
self.etud_moy_ue = moy_ue.compute_ue_moys_apc( self.etud_moy_ue = moy_ue.compute_ue_moys_apc(
self.sem_cube, self.sem_cube,
self.etuds, self.etuds,
self.formsemestre.modimpls_sorted, self.formsemestre.modimpls_sorted,
self.ues,
self.modimpl_inscr_df, self.modimpl_inscr_df,
self.modimpl_coefs_df, self.modimpl_coefs_df,
modimpls_mask, modimpls_mask,
self.dispense_ues,
block=self.formsemestre.block_moyennes, block=self.formsemestre.block_moyennes,
) )
# Les coefficients d'UE ne sont pas utilisés en APC # Les coefficients d'UE ne sont pas utilisés en APC

View File

@ -48,12 +48,13 @@ class ResultatsSemestre(ResultatsCache):
_cached_attrs = ( _cached_attrs = (
"bonus", "bonus",
"bonus_ues", "bonus_ues",
"dispense_ues",
"etud_coef_ue_df",
"etud_moy_gen_ranks", "etud_moy_gen_ranks",
"etud_moy_gen", "etud_moy_gen",
"etud_moy_ue", "etud_moy_ue",
"modimpl_inscr_df", "modimpl_inscr_df",
"modimpls_results", "modimpls_results",
"etud_coef_ue_df",
"moyennes_matieres", "moyennes_matieres",
) )
@ -66,6 +67,8 @@ class ResultatsSemestre(ResultatsCache):
"Bonus sur moy. gen. Series de float, index etudid" "Bonus sur moy. gen. Series de float, index etudid"
self.bonus_ues: pd.DataFrame = None # virtuel self.bonus_ues: pd.DataFrame = None # virtuel
"DataFrame de float, index etudid, columns: ue.id" "DataFrame de float, index etudid, columns: ue.id"
self.dispense_ues: set[tuple[int, int]] = set()
"""set des dispenses d'UE: (etudid, ue_id), en APC seulement."""
# ResultatsSemestreBUT ou ResultatsSemestreClassic # ResultatsSemestreBUT ou ResultatsSemestreClassic
self.etud_moy_ue = {} self.etud_moy_ue = {}
"etud_moy_ue: DataFrame columns UE, rows etudid" "etud_moy_ue: DataFrame columns UE, rows etudid"

View File

@ -36,7 +36,7 @@ from app.models.etudiants import (
from app.models.events import Scolog, ScolarNews from app.models.events import Scolog, ScolarNews
from app.models.formations import Formation, Matiere from app.models.formations import Formation, Matiere
from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags from app.models.modules import Module, ModuleUECoef, NotesTag, notes_modules_tags
from app.models.ues import UniteEns from app.models.ues import DispenseUE, UniteEns
from app.models.formsemestre import ( from app.models.formsemestre import (
FormSemestre, FormSemestre,
FormSemestreEtape, FormSemestreEtape,

View File

@ -58,6 +58,12 @@ class Identite(db.Model):
billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic")
# #
admission = db.relationship("Admission", backref="identite", lazy="dynamic") admission = db.relationship("Admission", backref="identite", lazy="dynamic")
dispense_ues = db.relationship(
"DispenseUE",
back_populates="etud",
cascade="all, delete",
passive_deletes=True,
)
# Relations avec les assiduites et les justificatifs # Relations avec les assiduites et les justificatifs
assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic") assiduites = db.relationship("Assiduite", backref="etudiant", lazy="dynamic")

View File

@ -5,6 +5,7 @@ from app import db, log
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.but_refcomp import ApcNiveau, ApcParcours from app.models.but_refcomp import ApcNiveau, ApcParcours
from app.models.modules import Module
from app.scodoc.sco_exceptions import ScoFormationConflict from app.scodoc.sco_exceptions import ScoFormationConflict
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -57,6 +58,12 @@ class UniteEns(db.Model):
# relations # relations
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue") matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
modules = db.relationship("Module", lazy="dynamic", backref="ue") modules = db.relationship("Module", lazy="dynamic", backref="ue")
dispense_ues = db.relationship(
"DispenseUE",
back_populates="ue",
cascade="all, delete",
passive_deletes=True,
)
def __repr__(self): def __repr__(self):
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={ return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
@ -237,3 +244,31 @@ class UniteEns(db.Model):
db.session.add(self) db.session.add(self)
db.session.commit() db.session.commit()
log(f"ue.set_parcour( {self}, {parcour} )") log(f"ue.set_parcour( {self}, {parcour} )")
class DispenseUE(db.Model):
"""Dispense d'UE
Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
qu'ils ne refont pas.
"""
__table_args__ = (db.UniqueConstraint("ue_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True)
ue_id = db.Column(
db.Integer,
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
index=True,
nullable=False,
)
ue = db.relationship("UniteEns", back_populates="dispense_ues")
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
etud = db.relationship("Identite", back_populates="dispense_ues")
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} {self.id} etud={
repr(self.etud)} ue={repr(self.ue)}>"""

View File

@ -36,7 +36,7 @@ from flask_login import current_user
from app import log from app import log
from app.models import ScolarNews from app.models import ModuleImpl, ScolarNews
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -126,10 +126,13 @@ def do_evaluation_create(
"""Create an evaluation""" """Create an evaluation"""
if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id):
raise AccessDenied( raise AccessDenied(
"Modification évaluation impossible pour %s" % current_user.get_nomplogin() f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
) )
args = locals() args = locals()
log("do_evaluation_create: args=" + str(args)) log("do_evaluation_create: args=" + str(args))
modimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
if modimpl is None:
raise ValueError("module not found")
check_evaluation_args(args) check_evaluation_args(args)
# Check numeros # Check numeros
module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True)
@ -172,16 +175,18 @@ def do_evaluation_create(
r = _evaluationEditor.create(cnx, args) r = _evaluationEditor.create(cnx, args)
# news # news
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) url = url_for(
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] "notes.moduleimpl_status",
mod["moduleimpl_id"] = M["moduleimpl_id"] scodoc_dept=g.scodoc_dept,
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod moduleimpl_id=moduleimpl_id,
)
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
obj=moduleimpl_id, obj=moduleimpl_id,
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod, text=f"""Création d'une évaluation dans <a href="{url}">{
url=mod["url"], modimpl.module.titre or '(module sans titre)'}</a>""",
url=url,
) )
return r return r

View File

@ -262,6 +262,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
""" """
authuser = current_user authuser = current_user
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
is_apc = formsemestre.formation.is_apc() is_apc = formsemestre.formation.is_apc()
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
@ -390,36 +391,39 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
H.append("</table>") H.append("</table>")
# Etudiants "dispensés" d'une UE (capitalisée) # Etudiants "dispensés" d'une UE (capitalisée)
UECaps = get_etuds_with_capitalized_ue(formsemestre_id) ues_cap_info = get_etuds_with_capitalized_ue(formsemestre_id)
if UECaps: if ues_cap_info:
H.append('<h3>Etudiants avec UEs capitalisées:</h3><ul class="ue_inscr_list">') H.append('<h3>Étudiants avec UEs capitalisées:</h3><ul class="ue_inscr_list">')
ues = [sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in UECaps.keys()] ues = [
sco_edit_ue.ue_list({"ue_id": ue_id})[0] for ue_id in ues_cap_info.keys()
]
ues.sort(key=lambda u: u["numero"]) ues.sort(key=lambda u: u["numero"])
for ue in ues: for ue in ues:
H.append( H.append(
'<li class="tit"><span class="tit">%(acronyme)s: %(titre)s</span>' % ue f"""<li class="tit"><span class="tit">{ue['acronyme']}: {ue['titre']}</span>"""
) )
H.append("<ul>") H.append("<ul>")
for info in UECaps[ue["ue_id"]]: for info in ues_cap_info[ue["ue_id"]]:
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0] etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
H.append( H.append(
'<li class="etud"><a class="discretelink" href="%s">%s</a>' f"""<li class="etud"><a class="discretelink" href="{
% (
url_for( url_for(
"scolar.ficheEtud", "scolar.ficheEtud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud["etudid"],
),
etud["nomprenom"],
) )
}">{etud["nomprenom"]}</a>"""
) )
if info["ue_status"]["event_date"]: if info["ue_status"]["event_date"]:
H.append( H.append(
"(cap. le %s)" f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})"""
% (info["ue_status"]["event_date"]).strftime("%d/%m/%Y")
) )
if is_apc:
if info["is_ins"]: is_inscrit_ue = (etud["etudid"], ue["id"]) not in res.dispense_ues
else:
# CLASSIQUE
is_inscrit_ue = info["is_ins"]
if is_inscrit_ue:
dm = ", ".join( dm = ", ".join(
[ [
m["code"] or m["abbrev"] or "pas_de_code" m["code"] or m["abbrev"] or "pas_de_code"
@ -427,28 +431,40 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
] ]
) )
H.append( H.append(
'actuellement inscrit dans <a title="%s" class="discretelink">%d modules</a>' f"""actuellement inscrit dans <a title="{dm}" class="discretelink"
% (dm, len(info["is_ins"])) >{len(info["is_ins"])} modules</a>"""
) )
if is_inscrit_ue:
if info["ue_status"]["is_capitalized"]: if info["ue_status"]["is_capitalized"]:
H.append( H.append(
"""<div><em style="font-size: 70%">UE actuelle moins bonne que l'UE capitalisée</em></div>""" """<div><em style="font-size: 70%">UE actuelle moins bonne que
l'UE capitalisée</em>
</div>"""
) )
else: else:
H.append( H.append(
"""<div><em style="font-size: 70%">UE actuelle meilleure que l'UE capitalisée</em></div>""" """<div><em style="font-size: 70%">UE actuelle meilleure que
l'UE capitalisée</em>
</div>"""
) )
if can_change: if can_change:
H.append( H.append(
'<div><a class="stdlink" href="etud_desinscrit_ue?etudid=%s&formsemestre_id=%s&ue_id=%s">désinscrire des modules de cette UE</a></div>' f"""<div><a class="stdlink" href="{
% (etud["etudid"], formsemestre_id, ue["ue_id"]) url_for("notes.etud_desinscrit_ue",
scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
}">désinscrire {"des modules" if not is_apc else ""} de cette UE</a></div>
"""
) )
else: else:
H.append("(non réinscrit dans cette UE)") H.append("(non réinscrit dans cette UE)")
if can_change: if can_change:
H.append( H.append(
'<div><a class="stdlink" href="etud_inscrit_ue?etudid=%s&formsemestre_id=%s&ue_id=%s">inscrire à tous les modules de cette UE</a></div>' f"""<div><a class="stdlink" href="{
% (etud["etudid"], formsemestre_id, ue["ue_id"]) url_for("notes.etud_inscrit_ue", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"],
formsemestre_id=formsemestre_id, ue_id=ue["ue_id"])
}">inscrire à {"" if is_apc else "tous les modules de"} cette UE</a></div>
"""
) )
H.append("</li>") H.append("</li>")
H.append("</ul></li>") H.append("</ul></li>")
@ -524,11 +540,11 @@ def _fmt_etud_set(ins, max_list_size=7):
) )
def get_etuds_with_capitalized_ue(formsemestre_id): def get_etuds_with_capitalized_ue(formsemestre_id: int) -> list[dict]:
"""For each UE, computes list of students capitalizing the UE. """For each UE, computes list of students capitalizing the UE.
returns { ue_id : [ { infos } ] } returns { ue_id : [ { infos } ] }
""" """
UECaps = scu.DictDefault(defaultvalue=[]) ues_cap_info = scu.DictDefault(defaultvalue=[])
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
@ -540,21 +556,22 @@ def get_etuds_with_capitalized_ue(formsemestre_id):
for etud in inscrits: for etud in inscrits:
ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"]) ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"])
if ue_status and ue_status["was_capitalized"]: if ue_status and ue_status["was_capitalized"]:
UECaps[ue["ue_id"]].append( ues_cap_info[ue["ue_id"]].append(
{ {
"etudid": etud["etudid"], "etudid": etud["etudid"],
"ue_status": ue_status, "ue_status": ue_status,
"is_ins": is_inscrit_ue( "is_ins": etud_modules_ue_inscr(
etud["etudid"], formsemestre_id, ue["ue_id"] etud["etudid"], formsemestre_id, ue["ue_id"]
), ),
} }
) )
return UECaps return ues_cap_info
def is_inscrit_ue(etudid, formsemestre_id, ue_id): def etud_modules_ue_inscr(etudid, formsemestre_id, ue_id) -> list[int]:
"""Modules de cette UE dans ce semestre """Modules de cette UE dans ce semestre
auxquels l'étudiant est inscrit. auxquels l'étudiant est inscrit.
Utile pour formations classiques seulement.
""" """
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT mod.id AS module_id, mod.* """SELECT mod.id AS module_id, mod.*
@ -573,8 +590,10 @@ def is_inscrit_ue(etudid, formsemestre_id, ue_id):
return r return r
def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id): def do_etud_desinscrit_ue_classic(etudid, formsemestre_id, ue_id):
"""Desincrit l'etudiant de tous les modules de cette UE dans ce semestre.""" """Désinscrit l'etudiant de tous les modules de cette UE dans ce semestre.
N'utiliser que pour les formations classiques, pas APC.
"""
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
@ -597,7 +616,7 @@ def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
cnx, cnx,
method="etud_desinscrit_ue", method="etud_desinscrit_ue",
etudid=etudid, etudid=etudid,
msg="desinscription UE %s" % ue_id, msg=f"desinscription UE {ue_id}",
commit=False, commit=False,
) )
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(

View File

@ -58,7 +58,7 @@ from app.models.formsemestre import FormSemestre
from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.formsemestre import FormSemestreUEComputationExpr
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import DispenseUE, UniteEns
from app.scodoc.sco_exceptions import ScoFormationConflict from app.scodoc.sco_exceptions import ScoFormationConflict
from app.views import notes_bp as bp from app.views import notes_bp as bp
@ -1588,12 +1588,30 @@ sco_publish(
@permission_required(Permission.ScoEtudInscrit) @permission_required(Permission.ScoEtudInscrit)
@scodoc7func @scodoc7func
def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): def etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
"""Desinscrit l'etudiant de tous les modules de cette UE dans ce semestre.""" """
sco_moduleimpl_inscriptions.do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id) - En classique: désinscrit l'etudiant de tous les modules de cette UE dans ce semestre.
- En APC: dispense de l'UE indiquée.
"""
etud = Identite.query.get_or_404(etudid)
ue = UniteEns.query.get_or_404(ue_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if ue.formation.is_apc():
if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0:
disp = DispenseUE(ue_id=ue_id, etudid=etudid)
db.session.add(disp)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
else:
sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic(
etudid, formsemestre_id, ue_id
)
flash(f"{etud.nomprenom} déinscrit de {ue.acronyme}")
return flask.redirect( return flask.redirect(
scu.ScoURL() url_for(
+ "/Notes/moduleimpl_inscriptions_stats?formsemestre_id=" "notes.moduleimpl_inscriptions_stats",
+ str(formsemestre_id) scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
) )

View File

@ -0,0 +1,43 @@
"""DispenseUE
Revision ID: f95656fdd3ef
Revises: 5542cac8c34a
Create Date: 2022-11-30 22:22:05.045255
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "f95656fdd3ef"
down_revision = "5542cac8c34a"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"dispenseUE",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("ue_id", sa.Integer(), nullable=False),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["ue_id"], ["notes_ue.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("ue_id", "etudid"),
)
op.create_index(
op.f("ix_dispenseUE_etudid"), "dispenseUE", ["etudid"], unique=False
)
op.create_index(op.f("ix_dispenseUE_ue_id"), "dispenseUE", ["ue_id"], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_dispenseUE_ue_id"), table_name="dispenseUE")
op.drop_index(op.f("ix_dispenseUE_etudid"), table_name="dispenseUE")
op.drop_table("dispenseUE")
# ### end Alembic commands ###

View File

@ -1,13 +1,17 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.6" SCOVERSION = "9.4.7"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"
SCONEWS = """ SCONEWS = """
<h4>Année 2022</h4> <h4>Année 2022</h4>
<ul> <ul>
<li>ScoDoc 9.4</li>
<ul>
<li>Jury BUT2 avec parcours BUT</li>
</ul>
<li>ScoDoc 9.3</li> <li>ScoDoc 9.3</li>
<ul> <ul>
<li>Nouvelle API REST pour connecter ScoDoc à d'autres applications<li> <li>Nouvelle API REST pour connecter ScoDoc à d'autres applications<li>

View File

@ -74,7 +74,6 @@ def test_ue_moy(test_client):
sem_cube, sem_cube,
etuds, etuds,
modimpls, modimpls,
ues,
modimpl_inscr_df, modimpl_inscr_df,
modimpl_coefs_df, modimpl_coefs_df,
modimpl_mask, modimpl_mask,
@ -123,7 +122,7 @@ def test_ue_moy(test_client):
modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted
] ]
etud_moy_ue = moy_ue.compute_ue_moys_apc( etud_moy_ue = moy_ue.compute_ue_moys_apc(
sem_cube, etuds, modimpls, ues, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask sem_cube, etuds, modimpls, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask
) )
assert etud_moy_ue[ue1.id][etudid] == n1 assert etud_moy_ue[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1 assert etud_moy_ue[ue2.id][etudid] == n1