Dispenses d'UE BUT associées à un formsemestre

This commit is contained in:
Emmanuel Viennet 2023-01-13 19:23:18 -03:00
parent 460ce79d92
commit 2e4742b39e
10 changed files with 186 additions and 52 deletions

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

@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat
from app.comp.bonus_spo import BonusSport from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
from app.models.moduleimpls import ModuleImpl from app.models.moduleimpls import ModuleImpl
from app.models.ues import UniteEns from app.models.ues import DispenseUE, UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -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

@ -59,9 +59,8 @@ class FormSemestre(db.Model):
titre = db.Column(db.Text(), nullable=False) titre = db.Column(db.Text(), nullable=False)
date_debut = db.Column(db.Date(), nullable=False) date_debut = db.Column(db.Date(), nullable=False)
date_fin = db.Column(db.Date(), nullable=False) date_fin = db.Column(db.Date(), nullable=False)
etat = db.Column( etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
db.Boolean(), nullable=False, default=True, server_default="true" "False si verrouillé"
) # False si verrouillé
modalite = db.Column( modalite = db.Column(
db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite") db.String(SHORT_STR_LEN), db.ForeignKey("notes_form_modalites.modalite")
) # "FI", "FAP", "FC", ... ) # "FI", "FAP", "FC", ...

View File

@ -1,6 +1,8 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE) """ScoDoc 9 models : Unités d'Enseignement (UE)
""" """
import pandas as pd
from app import db, log 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
@ -249,12 +251,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"),
@ -273,3 +286,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

@ -1088,8 +1088,9 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
"""<span class="help">cliquez sur un module pour saisir des notes</span>""" """<span class="help">cliquez sur un module pour saisir des notes</span>"""
) )
elif datetime.date.today() > formsemestre.date_fin: elif datetime.date.today() > formsemestre.date_fin:
if formsemestre.etat:
H.append( H.append(
"""<span class="formsemestre_status_warning">semestre terminé</span>""" """<span class="formsemestre_status_warning">semestre du passé non verrouillé</span>"""
) )
else: else:
H.append( H.append(

View File

@ -35,15 +35,13 @@ from flask_login import current_user
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, UniteEns from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_cache from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -51,8 +49,10 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False): def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
@ -527,14 +527,39 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
>{etud.nomprenom}</a></td>""" >{etud.nomprenom}</a></td>"""
) )
for ue in ues: for ue in ues:
td_class = ""
est_inscr = ues_etud.get(ue.id) # None si pas concerné est_inscr = ues_etud.get(ue.id) # None si pas concerné
if est_inscr is None: if est_inscr is None:
content = "" content = ""
else: else:
# Validations d'UE déjà enregistrées dans d'autres semestres
validations_ue = (
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
.filter(
ScolarFormSemestreValidation.formsemestre_id
!= res.formsemestre.id,
ScolarFormSemestreValidation.code.in_(
sco_codes_parcours.CODES_UE_VALIDES
),
)
.join(UniteEns)
.filter_by(ue_code=ue.ue_code)
.all()
)
validations_ue.sort(
key=lambda v: sco_codes_parcours.BUT_CODES_ORDERED.get(v.code, 0)
)
validation = validations_ue[-1] if validations_ue else None
expl_validation = (
f"""Validée ({validation.code}) le {validation.event_date.strftime("%d/%m/%Y")}"""
if validation
else ""
)
td_class = ' class="ue_validee"' if validation else ""
content = f"""<input type="checkbox" content = f"""<input type="checkbox"
{'checked' if est_inscr else ''} {'checked' if est_inscr else ''}
{'disabled' if read_only else ''} {'disabled' if read_only else ''}
title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}", title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}",
onchange="change_ue_inscr(this);" onchange="change_ue_inscr(this);"
data-url_inscr={ data-url_inscr={
url_for("notes.etud_inscrit_ue", url_for("notes.etud_inscrit_ue",
@ -549,7 +574,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
/> />
""" """
H.append(f"""<td>{content}</td>""") H.append(f"""<td{td_class}>{content}</td>""")
H.append( H.append(
"""</table> """</table>
</form> </form>

View File

@ -1992,6 +1992,10 @@ div.list_but_ue_inscriptions table td {
border-bottom: 1px solid salmon; border-bottom: 1px solid salmon;
} }
div.list_but_ue_inscriptions table td.ue_validee {
background-color: #a1f539;
}
form.list_but_ue_inscriptions { form.list_but_ue_inscriptions {
margin-bottom: 16px; margin-bottom: 16px;
} }
@ -2004,7 +2008,6 @@ form.list_but_ue_inscriptions td {
text-align: center; text-align: center;
} }
/* Formulaire edition des partitions */ /* Formulaire edition des partitions */
form#editpart table { form#editpart table {
border: 1px solid gray; border: 1px solid gray;

View File

@ -1615,15 +1615,22 @@ 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}") log(f"etud_desinscrit_ue {etud} {ue}")
Scolog.logdb( Scolog.logdb(
method="etud_desinscrit_ue", method="etud_desinscrit_ue",
etudid=etud.id, etudid=etud.id,
msg=f"Désinscription de l'UE {ue.acronyme}", msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
commit=True, commit=True,
) )
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id) sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
@ -1658,13 +1665,15 @@ def etud_inscrit_ue(etudid, formsemestre_id, ue_id):
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
ue = UniteEns.query.get_or_404(ue_id) ue = UniteEns.query.get_or_404(ue_id)
if ue.formation.is_apc(): if ue.formation.is_apc():
for disp in DispenseUE.query.filter_by(etudid=etud.id, ue_id=ue_id): for disp in DispenseUE.query.filter_by(
formsemestre_id=formsemestre_id, etudid=etud.id, ue_id=ue_id
):
db.session.delete(disp) db.session.delete(disp)
log(f"etud_inscrit_ue {etud} {ue}") log(f"etud_inscrit_ue {etud} {ue}")
Scolog.logdb( Scolog.logdb(
method="etud_inscrit_ue", method="etud_inscrit_ue",
etudid=etud.id, etudid=etud.id,
msg=f"Inscription à l'UE {ue.acronyme}", msg=f"Inscription à l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
commit=True, commit=True,
) )
db.session.commit() db.session.commit()

View File

@ -0,0 +1,86 @@
"""DispenseUE par semestre
Revision ID: 25e3ca6cc063
Revises: 7e5b519a27e1
Create Date: 2023-01-13 17:19:49.431591
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "25e3ca6cc063"
down_revision = "7e5b519a27e1"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"dispenseUE", sa.Column("formsemestre_id", sa.Integer(), nullable=True)
)
op.drop_constraint("dispenseUE_ue_id_etudid_key", "dispenseUE", type_="unique")
op.create_index(
op.f("ix_dispenseUE_formsemestre_id"),
"dispenseUE",
["formsemestre_id"],
unique=False,
)
op.create_unique_constraint(
None, "dispenseUE", ["formsemestre_id", "ue_id", "etudid"]
)
op.create_foreign_key(
None, "dispenseUE", "notes_formsemestre", ["formsemestre_id"], ["id"]
)
# ### end Alembic commands ###
# Affecte les dispenses au formsemestre le plus récent ayant cette UE
bind = op.get_bind()
session = Session(bind=bind)
dispenses = session.execute(
"""SELECT id, ue_id, etudid FROM "dispenseUE" WHERE formsemestre_id IS NULL;"""
).all()
for dispense_id, ue_id, etudid in dispenses:
formsemestre_ids = session.execute(
"""
SELECT notes_formsemestre.id
FROM notes_formsemestre, notes_formations, notes_ue, notes_formsemestre_inscription
WHERE notes_formsemestre.formation_id = notes_formations.id
and notes_ue.formation_id = notes_formations.id
and notes_ue.semestre_idx=notes_formsemestre.semestre_id
and notes_formsemestre_inscription.formsemestre_id=notes_formsemestre.id
and notes_ue.id = :ue_id
and notes_formsemestre_inscription.etudid = :etudid
ORDER BY notes_formsemestre.date_debut DESC
LIMIT 1;
""",
{"ue_id": ue_id, "etudid": etudid},
).all()
if formsemestre_ids:
formsemestre_id = formsemestre_ids[0][0]
session.execute(
"""
UPDATE "dispenseUE" SET formsemestre_id=:formsemestre_id WHERE id=:dispense_id""",
{"formsemestre_id": formsemestre_id, "dispense_id": dispense_id},
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
"dispenseUE_formsemestre_id_fkey", "dispenseUE", type_="foreignkey"
)
op.drop_constraint(
"dispenseUE_formsemestre_id_ue_id_etudid_key", "dispenseUE", type_="unique"
)
op.drop_index(op.f("ix_dispenseUE_formsemestre_id"), table_name="dispenseUE")
op.create_unique_constraint(
"dispenseUE_ue_id_etudid_key", "dispenseUE", ["ue_id", "etudid"]
)
op.drop_column("dispenseUE", "formsemestre_id")
# ### end Alembic commands ###

