WIP: BUT validations/parcours.

This commit is contained in:
Emmanuel Viennet 2022-05-29 17:34:03 +02:00
parent 6596bd778c
commit b25ba0bc39
10 changed files with 314 additions and 32 deletions

View File

@ -78,4 +78,6 @@ from app.models.but_refcomp import (
ApcAppCritique, ApcAppCritique,
ApcParcours, ApcParcours,
) )
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig

View File

@ -0,0 +1,100 @@
# -*- coding: UTF-8 -*
"""Décisions de jury validations) des RCUE et années du BUT
"""
from app import db
from app import log
from app.models import CODE_STR_LEN
from app.models.ues import UniteEns
from app.models.formsemestre import FormSemestre, FormSemestreInscription
class ApcValidationRCUE(db.Model):
"""Validation des niveaux de compétences
aka "regroupements cohérents d'UE" dans le jargon BUT.
"""
__tablename__ = "apc_validation_rcue"
# Assure unicité de la décision:
__table_args__ = (
db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
)
id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
# Les deux UE associées à ce niveau:
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
# optionnel, le parcours dans lequel se trouve la compétence:
parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
etud = db.relationship("Identite", backref="apc_validations_rcues")
formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues")
ue1 = db.relationship("UniteEns", foreign_keys=ue1_id)
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
parcour = db.relationship("ApcParcours")
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>"
def get_other_ue_rcue(ue: UniteEns, etudid: int) -> UniteEns:
"""L'autre UE du RCUE (niveau de compétence) pour cet étudiant,
None si pas trouvée.
"""
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
return None
q = UniteEns.query.filter(
FormSemestreInscription.etudid == etudid,
FormSemestreInscription.formsemestre_id == FormSemestre.id,
FormSemestre.formation_id == UniteEns.formation_id,
FormSemestre.semestre_id == UniteEns.semestre_idx,
UniteEns.niveau_competence_id == ue.niveau_competence_id,
UniteEns.semestre_idx != ue.semestre_idx,
)
if q.count() > 1:
log("Warning: get_other_ue_rcue: {q.count()} candidates UE")
return q.first()
class ApcValidationAnnee(db.Model):
"""Validation des années du BUT"""
__tablename__ = "apc_validation_annee"
# Assure unicité de la décision:
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),)
id = db.Column(db.Integer, primary_key=True)
etudid = db.Column(
db.Integer,
db.ForeignKey("identite.id", ondelete="CASCADE"),
index=True,
nullable=False,
)
ordre = db.Column(db.Integer, nullable=False)
"numéro de l'année: 1, 2, 3"
formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
)
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
etud = db.relationship("Identite", backref="apc_validations_annees")
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}:{self.code!r}>"

View File

@ -1,10 +1,17 @@
"""ScoDoc 9 models : Formations """ScoDoc 9 models : Formations
""" """
import flask_sqlalchemy
import app import app
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -148,6 +155,18 @@ class Formation(db.Model):
if change: if change:
app.clear_scodoc_cache() app.clear_scodoc_cache()
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery::
"""Les UEs d'un parcours de la formation.
Exemple: pour avoir les UE du semestre 3, faire
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
"""
return UniteEns.query.filter_by(formation=self).filter(
UniteEns.niveau_competence_id == ApcNiveau.id,
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcAnneeParcours.parcours_id == parcour.id,
)
class Matiere(db.Model): class Matiere(db.Model):
"""Matières: regroupe les modules d'une UE """Matières: regroupe les modules d'une UE

View File

@ -14,6 +14,12 @@ from app import 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
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.but_refcomp import (
ApcAnneeParcours,
ApcNiveau,
ApcParcours,
ApcParcoursNiveauCompetence,
)
from app.models.groups import GroupDescr, Partition from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -211,6 +217,22 @@ class FormSemestre(db.Model):
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT) sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
return sem_ues.order_by(UniteEns.numero) return sem_ues.order_by(UniteEns.numero)
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
"""UE que suit l'étudiant dans ce semestre BUT
en fonction du parcours dans lequel il est inscrit.
Si voulez les UE d'un parcour, il est plus efficace de passer par
`formation.query_ues_parcour(parcour)`.
"""
return self.query_ues().filter(
FormSemestreInscription.etudid == etudid,
FormSemestreInscription.formsemestre == self,
UniteEns.niveau_competence_id == ApcNiveau.id,
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
)
@cached_property @cached_property
def modimpls_sorted(self) -> list[ModuleImpl]: def modimpls_sorted(self) -> list[ModuleImpl]:
"""Liste des modimpls du semestre (y compris bonus) """Liste des modimpls du semestre (y compris bonus)

