diff --git a/app/models/__init__.py b/app/models/__init__.py
index d259966ff..84f1332ed 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -76,5 +76,6 @@ from app.models.but_refcomp import (
ApcCompetence,
ApcSituationPro,
ApcAppCritique,
+ ApcParcours,
)
from app.models.config import ScoDocSiteConfig
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 90461c988..393e11786 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -5,6 +5,7 @@
import datetime
from functools import cached_property
+from flask import flash
import flask_sqlalchemy
from sqlalchemy.sql import text
@@ -13,6 +14,7 @@ from app import log
from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
+from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu
from app.models.but_refcomp import ApcParcours
@@ -480,6 +482,85 @@ class FormSemestre(db.Model):
"""Map { etudid : inscription } (incluant DEM et DEF)"""
return {ins.etud.id: ins for ins in self.inscriptions}
+ def setup_parcours_groups(self) -> None:
+ """Vérifie et créee si besoin la partition et les groupes de parcours BUT."""
+ if not self.formation.is_apc():
+ return
+ partition = Partition.query.filter_by(
+ formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
+ ).first()
+ if partition is None:
+ # Création de la partition de parcours
+ partition = Partition(
+ formsemestre_id=self.id,
+ partition_name=scu.PARTITION_PARCOURS,
+ numero=-1,
+ )
+ db.session.add(partition)
+ db.session.flush() # pour avoir un id
+ flash(f"Partition Parcours créée.")
+
+ for parcour in self.parcours:
+ if parcour.code:
+ group = GroupDescr.query.filter_by(
+ partition_id=partition.id, group_name=parcour.code
+ ).first()
+ if not group:
+ partition.groups.append(GroupDescr(group_name=parcour.code))
+ db.session.commit()
+
+ def update_inscriptions_parcours_from_groups(self) -> None:
+ """Met à jour les inscriptions dans les parcours du semestres en
+ fonction des groupes de parcours.
+ Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
+ et leur nom est le code du parcours (eg "Cyber").
+ """
+ partition = Partition.query.filter_by(
+ formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
+ ).first()
+ if partition is None: # pas de partition de parcours
+ return
+
+ # Efface les inscriptions aux parcours:
+ db.session.execute(
+ text(
+ """UPDATE notes_formsemestre_inscription
+ SET parcour_id=NULL
+ WHERE formsemestre_id=:formsemestre_id
+ """
+ ),
+ {
+ "formsemestre_id": self.id,
+ },
+ )
+ # Inscrit les étudiants des groupes de parcours:
+ for group in partition.groups:
+ query = ApcParcours.query.filter_by(code=group.group_name)
+ if query.count() != 1:
+ log(
+ f"""update_inscriptions_parcours_from_groups: {
+ query.count()} parcours with code {group.group_name}"""
+ )
+ continue
+ parcour = query.first()
+ db.session.execute(
+ text(
+ """UPDATE notes_formsemestre_inscription ins
+ SET parcour_id=:parcour_id
+ FROM group_membership gm
+ WHERE formsemestre_id=:formsemestre_id
+ AND gm.etudid = ins.etudid
+ AND gm.group_id = :group_id
+ """
+ ),
+ {
+ "formsemestre_id": self.id,
+ "parcour_id": parcour.id,
+ "group_id": group.id,
+ },
+ )
+ db.session.commit()
+
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
notes_formsemestre_responsables = db.Table(
diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py
index 11792057c..8d286b227 100644
--- a/app/scodoc/sco_formsemestre.py
+++ b/app/scodoc/sco_formsemestre.py
@@ -141,7 +141,9 @@ def do_formsemestre_list(*a, **kw):
def _formsemestre_enrich(sem):
- """Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
+ """Ajoute champs souvent utiles: titre + annee et dateord (pour tris).
+ XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre.
+ """
# imports ici pour eviter refs circulaires
from app.scodoc import sco_formsemestre_edit
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index 1acf5b95f..c4e329462 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -548,7 +548,8 @@ def do_formsemestre_createwithmodules(edit=False):
"allowed_values": [
str(parcour.id) for parcour in ref_comp.parcours
],
- "explanation": "Parcours proposés dans ce semestre.",
+ "explanation": """Parcours proposés dans ce semestre.
+ S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""",
},
)
]
@@ -905,7 +906,7 @@ def do_formsemestre_createwithmodules(edit=False):
modargs, formsemestre_id=formsemestre_id
)
mod = sco_edit_module.module_list({"module_id": module_id})[0]
- # --- Assocation des parcours
+ # --- Association des parcours
formsemestre = FormSemestre.query.get(formsemestre_id)
if "parcours" in tf[2]:
formsemestre.parcours = [
@@ -914,6 +915,8 @@ def do_formsemestre_createwithmodules(edit=False):
]
db.session.add(formsemestre)
db.session.commit()
+ # --- Crée ou met à jour les groupes de parcours BUT
+ formsemestre.setup_parcours_groups()
# --- Fin
if edit:
if msg:
diff --git a/app/scodoc/sco_formsemestre_validation.py b/app/scodoc/sco_formsemestre_validation.py
index f4896a8dc..1f22f536e 100644
--- a/app/scodoc/sco_formsemestre_validation.py
+++ b/app/scodoc/sco_formsemestre_validation.py
@@ -581,14 +581,20 @@ def formsemestre_recap_parcours_table(
else:
pm = plusminus % sem["formsemestre_id"]
- H.append(
- '
%s%s | '
- % (bgcolor, num_sem, pm)
+ inscr = formsemestre.etuds_inscriptions.get(etudid)
+ parcours_name = (
+ f' {inscr.parcour.code}'
+ if (inscr and inscr.parcour)
+ else ""
)
- H.append('%(mois_debut)s | ' % sem)
H.append(
- '%s | '
- % (a_url, sem["formsemestre_id"], etudid, sem["titreannee"])
+ f"""
+ {num_sem}{pm} |
+ {sem['mois_debut']} |
+ {formsemestre.titre_annee()}{parcours_name} |
+ """
)
if decision_sem:
H.append('%s | ' % decision_sem["code"])
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index 50e0e70ee..21fb5b722 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -107,14 +107,19 @@ def get_group(group_id: int):
return r[0]
-def group_delete(group, force=False):
+def group_delete(group_id: int):
"""Delete a group."""
# if not group['group_name'] and not force:
# raise ValueError('cannot suppress this group')
# remove memberships:
- ndb.SimpleQuery("DELETE FROM group_membership WHERE group_id=%(group_id)s", group)
+ ndb.SimpleQuery(
+ "DELETE FROM group_membership WHERE group_id=%(group_id)s",
+ {"group_id": group_id},
+ )
# delete group:
- ndb.SimpleQuery("DELETE FROM group_descr WHERE id=%(group_id)s", group)
+ ndb.SimpleQuery(
+ "DELETE FROM group_descr WHERE id=%(group_id)s", {"group_id": group_id}
+ )
def get_partition(partition_id):
@@ -690,7 +695,12 @@ def change_etud_group_in_partition(etudid, group_id, partition=None):
% (formsemestre_id, partition["partition_name"], group["group_name"]),
)
cnx.commit()
- # 4- invalidate cache
+
+ # 5- Update parcours
+ formsemestre = FormSemestre.query.get(formsemestre_id)
+ formsemestre.update_inscriptions_parcours_from_groups()
+
+ # 6- invalidate cache
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > change etud group
@@ -720,7 +730,7 @@ def setGroups(
return response
partition = get_partition(partition_id)
- if not partition["group_editable"]:
+ if not partition["groups_editable"]:
msg = "setGroups: partition non editable"
log(msg)
return xml_error(msg, code=403)
@@ -796,6 +806,10 @@ def setGroups(
for etudid in fs[1:-1]:
change_etud_group_in_partition(etudid, group_id, partition)
+ # Update parcours
+ formsemestre = FormSemestre.query.get(formsemestre_id)
+ formsemestre.update_inscriptions_parcours_from_groups()
+
data = (
'Groupes enregistrés'
)
@@ -835,21 +849,18 @@ def delete_group(group_id, partition_id=None):
affectation aux groupes)
partition_id est optionnel et ne sert que pour verifier que le groupe
est bien dans cette partition.
+ S'il s'agit d'un groupe de parcours, affecte l'inscription des étudiants aux parcours.
"""
- group = get_group(group_id)
+ group = GroupDescr.query.get_or_404(group_id)
if partition_id:
- if partition_id != group["partition_id"]:
+ if partition_id != group.partition_id:
raise ValueError("inconsistent partition/group")
- else:
- partition_id = group["partition_id"]
- partition = get_partition(partition_id)
- if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]):
+ if not sco_permissions_check.can_change_groups(group.partition.formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
- log(
- "delete_group: group_id=%s group_name=%s partition_name=%s"
- % (group_id, group["group_name"], partition["partition_name"])
- )
- group_delete(group)
+ log(f"delete_group: group={group:r} partition={group.partition}")
+ formsemestre = group.partition.formsemestre
+ group_delete(group.id)
+ formsemestre.update_inscriptions_parcours_from_groups()
def partition_create(
@@ -1097,11 +1108,14 @@ def partition_set_attr(partition_id, attr, value):
def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
"""Suppress a partition (and all groups within).
- default partition cannot be suppressed (unless force)"""
+ The default partition cannot be suppressed (unless force).
+ Si la partition de parcours est supprimée, les étudiants sont désinscrits des parcours.
+ """
partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"]
if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
+ formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not partition["partition_name"] and not force:
raise ValueError("cannot suppress this partition")
@@ -1127,10 +1141,12 @@ def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=Fal
log("partition_delete: partition_id=%s" % partition_id)
# 1- groups
for group in groups:
- group_delete(group, force=force)
+ group_delete(group["group_id"])
# 2- partition
partitionEditor.delete(cnx, partition_id)
+ formsemestre.update_inscriptions_parcours_from_groups()
+
# redirect to partition edit page:
if redirect:
return flask.redirect(
@@ -1214,7 +1230,8 @@ def partition_rename(partition_id):
"default": partition["partition_name"],
"allow_null": False,
"size": 12,
- "validator": lambda val, _: len(val) < SHORT_STR_LEN,
+ "validator": lambda val, _: (len(val) < SHORT_STR_LEN)
+ and (val != scu.PARTITION_PARCOURS),
},
),
),
@@ -1246,6 +1263,8 @@ def partition_set_name(partition_id, partition_name, redirect=1):
partition = get_partition(partition_id)
if partition["partition_name"] is None:
raise ValueError("can't set a name to default partition")
+ if partition_name == scu.PARTITION_PARCOURS:
+ raise ScoValueError(f"nom de partition {scu.PARTITION_PARCOURS} réservé.")
formsemestre_id = partition["formsemestre_id"]
# check unicity
@@ -1415,7 +1434,7 @@ def groups_auto_repartition(partition_id=None):
group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
# Détruit les groupes existant de cette partition
for old_group in get_partition_groups(partition):
- group_delete(old_group)
+ group_delete(old_group["group_id"])
# Crée les nouveaux groupes
group_ids = []
for group_name in group_names:
diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py
index 482904678..b4fa4511b 100644
--- a/app/scodoc/sco_groups_edit.py
+++ b/app/scodoc/sco_groups_edit.py
@@ -43,6 +43,7 @@ def affect_groups(partition_id):
formsemestre_id = partition["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")
+ partition.formsemestre.setup_parcours_groups()
return render_template(
"scolar/affect_groups.html",
sco_header=html_sco_header.sco_header(
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 1696009a9..062c69c6d 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -2267,6 +2267,14 @@ span.missing_value {
color: red;
}
+span.code_parcours {
+ color: white;
+ background-color: rgb(254, 95, 246);
+ padding-left: 4px;
+ padding-right: 4px;
+ border-radius: 2px;
+}
+
tr#tf_module_parcours>td {
background-color: rgb(229, 229, 229);
}
diff --git a/app/views/scolar.py b/app/views/scolar.py
index 8a07d4895..f1e860deb 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -918,18 +918,7 @@ def create_partition_parcours(formsemestre_id):
"""Création d'une partitions nommée "Parcours" (PARTITION_PARCOURS)
avec un groupe par parcours."""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- if scu.PARTITION_PARCOURS in (p.partition_name for p in formsemestre.partitions):
- flash(f"""Partition "{scu.PARTITION_PARCOURS}" déjà existante""")
- else:
- partition_id = sco_groups.partition_create(
- formsemestre_id, partition_name=scu.PARTITION_PARCOURS, redirect=False
- )
- n = 0
- for parcour in formsemestre.parcours:
- if parcour.code:
- _ = sco_groups.create_group(partition_id, group_name=parcour.code)
- n += 1
- flash(f"Partition Parcours créée avec {n} groupes.")
+ formsemestre.setup_parcours_groups()
return flask.redirect(
url_for(
"scolar.edit_partition_form",