diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index a3b3eb897c6a13aec43207df758b4f3543e60685..c9c4c00fec908dcb6d67e12d7ef94d6464d798a5 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -80,6 +80,9 @@ class BulletinBUT: """ res = self.res + if (etud.id, ue.id) in self.res.dispense_ues: + return {} + if ue.type == UE_SPORT: modimpls_spo = [ modimpl diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 698851e1151ff321c45bb0a90a9b79ccb7037c41..523cab3f15f1e730045096906b3a98f3efa38c57 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -32,9 +32,17 @@ import pandas as pd from app import db 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.models.formsemestre import FormSemestre from app.scodoc import sco_codes_parcours from app.scodoc import sco_preferences from app.scodoc.sco_codes_parcours import UE_SPORT @@ -140,7 +148,8 @@ def df_load_modimpl_coefs( mod_coef.ue_id ] = mod_coef.coef 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 # Initialisation des poids non fixés: # 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_evals_poids[modimpl.id] = evals_poids modimpls_notes.append(etuds_moy_module) - if len(modimpls_notes): + if len(modimpls_notes) > 0: cube = notes_sem_assemble_cube(modimpls_notes) else: 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( sem_cube: np.array, etuds: list, modimpls: list, - ues: list, modimpl_inscr_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame, modimpl_mask: np.array, + dispense_ues: set[tuple[int, int]], block: bool = False, ) -> pd.DataFrame: """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) modimpls : liste des module_impl (dim. 1 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_mask: liste de booléens, indiquants le module doit être pris ou pas. (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 """ nb_etuds, nb_modules, nb_ues_no_bonus = sem_cube.shape - nb_ues_tot = len(ues) assert len(modimpls) == nb_modules if block or nb_modules == 0 or nb_etuds == 0 or nb_ues_no_bonus == 0: return pd.DataFrame( @@ -278,11 +311,16 @@ def compute_ue_moys_apc( etud_moy_ue = np.sum( modimpl_coefs_etuds_no_nan * sem_cube_inscrits, axis=1 ) / np.sum(modimpl_coefs_etuds_no_nan, axis=1) - return pd.DataFrame( + etud_moy_ue_df = pd.DataFrame( etud_moy_ue, index=modimpl_inscr_df.index, # les etudids 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( @@ -435,7 +473,7 @@ def compute_mat_moys_classic( Résultat: - moyennes: pd.Series, index etudid """ - if (not len(modimpl_mask)) or ( + if (0 == len(modimpl_mask)) or ( sem_matrix.shape[0] == 0 ): # aucun module ou aucun étudiant # etud_moy_gen_s, etud_moy_ue_df, etud_coef_ue_df diff --git a/app/comp/res_but.py b/app/comp/res_but.py index 6ae6285f59154bd85592da0638e997334afe3e27..bd8891116a16f6f169796035372ded31e20ef307 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -39,6 +39,7 @@ class ResultatsSemestreBUT(NotesTableCompat): """ndarray (etuds x modimpl x ue)""" self.etuds_parcour_id = None """Parcours de chaque étudiant { etudid : parcour_id }""" + if not self.load_cached(): t0 = time.time() self.compute() @@ -71,14 +72,17 @@ class ResultatsSemestreBUT(NotesTableCompat): modimpl.module.ue.type != UE_SPORT 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.sem_cube, self.etuds, self.formsemestre.modimpls_sorted, - self.ues, self.modimpl_inscr_df, self.modimpl_coefs_df, modimpls_mask, + self.dispense_ues, block=self.formsemestre.block_moyennes, ) # Les coefficients d'UE ne sont pas utilisés en APC diff --git a/app/comp/res_common.py b/app/comp/res_common.py index cd7e5f93cc697ea2cad4a61cb9fc5444afb8e860..567c658d323b97ddc360883d1e2ab69ca6a95a76 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -48,12 +48,13 @@ class ResultatsSemestre(ResultatsCache): _cached_attrs = ( "bonus", "bonus_ues", + "dispense_ues", + "etud_coef_ue_df", "etud_moy_gen_ranks", "etud_moy_gen", "etud_moy_ue", "modimpl_inscr_df", "modimpls_results", - "etud_coef_ue_df", "moyennes_matieres", ) @@ -66,6 +67,8 @@ class ResultatsSemestre(ResultatsCache): "Bonus sur moy. gen. Series de float, index etudid" self.bonus_ues: pd.DataFrame = None # virtuel "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 self.etud_moy_ue = {} "etud_moy_ue: DataFrame columns UE, rows etudid" diff --git a/app/models/__init__.py b/app/models/__init__.py index 23c677c8632c5f0c6acaf344c44cf92e7e359182..a832ad74f0a5c946ca51b944dfbdf55571845c0e 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -36,7 +36,7 @@ from app.models.etudiants import ( from app.models.events import Scolog, ScolarNews from app.models.formations import Formation, Matiere 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 ( FormSemestre, FormSemestreEtape, diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 89bb90de84002a9b75842a576a3eb163db4e30c3..5d7052afee9ddc32817090427347269c4c8006c4 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -58,6 +58,12 @@ class Identite(db.Model): billets = db.relationship("BilletAbsence", backref="etudiant", lazy="dynamic") # admission = db.relationship("Admission", backref="identite", lazy="dynamic") + dispense_ues = db.relationship( + "DispenseUE", + back_populates="etud", + cascade="all, delete", + passive_deletes=True, + ) def __repr__(self): return ( diff --git a/app/models/ues.py b/app/models/ues.py index a832375b18f147a1fe5d17931662432042a72bdc..964dfae28b541620047dec3fff88944e48ec50bb 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -5,6 +5,7 @@ from app import db, log 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 @@ -57,6 +58,12 @@ class UniteEns(db.Model): # relations matieres = db.relationship("Matiere", 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): return f"""<{self.__class__.__name__}(id={self.id}, formation_id={ @@ -237,3 +244,31 @@ class UniteEns(db.Model): db.session.add(self) db.session.commit() 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)}>""" diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 09873bbe17ee1be226c8d92c31cd459055c71123..d3e27dae2dd2e41fcb3ae10f2a4610189c7764ea 100644 --- a/app/scodoc/sco_evaluation_db.py +++ b/app/scodoc/sco_evaluation_db.py @@ -36,7 +36,7 @@ from flask_login import current_user 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 import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -126,10 +126,13 @@ def do_evaluation_create( """Create an evaluation""" if not sco_permissions_check.can_edit_evaluation(moduleimpl_id=moduleimpl_id): raise AccessDenied( - "Modification évaluation impossible pour %s" % current_user.get_nomplogin() + f"Modification évaluation impossible pour {current_user.get_nomplogin()}" ) args = locals() 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 numeros module_evaluation_renumber(moduleimpl_id, only_if_unumbered=True) @@ -172,16 +175,18 @@ def do_evaluation_create( r = _evaluationEditor.create(cnx, args) # news - M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0] - sco_cache.invalidate_formsemestre(formsemestre_id=M["formsemestre_id"]) - mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] - mod["moduleimpl_id"] = M["moduleimpl_id"] - mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod + sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id) + url = url_for( + "notes.moduleimpl_status", + scodoc_dept=g.scodoc_dept, + moduleimpl_id=moduleimpl_id, + ) ScolarNews.add( typ=ScolarNews.NEWS_NOTE, obj=moduleimpl_id, - text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod, - url=mod["url"], + text=f"""Création d'une évaluation dans <a href="{url}">{ + modimpl.module.titre or '(module sans titre)'}</a>""", + url=url, ) return r diff --git a/app/scodoc/sco_moduleimpl_inscriptions.py b/app/scodoc/sco_moduleimpl_inscriptions.py index a6d037ae19d7114d282d1d6919af416aa35190d5..75aad0aa484a4d84f502329eef8639fa1ba28989 100644 --- a/app/scodoc/sco_moduleimpl_inscriptions.py +++ b/app/scodoc/sco_moduleimpl_inscriptions.py @@ -262,6 +262,7 @@ def moduleimpl_inscriptions_stats(formsemestre_id): """ authuser = current_user formsemestre = FormSemestre.query.get_or_404(formsemestre_id) + res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) is_apc = formsemestre.formation.is_apc() inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} @@ -390,65 +391,80 @@ def moduleimpl_inscriptions_stats(formsemestre_id): H.append("</table>") # Etudiants "dispensés" d'une UE (capitalisée) - UECaps = get_etuds_with_capitalized_ue(formsemestre_id) - if UECaps: - H.append('<h3>Etudiants 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_cap_info = get_etuds_with_capitalized_ue(formsemestre_id) + if ues_cap_info: + 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 ues_cap_info.keys() + ] ues.sort(key=lambda u: u["numero"]) for ue in ues: 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>") - 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] H.append( - '<li class="etud"><a class="discretelink" href="%s">%s</a>' - % ( + f"""<li class="etud"><a class="discretelink" href="{ url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"], - ), - etud["nomprenom"], - ) + ) + }">{etud["nomprenom"]}</a>""" ) if info["ue_status"]["event_date"]: H.append( - "(cap. le %s)" - % (info["ue_status"]["event_date"]).strftime("%d/%m/%Y") - ) - - if info["is_ins"]: - dm = ", ".join( - [ - m["code"] or m["abbrev"] or "pas_de_code" - for m in info["is_ins"] - ] - ) - H.append( - 'actuellement inscrit dans <a title="%s" class="discretelink">%d modules</a>' - % (dm, len(info["is_ins"])) + f"""(cap. le {info["ue_status"]["event_date"].strftime("%d/%m/%Y")})""" ) + if is_apc: + 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( + [ + m["code"] or m["abbrev"] or "pas_de_code" + for m in info["is_ins"] + ] + ) + H.append( + f"""actuellement inscrit dans <a title="{dm}" class="discretelink" + >{len(info["is_ins"])} modules</a>""" + ) + if is_inscrit_ue: if info["ue_status"]["is_capitalized"]: 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: 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: 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>' - % (etud["etudid"], formsemestre_id, ue["ue_id"]) + f"""<div><a class="stdlink" href="{ + 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: H.append("(non réinscrit dans cette UE)") if can_change: 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>' - % (etud["etudid"], formsemestre_id, ue["ue_id"]) + f"""<div><a class="stdlink" href="{ + 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("</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. returns { ue_id : [ { infos } ] } """ - UECaps = scu.DictDefault(defaultvalue=[]) + ues_cap_info = scu.DictDefault(defaultvalue=[]) formsemestre = FormSemestre.query.get_or_404(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) @@ -540,21 +556,22 @@ def get_etuds_with_capitalized_ue(formsemestre_id): for etud in inscrits: ue_status = nt.get_etud_ue_status(etud["etudid"], ue["ue_id"]) if ue_status and ue_status["was_capitalized"]: - UECaps[ue["ue_id"]].append( + ues_cap_info[ue["ue_id"]].append( { "etudid": etud["etudid"], "ue_status": ue_status, - "is_ins": is_inscrit_ue( + "is_ins": etud_modules_ue_inscr( 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 auxquels l'étudiant est inscrit. + Utile pour formations classiques seulement. """ r = ndb.SimpleDictFetch( """SELECT mod.id AS module_id, mod.* @@ -573,8 +590,10 @@ def is_inscrit_ue(etudid, formsemestre_id, ue_id): return r -def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id): - """Desincrit l'etudiant de tous les modules de cette UE dans ce semestre.""" +def do_etud_desinscrit_ue_classic(etudid, formsemestre_id, ue_id): + """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() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( @@ -597,7 +616,7 @@ def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id): cnx, method="etud_desinscrit_ue", etudid=etudid, - msg="desinscription UE %s" % ue_id, + msg=f"desinscription UE {ue_id}", commit=False, ) sco_cache.invalidate_formsemestre( diff --git a/app/views/notes.py b/app/views/notes.py index a104bd99d5e57fc60f98c47f2a723cf65b501bb5..f7e5473ff227385bb5dceda006de44d3fc8d58c1 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -58,7 +58,7 @@ from app.models.formsemestre import FormSemestre from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.moduleimpls import ModuleImpl 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.views import notes_bp as bp @@ -1588,12 +1588,30 @@ sco_publish( @permission_required(Permission.ScoEtudInscrit) @scodoc7func 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( - scu.ScoURL() - + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id=" - + str(formsemestre_id) + url_for( + "notes.moduleimpl_inscriptions_stats", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre_id, + ) ) diff --git a/migrations/versions/f95656fdd3ef_dispenseue.py b/migrations/versions/f95656fdd3ef_dispenseue.py new file mode 100644 index 0000000000000000000000000000000000000000..7da77b91009d9c9b51c6f6d15dfe2054f878d9d8 --- /dev/null +++ b/migrations/versions/f95656fdd3ef_dispenseue.py @@ -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 ### diff --git a/sco_version.py b/sco_version.py index 6ef4172ca82aaae260406063345ac4ec7ec41400..d3d81902fcc0c89fde0d3881bd791e455d084c16 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,13 +1,17 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.6" +SCOVERSION = "9.4.7" SCONAME = "ScoDoc" SCONEWS = """ <h4>Année 2022</h4> <ul> +<li>ScoDoc 9.4</li> + <ul> + <li>Jury BUT2 avec parcours BUT</li> + </ul> <li>ScoDoc 9.3</li> <ul> <li>Nouvelle API REST pour connecter ScoDoc à d'autres applications<li> diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py index bc0ad62252a247bf99662e38bb3e802702f7d938..8585ec60fb6eb4605b7944d9dacdce523d823235 100644 --- a/tests/unit/test_but_ues.py +++ b/tests/unit/test_but_ues.py @@ -74,7 +74,6 @@ def test_ue_moy(test_client): sem_cube, etuds, modimpls, - ues, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask, @@ -123,7 +122,7 @@ def test_ue_moy(test_client): modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted ] 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[ue2.id][etudid] == n1