1
0
forked from ScoDoc/ScoDoc

correction etat branche + tests unitaire + tests api

This commit is contained in:
iziram 2023-02-03 14:51:05 +01:00
parent a63e14ce06
commit 4d72fec42d
10 changed files with 140 additions and 85 deletions

View File

@ -400,8 +400,8 @@ def justif_import(justif_id: int = None):
return json_error(404, err.args[0]) return json_error(404, err.args[0])
@bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["GET"]) @bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
@api_web_bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["GET"]) @api_web_bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
@scodoc @scodoc
@login_required @login_required
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)

View File

@ -33,10 +33,7 @@ import pandas as pd
from app import db from app import db
from app import models from app import models
from app.models import ( from app.models import (
DispenseUE,
FormSemestre, FormSemestre,
FormSemestreInscription,
Identite,
Module, Module,
ModuleImpl, ModuleImpl,
ModuleUECoef, 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( def compute_ue_moys_apc(
sem_cube: np.array, sem_cube: np.array,
etuds: list, etuds: list,

View File

@ -72,7 +72,7 @@ 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.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
self.formsemestre, self.modimpl_inscr_df.index, self.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(

View File

@ -256,12 +256,23 @@ class UniteEns(db.Model):
class DispenseUE(db.Model): class DispenseUE(db.Model):
"""Dispense d'UE """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. 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) 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( ue_id = db.Column(
db.Integer, db.Integer,
db.ForeignKey(UniteEns.id, ondelete="CASCADE"), db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
@ -280,3 +291,25 @@ class DispenseUE(db.Model):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"""<{self.__class__.__name__} {self.id} etud={ return f"""<{self.__class__.__name__} {self.id} etud={
repr(self.etud)} ue={repr(self.ue)}>""" 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

View File

@ -36,7 +36,7 @@ from flask_login import current_user
from app import db, log 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 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

View File

@ -1621,10 +1621,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if ue.formation.is_apc(): if ue.formation.is_apc():
if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0: if (
disp = DispenseUE(ue_id=ue_id, etudid=etudid) 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.add(disp)
db.session.commit() 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) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
else: else:
sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic( sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic(

View File

@ -10,58 +10,86 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'dbcf2175e87f' revision = "dbcf2175e87f"
down_revision = '5c7b208355df' down_revision = "5c7b208355df"
branch_labels = None branch_labels = None
depends_on = None depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('justificatifs', op.create_table(
sa.Column('id', sa.Integer(), nullable=False), "justificatifs",
sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), sa.Column(
sa.Column('etudid', sa.Integer(), nullable=False), "date_debut",
sa.Column('etat', sa.Integer(), nullable=False), sa.DateTime(timezone=True),
sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), server_default=sa.text("now()"),
sa.Column('raison', sa.Text(), nullable=True), nullable=False,
sa.Column('fichier', sa.Text(), nullable=True), ),
sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), sa.Column(
sa.PrimaryKeyConstraint('id') "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_index(
op.create_table('assiduites', op.f("ix_justificatifs_etudid"), "justificatifs", ["etudid"], unique=False
sa.Column('id', sa.Integer(), nullable=False), )
sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), op.create_table(
sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), "assiduites",
sa.Column('moduleimpl_id', sa.Integer(), nullable=True), sa.Column("id", sa.Integer(), nullable=False),
sa.Column('etudid', sa.Integer(), nullable=False), sa.Column(
sa.Column('etat', sa.Integer(), nullable=False), "date_debut",
sa.Column('desc', sa.Text(), nullable=True), sa.DateTime(timezone=True),
sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), server_default=sa.text("now()"),
sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'), nullable=False,
sa.ForeignKeyConstraint(['moduleimpl_id'], ['notes_moduleimpl.id'], ondelete='SET NULL'), ),
sa.PrimaryKeyConstraint('id') 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 ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.add_column('dispenseUE', sa.Column('formsemestre_id', sa.INTEGER(), autoincrement=False, nullable=True)) op.drop_index(op.f("ix_assiduites_etudid"), table_name="assiduites")
op.create_foreign_key('dispenseUE_formsemestre_id_fkey', 'dispenseUE', 'notes_formsemestre', ['formsemestre_id'], ['id']) op.drop_table("assiduites")
op.drop_constraint(None, 'dispenseUE', type_='unique') op.drop_index(op.f("ix_justificatifs_etudid"), table_name="justificatifs")
op.create_index('ix_dispenseUE_formsemestre_id', 'dispenseUE', ['formsemestre_id'], unique=False) op.drop_table("justificatifs")
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')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.35" SCOVERSION = "9.4.36"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -342,21 +342,23 @@ def test_list_justificatifs(api_headers):
check_failure_get(f"/justificatif/list/{FAUX}", 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}" 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 return res
def test_export(api_headers): def test_export(api_headers):
# Bon fonctionnement # 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 # Mauvais fonctionnement
assert get_export(FAUX, "test_api_justificatif.txt", api_headers).status_code == 404 assert (
assert get_export(1, "blabla.txt", api_headers).status_code == 404 post_export(FAUX, "test_api_justificatif.txt", api_headers).status_code == 404
assert get_export(2, "blabla.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): def test_remove_justificatif(api_headers):

View File

@ -123,7 +123,13 @@ 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, 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[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1 assert etud_moy_ue[ue2.id][etudid] == n1