View File

@ -122,10 +122,12 @@ ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
ATB = "ATB" ATB = "ATB"
AJ = "AJ" AJ = "AJ"
CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis) CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
NAR = "NAR"
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission) DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
DEM = "DEM" DEM = "DEM"
JSD = "JSD" # jurytenu mais pas de code (Jury Sans Décision)
NAR = "NAR"
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
# codes actions # codes actions
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
@ -156,9 +158,7 @@ CODES_EXPL = {
RAT: "En attente d'un rattrapage", RAT: "En attente d'un rattrapage",
DEM: "Démission", DEM: "Démission",
} }
# Nota: ces explications sont personnalisables via le fichier
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
# variable: CONFIG.CODES_EXP
# Les codes de semestres: # Les codes de semestres:
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT} CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
@ -169,6 +169,9 @@ CODES_SEM_REO = {NAR: 1} # reorientation
CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée
# Pour le BUT:
CODES_RCUE = {ADM, AJ, CMP}
def code_semestre_validant(code: str) -> bool: def code_semestre_validant(code: str) -> bool:
"Vrai si ce CODE entraine la validation du semestre" "Vrai si ce CODE entraine la validation du semestre"

View File

@ -258,6 +258,7 @@ def do_formsemestre_inscription_with_modules(
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS """Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
(donc sauf le sport) (donc sauf le sport)
""" """
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
# inscription au semestre # inscription au semestre
args = {"formsemestre_id": formsemestre_id, "etudid": etudid} args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
if etat is not None: if etat is not None:
@ -284,7 +285,7 @@ def do_formsemestre_inscription_with_modules(
f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition" f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition"
) )
# inscription a tous les modules de ce semestre # Inscription à tous les modules de ce semestre
modimpls = sco_moduleimpl.moduleimpl_withmodule_list( modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) )
@ -294,6 +295,8 @@ def do_formsemestre_inscription_with_modules(
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid}, {"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )
# Mise à jour des inscriptions aux parcours:
formsemestre.update_inscriptions_parcours_from_groups()
def formsemestre_inscription_with_modules_etud( def formsemestre_inscription_with_modules_etud(

View File

@ -149,7 +149,10 @@ def formsemestre_status_menubar(sem):
{ {
"title": "Voir la formation %(acronyme)s (v%(version)s)" % F, "title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
"endpoint": "notes.ue_table", "endpoint": "notes.ue_table",
"args": {"formation_id": sem["formation_id"]}, "args": {
"formation_id": sem["formation_id"],
"semestre_idx": sem["semestre_id"],
},
"enabled": True, "enabled": True,
"helpmsg": "Tableau de bord du semestre", "helpmsg": "Tableau de bord du semestre",
}, },

View File

@ -29,6 +29,7 @@
""" """
from flask import render_template from flask import render_template
from app.models import Partition
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
@ -39,8 +40,8 @@ def affect_groups(partition_id):
Permet aussi la creation et la suppression de groupes. Permet aussi la creation et la suppression de groupes.
""" """
# réécrit pour 9.0.47 avec un template # réécrit pour 9.0.47 avec un template
partition = sco_groups.get_partition(partition_id) partition = Partition.query.get_or_404(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition.formsemestre_id
if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("vous n'avez pas la permission de modifier les groupes") raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
partition.formsemestre.setup_parcours_groups() partition.formsemestre.setup_parcours_groups()
@ -53,8 +54,9 @@ def affect_groups(partition_id):
), ),
sco_footer=html_sco_header.sco_footer(), sco_footer=html_sco_header.sco_footer(),
partition=partition, partition=partition,
partitions_list=sco_groups.get_partitions_list( # Liste des partitions sans celle par defaut:
formsemestre_id, with_default=False partitions_list=partition.formsemestre.partitions.filter(
Partition.partition_name != None
), ),
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
) )

View File

@ -1,13 +1,13 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{{ sco_header|safe }} {{ sco_header|safe }}
<h2 class="formsemestre">Affectation aux groupes de {{ partition["partition_name"] }}</h2> <h2 class="formsemestre">Affectation aux groupes de {{ partition.partition_name }}</h2>
<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne <p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne
sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>". sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>".
Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien
"suppr." en haut à droite de sa boite. "suppr." en haut à droite de sa boite.
Vous pouvez aussi <a class="stdlink" Vous pouvez aussi <a class="stdlink"
href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition['partition_id']) }}" href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition.id) }}"
>répartir automatiquement les groupes</a>. >répartir automatiquement les groupes</a>.
</p> </p>
@ -15,7 +15,7 @@ href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, pa
<div id="ginfo"></div> <div id="ginfo"></div>
<div id="savedinfo"></div> <div id="savedinfo"></div>
<form name="formGroup" id="formGroup" onSubmit="return false;"> <form name="formGroup" id="formGroup" onSubmit="return false;">
<input type="hidden" name="partition_id" value="{{ partition['partition_id'] }}"/> <input type="hidden" name="partition_id" value="{{ partition.id }}"/>
<input name="groupName" size="6"/> <input name="groupName" size="6"/>
<input type="button" onClick="createGroup();" value="Créer groupe"/> <input type="button" onClick="createGroup();" value="Créer groupe"/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
@ -26,10 +26,10 @@ href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, pa
value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;Éditer groupes de value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;Éditer groupes de
<select name="other_partition_id" onchange="GotoAnother();"> <select name="other_partition_id" onchange="GotoAnother();">
{% for p in partitions_list %} {% for p in partitions_list %}
<option value="{{ p['id'] }}" {{ <option value="{{ p.id }}" {{
"selected" if p['partition_id'] == partition['partition_id'] "selected" if p.id == partition.id
}}>{{ }}>{{
p["partition_name"] p.partition_name
}}</option> }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -0,0 +1,128 @@
"""Validations BUT
Revision ID: 4311cc342dbd
Revises: a2771105c21c
Create Date: 2022-05-28 16:46:09.861248
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "4311cc342dbd"
down_revision = "a2771105c21c"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"apc_validation_annee",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.Column("ordre", sa.Integer(), nullable=False),
sa.Column("formsemestre_id", sa.Integer(), nullable=True),
sa.Column("annee_scolaire", sa.Integer(), nullable=False),
sa.Column(
"date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("code", sa.String(length=16), nullable=False),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["formsemestre_id"],
["notes_formsemestre.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("etudid", "annee_scolaire"),
)
op.create_index(
op.f("ix_apc_validation_annee_code"),
"apc_validation_annee",
["code"],
unique=False,
)
op.create_index(
op.f("ix_apc_validation_annee_etudid"),
"apc_validation_annee",
["etudid"],
unique=False,
)
op.create_table(
"apc_validation_rcue",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.Column("formsemestre_id", sa.Integer(), nullable=True),
sa.Column("ue1_id", sa.Integer(), nullable=False),
sa.Column("ue2_id", sa.Integer(), nullable=False),
sa.Column("parcours_id", sa.Integer(), nullable=True),
sa.Column(
"date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("code", sa.String(length=16), nullable=False),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["formsemestre_id"],
["notes_formsemestre.id"],
),
sa.ForeignKeyConstraint(
["parcours_id"],
["apc_parcours.id"],
),
sa.ForeignKeyConstraint(
["ue1_id"],
["notes_ue.id"],
),
sa.ForeignKeyConstraint(
["ue2_id"],
["notes_ue.id"],
),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
)
op.create_index(
op.f("ix_apc_validation_rcue_code"),
"apc_validation_rcue",
["code"],
unique=False,
)
op.create_index(
op.f("ix_apc_validation_rcue_etudid"),
"apc_validation_rcue",
["etudid"],
unique=False,
)
op.create_index(
op.f("ix_apc_validation_rcue_formsemestre_id"),
"apc_validation_rcue",
["formsemestre_id"],
unique=False,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_apc_validation_rcue_formsemestre_id"), table_name="apc_validation_rcue"
)
op.drop_index(
op.f("ix_apc_validation_rcue_etudid"), table_name="apc_validation_rcue"
)
op.drop_index(op.f("ix_apc_validation_rcue_code"), table_name="apc_validation_rcue")
op.drop_table("apc_validation_rcue")
op.drop_index(
op.f("ix_apc_validation_annee_etudid"), table_name="apc_validation_annee"
)
op.drop_index(
op.f("ix_apc_validation_annee_code"), table_name="apc_validation_annee"
)
op.drop_table("apc_validation_annee")
# ### end Alembic commands ###