diff --git a/app/api/justificatif.py b/app/api/justificatif.py index 374f2cb9f..3b0250081 100644 --- a/app/api/justificatif.py +++ b/app/api/justificatif.py @@ -400,8 +400,8 @@ def justif_import(justif_id: int = None): return json_error(404, err.args[0]) -@bp.route("/justificatif/export//", methods=["GET"]) -@api_web_bp.route("/justificatif/export//", methods=["GET"]) +@bp.route("/justificatif/export//", methods=["POST"]) +@api_web_bp.route("/justificatif/export//", methods=["POST"]) @scodoc @login_required @permission_required(Permission.ScoView) diff --git a/app/comp/moy_ue.py b/app/comp/moy_ue.py index 8c1d68dab..c533c21aa 100644 --- a/app/comp/moy_ue.py +++ b/app/comp/moy_ue.py @@ -33,10 +33,7 @@ import pandas as pd from app import db from app import models from app.models import ( - DispenseUE, FormSemestre, - FormSemestreInscription, - Identite, Module, ModuleImpl, ModuleUECoef, @@ -218,31 +215,6 @@ 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, diff --git a/app/comp/res_but.py b/app/comp/res_but.py index cf7f41018..f3f0c97db 100644 --- a/app/comp/res_but.py +++ b/app/comp/res_but.py @@ -72,7 +72,7 @@ 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.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set( self.formsemestre, self.modimpl_inscr_df.index, self.ues ) self.etud_moy_ue = moy_ue.compute_ue_moys_apc( diff --git a/app/models/ues.py b/app/models/ues.py index 2f14ee74f..596e0bef6 100644 --- a/app/models/ues.py +++ b/app/models/ues.py @@ -256,12 +256,23 @@ class UniteEns(db.Model): class DispenseUE(db.Model): """Dispense d'UE - Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée + Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée qu'ils ne refont pas. + La dispense d'UE n'est PAS une validation: + - elle n'est pas affectée par les décisions de jury (pas effacée) + - elle est associée à un formsemestre + - elle ne permet pas la délivrance d'ECTS ou du diplôme. + + On utilise cette dispense et non une "inscription" par souci d'efficacité: + en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours, + la dispense étant une exception. """ - __table_args__ = (db.UniqueConstraint("ue_id", "etudid"),) + __table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),) id = db.Column(db.Integer, primary_key=True) + formsemestre_id = formsemestre_id = db.Column( + db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True + ) ue_id = db.Column( db.Integer, db.ForeignKey(UniteEns.id, ondelete="CASCADE"), @@ -280,3 +291,25 @@ class DispenseUE(db.Model): def __repr__(self) -> str: return f"""<{self.__class__.__name__} {self.id} etud={ repr(self.etud)} ue={repr(self.ue)}>""" + + @classmethod + def load_formsemestre_dispense_ues_set( + cls, 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). + """ + # Prend toutes les dispenses obtenues par des étudiants de ce formsemestre, + # puis filtre sur inscrits et ues + ue_ids = {ue.id for ue in ues} + dispense_ues = { + (dispense_ue.etudid, dispense_ue.ue_id) + for dispense_ue in DispenseUE.query.filter_by( + formsemestre_id=formsemestre.id + ) + if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids + } + return dispense_ues diff --git a/app/scodoc/sco_evaluation_db.py b/app/scodoc/sco_evaluation_db.py index 134bcd1ad..b99af2cc3 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 db, log -from app.models import ModuleImpl, ScolarNews +from app.models import Evaluation, 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 diff --git a/app/views/notes.py b/app/views/notes.py index b5b65bb47..d5b9bca9f 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1621,10 +1621,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id): 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) + if ( + DispenseUE.query.filter_by( + formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id + ).count() + == 0 + ): + disp = DispenseUE( + formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid + ) db.session.add(disp) db.session.commit() + log(f"etud_desinscrit_ue {etud} {ue}") + Scolog.logdb( + method="etud_desinscrit_ue", + etudid=etud.id, + msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}", + commit=True, + ) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) else: sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic( diff --git a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py index 7b57ccbfb..f41044e25 100644 --- a/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py +++ b/migrations/versions/dbcf2175e87f_modèles_assiduites_justificatifs.py @@ -10,58 +10,86 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = 'dbcf2175e87f' -down_revision = '5c7b208355df' +revision = "dbcf2175e87f" +down_revision = "5c7b208355df" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_table('justificatifs', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('etudid', sa.Integer(), nullable=False), - sa.Column('etat', sa.Integer(), nullable=False), - sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.Column('raison', sa.Text(), nullable=True), - sa.Column('fichier', sa.Text(), nullable=True), - sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_table( + "justificatifs", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date_debut", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "date_fin", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("etat", sa.Integer(), nullable=False), + sa.Column( + "entry_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.Column("raison", sa.Text(), nullable=True), + sa.Column("fichier", sa.Text(), nullable=True), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_justificatifs_etudid'), 'justificatifs', ['etudid'], unique=False) - op.create_table('assiduites', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), - sa.Column('moduleimpl_id', sa.Integer(), nullable=True), - sa.Column('etudid', sa.Integer(), nullable=False), - sa.Column('etat', sa.Integer(), nullable=False), - sa.Column('desc', sa.Text(), nullable=True), - sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), - sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), - sa.ForeignKeyConstraint(['moduleimpl_id'], ['notes_moduleimpl.id'], ondelete='SET NULL'), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_justificatifs_etudid"), "justificatifs", ["etudid"], unique=False + ) + op.create_table( + "assiduites", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column( + "date_debut", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "date_fin", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column("moduleimpl_id", sa.Integer(), nullable=True), + sa.Column("etudid", sa.Integer(), nullable=False), + sa.Column("etat", sa.Integer(), nullable=False), + sa.Column("desc", sa.Text(), nullable=True), + sa.Column( + "entry_date", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=True, + ), + sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["moduleimpl_id"], ["notes_moduleimpl.id"], ondelete="SET NULL" + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_assiduites_etudid"), "assiduites", ["etudid"], unique=False ) - op.create_index(op.f('ix_assiduites_etudid'), 'assiduites', ['etudid'], unique=False) - op.drop_constraint('dispenseUE_formsemestre_id_ue_id_etudid_key', 'dispenseUE', type_='unique') - op.drop_index('ix_dispenseUE_formsemestre_id', table_name='dispenseUE') - op.create_unique_constraint(None, 'dispenseUE', ['ue_id', 'etudid']) - op.drop_constraint('dispenseUE_formsemestre_id_fkey', 'dispenseUE', type_='foreignkey') - op.drop_column('dispenseUE', 'formsemestre_id') # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('dispenseUE', sa.Column('formsemestre_id', sa.INTEGER(), autoincrement=False, nullable=True)) - op.create_foreign_key('dispenseUE_formsemestre_id_fkey', 'dispenseUE', 'notes_formsemestre', ['formsemestre_id'], ['id']) - op.drop_constraint(None, 'dispenseUE', type_='unique') - op.create_index('ix_dispenseUE_formsemestre_id', 'dispenseUE', ['formsemestre_id'], unique=False) - op.create_unique_constraint('dispenseUE_formsemestre_id_ue_id_etudid_key', 'dispenseUE', ['formsemestre_id', 'ue_id', 'etudid']) - op.drop_index(op.f('ix_assiduites_etudid'), table_name='assiduites') - op.drop_table('assiduites') - op.drop_index(op.f('ix_justificatifs_etudid'), table_name='justificatifs') - op.drop_table('justificatifs') + op.drop_index(op.f("ix_assiduites_etudid"), table_name="assiduites") + op.drop_table("assiduites") + op.drop_index(op.f("ix_justificatifs_etudid"), table_name="justificatifs") + op.drop_table("justificatifs") # ### end Alembic commands ### diff --git a/sco_version.py b/sco_version.py index c42cd066c..16ec61569 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.35" +SCOVERSION = "9.4.36" SCONAME = "ScoDoc" diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 8c5f4e2b5..8e5b16e3e 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -342,21 +342,23 @@ def test_list_justificatifs(api_headers): check_failure_get(f"/justificatif/list/{FAUX}", api_headers) -def get_export(id: int, fname: str, api_headers): +def post_export(id: int, fname: str, api_headers): url: str = API_URL + f"/justificatif/export/{id}/{fname}" - res = requests.get(url, headers=api_headers) + res = requests.post(url, headers=api_headers) return res def test_export(api_headers): # Bon fonctionnement - assert get_export(1, "test_api_justificatif.txt", api_headers).status_code == 200 + assert post_export(1, "test_api_justificatif.txt", api_headers).status_code == 200 # Mauvais fonctionnement - assert get_export(FAUX, "test_api_justificatif.txt", api_headers).status_code == 404 - assert get_export(1, "blabla.txt", api_headers).status_code == 404 - assert get_export(2, "blabla.txt", api_headers).status_code == 404 + assert ( + post_export(FAUX, "test_api_justificatif.txt", api_headers).status_code == 404 + ) + assert post_export(1, "blabla.txt", api_headers).status_code == 404 + assert post_export(2, "blabla.txt", api_headers).status_code == 404 def test_remove_justificatif(api_headers): diff --git a/tests/unit/test_but_ues.py b/tests/unit/test_but_ues.py index 64a109587..3c38a6e7e 100644 --- a/tests/unit/test_but_ues.py +++ b/tests/unit/test_but_ues.py @@ -123,7 +123,13 @@ 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, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask + sem_cube, + etuds, + modimpls, + modimpl_inscr_df, + modimpl_coefs_df, + modimpl_mask, + set(), ) assert etud_moy_ue[ue1.id][etudid] == n1 assert etud_moy_ue[ue2.id][etudid] == n1