diff --git a/app/api/partitions.py b/app/api/partitions.py index 1f5ef8a1..4d752b65 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -12,6 +12,7 @@ from operator import attrgetter from flask import g, request from flask_json import as_json from flask_login import login_required +from sqlalchemy.exc import IntegrityError import app from app import db, log @@ -23,6 +24,7 @@ from app.models import GroupDescr, Partition, Scolog from app.models.groups import group_membership from app.scodoc import sco_cache from app.scodoc import sco_groups +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu @@ -182,10 +184,12 @@ def set_etud_group(etudid: int, group_id: int): if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return json_error(404, "etud non inscrit au formsemestre du groupe") - sco_groups.change_etud_group_in_partition( - etudid, group_id, group.partition.to_dict() - ) - + try: + sco_groups.change_etud_group_in_partition(etudid, group) + except ScoValueError as exc: + return json_error(404, exc.args[0]) + except IntegrityError: + return json_error(404, "échec de l'enregistrement") return {"group_id": group_id, "etudid": etudid} diff --git a/app/models/groups.py b/app/models/groups.py index 1d07ed21..35d22f8e 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -8,11 +8,13 @@ """ScoDoc models: Groups & partitions """ from operator import attrgetter +from sqlalchemy.exc import IntegrityError from app import db from app.models import SHORT_STR_LEN from app.models import GROUPNAME_STR_LEN from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import ScoValueError class Partition(db.Model): @@ -117,6 +119,40 @@ class Partition(db.Model): .first() ) + def set_etud_group(self, etudid: int, group: "GroupDescr"): + """Affect etudid to group_id in given partition. + Raises IntegrityError si conflit, + or ValueError si ce group_id n'est pas dans cette partition + ou que l'étudiant n'est pas inscrit au semestre. + """ + if not group.id in (g.id for g in self.groups): + raise ScoValueError( + f"""Le groupe {group.id} n'est pas dans la partition {self.partition_name or "tous"}""" + ) + if etudid not in (e.id for e in self.formsemestre.etuds): + raise ScoValueError( + f"etudiant {etudid} non inscrit au formsemestre du groupe {group.id}" + ) + try: + existing_row = ( + db.session.query(group_membership) + .filter_by(etudid=etudid) + .join(GroupDescr) + .filter_by(partition_id=self.id) + .first() + ) + if existing_row: + existing_row.update({"group_id": group.id}) + else: + new_row = group_membership.insert().values( + etudid=etudid, group_id=group.id + ) + db.session.execute(new_row) + db.session.commit() + except IntegrityError: + db.session.rollback() + raise + class GroupDescr(db.Model): """Description d'un groupe d'une partition""" diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 90f01f04..4d7aa131 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -546,6 +546,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No ue = UniteEns.query.get(ue_id) flash(f"UE créée (code {ue.ue_code})") else: + if not tf[2]["numero"]: + tf[2]["numero"] = 0 do_ue_edit(tf[2]) flash("UE modifiée") diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8b1da28a..b02fd337 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -48,9 +48,9 @@ from sqlalchemy.sql import text from app import db from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, Identite +from app.models import FormSemestre, Identite, Scolog from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN -from app.models.groups import GroupDescr, Partition +from app.models.groups import GroupDescr, Partition, group_membership import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app import log, cache @@ -94,7 +94,7 @@ groupEditor = ndb.EditableTable( group_list = groupEditor.list -def get_group(group_id: int) -> dict: +def get_group(group_id: int) -> dict: # OBSOLETE ! """Returns group object, with partition""" r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* @@ -124,7 +124,7 @@ def group_delete(group_id: int): ) -def get_partition(partition_id): +def get_partition(partition_id): # OBSOLETE r = ndb.SimpleDictFetch( """SELECT p.id AS partition_id, p.* FROM partition p @@ -200,7 +200,7 @@ def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict: return d -def get_partition_groups(partition): +def get_partition_groups(partition): # OBSOLETE ! """List of groups in this partition (list of dicts). Some groups may be empty.""" return ndb.SimpleDictFetch( @@ -637,7 +637,7 @@ def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre): return "" # parcours normal, ne le signale pas -def set_group(etudid: int, group_id: int) -> bool: +def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE ! """Inscrit l'étudiant au groupe. Return True if ok, False si deja inscrit. Warning: @@ -664,55 +664,31 @@ def set_group(etudid: int, group_id: int) -> bool: return True -def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None): - """Inscrit etud au groupe de cette partition, - et le desinscrit d'autres groupes de cette partition. +def change_etud_group_in_partition(etudid: int, group: GroupDescr): + """Inscrit etud au groupe + (et le desinscrit d'autres groupes de cette partition.) """ - log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id)) - # 0- La partition - group = get_group(group_id) - if partition: - # verifie que le groupe est bien dans cette partition: - if group["partition_id"] != partition["partition_id"]: - raise ValueError( - "inconsistent group/partition (group_id=%s, partition_id=%s)" - % (group_id, partition["partition_id"]) - ) - else: - partition = get_partition(group["partition_id"]) - # 1- Supprime membership dans cette partition - ndb.SimpleQuery( - """DELETE FROM group_membership gm - WHERE EXISTS - (SELECT 1 FROM group_descr gd - WHERE gm.etudid = %(etudid)s - AND gm.group_id = gd.id - AND gd.partition_id = %(partition_id)s) - """, - {"etudid": etudid, "partition_id": partition["partition_id"]}, - ) - # 2- associe au nouveau groupe - set_group(etudid, group_id) + log(f"change_etud_group_in_partition: etudid={etudid} group={group}") - # 3- log - formsemestre_id = partition["formsemestre_id"] - cnx = ndb.GetDBConnexion() - logdb( - cnx, + group.partition.set_etud_group(etudid, group) + + # - log + formsemestre: FormSemestre = group.partition.formsemestre + Scolog.logdb( method="changeGroup", etudid=etudid, - msg="formsemestre_id=%s,partition_name=%s, group_name=%s" - % (formsemestre_id, partition["partition_name"], group["group_name"]), + msg=f"""formsemestre_id={formsemestre.id}, partition_name={ + group.partition.partition_name or ""}, group_name={group.group_name or ""}""", + commit=True, ) - cnx.commit() - # 5- Update parcours - formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) - formsemestre.update_inscriptions_parcours_from_groups() + # - Update parcours + if group.partition.partition_name == scu.PARTITION_PARCOURS: + formsemestre.update_inscriptions_parcours_from_groups() - # 6- invalidate cache + # - invalidate cache sco_cache.invalidate_formsemestre( - formsemestre_id=formsemestre_id + formsemestre_id=formsemestre.id ) # > change etud group @@ -769,7 +745,7 @@ def setGroups( except ValueError: log(f"setGroups: ignoring invalid group_id={group_id}") continue - group = get_group(group_id) + group: GroupDescr = GroupDescr.query.get(group_id) # Anciens membres du groupe: old_members = get_group_members(group_id) old_members_set = set([x["etudid"] for x in old_members]) @@ -783,7 +759,7 @@ def setGroups( if (etudid not in etud_groups) or ( group_id != etud_groups[etudid].get(partition_id, "") ): # pas le meme groupe qu'actuel - change_etud_group_in_partition(etudid, group_id, partition) + change_etud_group_in_partition(etudid, group) # Retire les anciens membres: cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) @@ -819,7 +795,7 @@ def setGroups( return xml_error(msg, code=404) # Place dans ce groupe les etudiants indiqués: for etudid in fs[1:-1]: - change_etud_group_in_partition(etudid, group.id, partition) + change_etud_group_in_partition(etudid, group.id) # Update parcours formsemestre.update_inscriptions_parcours_from_groups() @@ -1460,10 +1436,10 @@ def groups_auto_repartition(partition_id=None): for old_group in get_partition_groups(partition): group_delete(old_group["group_id"]) # Crée les nouveaux groupes - group_ids = [] + groups = [] for group_name in group_names: if group_name.strip(): - group_ids.append(create_group(partition_id, group_name).id) + groups.append(create_group(partition_id, group_name)) # nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) identdict = nt.identdict @@ -1481,16 +1457,16 @@ def groups_auto_repartition(partition_id=None): # affect aux groupes: n = len(identdict) igroup = 0 - nbgroups = len(group_ids) + nbgroups = len(groups) while n > 0: for civilite in civilites: if len(listes[civilite]): n -= 1 etudid = listes[civilite].pop()[1] - group_id = group_ids[igroup] + group = groups[igroup] igroup = (igroup + 1) % nbgroups - change_etud_group_in_partition(etudid, group_id, partition) - log("%s in group %s" % (etudid, group_id)) + change_etud_group_in_partition(etudid, group) + log("%s in group %s" % (etudid, group.id)) return flask.redirect(dest_url) @@ -1520,10 +1496,11 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"): Si la partition existe déjà, ses groupes sont mis à jour (les groupes devenant vides ne sont pas supprimés). """ + # A RE-ECRIRE pour utiliser les modèles. from app.scodoc import sco_formsemestre_inscriptions partition_name = str(partition_name) - log("create_etapes_partition(%s)" % formsemestre_id) + log(f"create_etapes_partition({formsemestre_id})") ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} ) @@ -1542,20 +1519,17 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"): pid = partition_create( formsemestre_id, partition_name=partition_name, redirect=False ) - partition = get_partition(pid) - groups = get_partition_groups(partition) + partition: Partition = Partition.query.get(pid) + groups = partition.groups groups_by_names = {g["group_name"]: g for g in groups} for etape in etapes: - if not (etape in groups_by_names): + if etape not in groups_by_names: new_group = create_group(pid, etape) - g = get_group(new_group.id) # XXX transition: recupere old style dict - groups_by_names[etape] = g + groups_by_names[etape] = new_group # Place les etudiants dans les groupes for i in ins: if i["etape"]: - change_etud_group_in_partition( - i["etudid"], groups_by_names[i["etape"]]["group_id"], partition - ) + change_etud_group_in_partition(i["etudid"], groups_by_names[i["etape"]]) def do_evaluation_listeetuds_groups( diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index e7eb8dce..5bf61fda 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -723,7 +723,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None group = GroupDescr.query.get(group_id) if group.partition.groups_editable: sco_groups.change_etud_group_in_partition( - args["etudid"], group_id + args["etudid"], group ) else: log("scolars_import_admission: partition non editable") diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 1ebf679f..50260e27 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -36,13 +36,12 @@ from flask import url_for, g, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log -from app.models import Formation, FormSemestre +from app.models import Formation, FormSemestre, GroupDescr from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_etud -from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups @@ -177,6 +176,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False): (la liste doit avoir été vérifiée au préalable) En option: inscrit aux mêmes groupes que dans le semestre origine """ + # TODO à ré-écrire pour utiliser le smodèle, notamment GroupDescr formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"]) formsemestre.setup_parcours_groups() log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") @@ -220,11 +220,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False): # Inscrit aux groupes for partition_group in partition_groups: - sco_groups.change_etud_group_in_partition( - etudid, - partition_group["group_id"], - partition_group, - ) + group: GroupDescr = GroupDescr.query.get(partition_group["group_id"]) + sco_groups.change_etud_group_in_partition(etudid, group) def do_desinscrit(sem, etudids): diff --git a/sco_version.py b/sco_version.py index 0bd507e8..95fb08eb 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.97" +SCOVERSION = "9.4.98" SCONAME = "ScoDoc"