From 45449f0465853e3ee27c4545ab30ce9ba5e8b00f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 28 May 2022 11:38:22 +0200 Subject: [PATCH] BUT: Partition de parcours et inscriptions --- app/models/__init__.py | 1 + app/models/formsemestre.py | 81 +++++++++++++++++++++++ app/scodoc/sco_formsemestre.py | 4 +- app/scodoc/sco_formsemestre_edit.py | 7 +- app/scodoc/sco_formsemestre_validation.py | 18 +++-- app/scodoc/sco_groups.py | 59 +++++++++++------ app/scodoc/sco_groups_edit.py | 1 + app/static/css/scodoc.css | 8 +++ app/views/scolar.py | 13 +--- 9 files changed, 151 insertions(+), 41 deletions(-) diff --git a/app/models/__init__.py b/app/models/__init__.py index d259966f..84f1332e 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 90461c98..393e1178 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 11792057..8d286b22 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 1acf5b95..c4e32946 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 f4896a8d..1f22f536 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 50e0e70e..21fb5b72 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 48290467..b4fa4511 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 1696009a..062c69c6 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 8a07d489..f1e860de 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",