View File

@ -24,7 +24,7 @@ from app import clear_scodoc_cache
from app import models from app import models
from app.auth.models import User, Role, UserRole from app.auth.models import User, Role, UserRole
from app.scodoc.sco_logos import make_logo_local from app.entreprises.models import entreprises_reset_database
from app.models import departements from app.models import departements
from app.models import Formation, UniteEns, Matiere, Module from app.models import Formation, UniteEns, Matiere, Module
from app.models import FormSemestre, FormSemestreInscription from app.models import FormSemestre, FormSemestreInscription
@ -32,8 +32,9 @@ from app.models import GroupDescr
from app.models import Identite from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription from app.models import ModuleImpl, ModuleImplInscription
from app.models import Partition from app.models import Partition
from app.models import ScolarFormSemestreValidation
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation
from app.entreprises.models import entreprises_reset_database from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar from app.views import notes, scolar
import tools import tools
@ -52,6 +53,7 @@ def make_shell_context():
import app as mapp # le package app import app as mapp # le package app
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
return { return {
@ -83,7 +85,9 @@ def make_shell_context():
"pp": pp, "pp": pp,
"Role": Role, "Role": Role,
"res_sem": res_sem, "res_sem": res_sem,
"ResultatsSemestreBUT": ResultatsSemestreBUT,
"scolar": scolar, "scolar": scolar,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews, "ScolarNews": models.ScolarNews,
"scu": scu, "scu": scu,
"UniteEns": UniteEns, "UniteEns": UniteEns,