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 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,

View File

@ -16,7 +16,7 @@ from app.comp.res_compat import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
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 import sco_preferences
@ -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(

View File

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

View File

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

View File

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

View File

@ -35,15 +35,13 @@ from flask_login import current_user
from app.comp import res_sem
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.scodoc.scolog import logdb
from app.scodoc import html_sco_header
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_ue
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_groups
from app.scodoc import sco_moduleimpl
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
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>"""
)
for ue in ues:
td_class = ""
est_inscr = ues_etud.get(ue.id) # None si pas concerné
if est_inscr is None:
content = ""
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"
{'checked' if est_inscr 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);"
data-url_inscr={
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(
"""</table>
</form>

View File

@ -1992,6 +1992,10 @@ div.list_but_ue_inscriptions table td {
border-bottom: 1px solid salmon;
}
div.list_but_ue_inscriptions table td.ue_validee {
background-color: #a1f539;
}
form.list_but_ue_inscriptions {
margin-bottom: 16px;
}
@ -2004,7 +2008,6 @@ form.list_but_ue_inscriptions td {
text-align: center;
}
/* Formulaire edition des partitions */
form#editpart table {
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)
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}",
msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
commit=True,
)
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)
ue = UniteEns.query.get_or_404(ue_id)
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)
log(f"etud_inscrit_ue {etud} {ue}")
Scolog.logdb(
method="etud_inscrit_ue",
etudid=etud.id,
msg=f"Inscription à l'UE {ue.acronyme}",
msg=f"Inscription à l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
commit=True,
)
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.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 Formation, UniteEns, Matiere, Module
from app.models import FormSemestre, FormSemestreInscription
@ -32,8 +32,9 @@ from app.models import GroupDescr
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models import Partition
from app.models import ScolarFormSemestreValidation
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.views import notes, scolar
import tools
@ -52,6 +53,7 @@ def make_shell_context():
import app as mapp # le package app
from app.scodoc import notesdb as ndb
from app.comp import res_sem
from app.comp.res_but import ResultatsSemestreBUT
from app.scodoc import sco_utils as scu
return {
@ -83,7 +85,9 @@ def make_shell_context():
"pp": pp,
"Role": Role,
"res_sem": res_sem,
"ResultatsSemestreBUT": ResultatsSemestreBUT,
"scolar": scolar,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews,
"scu": scu,
"UniteEns": UniteEns,