diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index 6b76e22505..8ab323cee5 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -573,6 +573,17 @@ class FormSemestre(db.Model):
user
)
+ def can_change_groups(self, user: User = None) -> bool:
+ """Vrai si l'utilisateur (par def. current) peut changer les groupes dans
+ ce semestre: vérifie permission et verrouillage.
+ """
+ if not self.etat:
+ return False # semestre verrouillé
+ user = user or current_user
+ if user.has_permission(Permission.ScoEtudChangeGroups):
+ return True # typiquement admin, chef dept
+ return self.est_responsable(user)
+
def can_edit_jury(self, user: User = None):
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
dans ce semestre: vérifie permission et verrouillage.
diff --git a/app/models/groups.py b/app/models/groups.py
index dce377da8b..8e9d5b6200 100644
--- a/app/models/groups.py
+++ b/app/models/groups.py
@@ -10,11 +10,11 @@
from operator import attrgetter
from sqlalchemy.exc import IntegrityError
-from app import db
+from app import db, log
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
+from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
class Partition(db.Model):
@@ -119,7 +119,7 @@ class Partition(db.Model):
.first()
)
- def set_etud_group(self, etudid: int, group: "GroupDescr") -> bool:
+ def set_etud_group(self, etud: "Identite", group: "GroupDescr") -> bool:
"""Affect etudid to group_id in given partition.
Raises IntegrityError si conflit,
or ValueError si ce group_id n'est pas dans cette partition
@@ -128,36 +128,37 @@ class Partition(db.Model):
"""
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"}"""
+ 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):
+ if etud.id not in (e.id for e in self.formsemestre.etuds):
raise ScoValueError(
- f"etudiant {etudid} non inscrit au formsemestre du groupe {group.id}"
+ f"""étudiant {etud.nomprenom} non inscrit au formsemestre du groupe {
+ group.group_name}"""
)
try:
existing_row = (
db.session.query(group_membership)
- .filter_by(etudid=etudid)
+ .filter_by(etudid=etud.id)
.join(GroupDescr)
.filter_by(partition_id=self.id)
.first()
)
- existing_group_id = existing_row[1]
if existing_row:
+ existing_group_id = existing_row[1]
if group.id == existing_group_id:
return False
- update_row = (
- group_membership.update()
- .where(
- group_membership.c.etudid == etudid,
- group_membership.c.group_id == existing_group_id,
- )
- .values(group_id=group.id)
- )
- db.session.execute(update_row)
+ # Fait le changement avec l'ORM sinon risque élevé de blocage
+ existing_group = GroupDescr.query.get(existing_group_id)
+ db.session.commit()
+ group.etuds.append(etud)
+ existing_group.etuds.remove(etud)
+ db.session.add(etud)
+ db.session.add(existing_group)
+ db.session.add(group)
else:
new_row = group_membership.insert().values(
- etudid=etudid, group_id=group.id
+ etudid=etud.id, group_id=group.id
)
db.session.execute(new_row)
db.session.commit()
@@ -166,6 +167,33 @@ class Partition(db.Model):
raise
return True
+ def create_group(self, group_name="", default=False) -> "GroupDescr":
+ "Crée un groupe dans cette partition"
+ if not self.formsemestre.can_change_groups():
+ raise AccessDenied(
+ """Vous n'avez pas le droit d'effectuer cette opération,
+ ou bien le semestre est verrouillé !"""
+ )
+ if group_name:
+ group_name = group_name.strip()
+ if not group_name and not default:
+ raise ValueError("invalid group name: ()")
+ if not GroupDescr.check_name(self, group_name, default=default):
+ raise ScoValueError(
+ f"Le groupe {group_name} existe déjà dans cette partition"
+ )
+ numeros = [g.numero if g.numero is not None else 0 for g in self.groups]
+ if len(numeros) > 0:
+ new_numero = max(numeros) + 1
+ else:
+ new_numero = 0
+ group = GroupDescr(partition=self, group_name=group_name, numero=new_numero)
+ db.session.add(group)
+ db.session.commit()
+ log(f"create_group: created group_id={group.id}")
+ #
+ return group
+
class GroupDescr(db.Model):
"""Description d'un groupe d'une partition"""
diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py
index 8438ba09c7..689dab3475 100644
--- a/app/scodoc/sco_groups.py
+++ b/app/scodoc/sco_groups.py
@@ -34,7 +34,6 @@ Optimisation possible:
"""
import collections
-import operator
import time
from xml.etree import ElementTree
@@ -45,7 +44,7 @@ from flask import g, request
from flask import url_for, make_response
from sqlalchemy.sql import text
-from app import db
+from app import cache, db, log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, Scolog
@@ -53,7 +52,6 @@ from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN
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
from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
@@ -669,7 +667,8 @@ def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
(et le désinscrit d'autres groupes de cette partition)
Return True si changement, False s'il était déjà dans ce groupe.
"""
- if not group.partition.set_etud_group(etudid, group):
+ etud: Identite = Identite.query.get_or_404(etudid)
+ if not group.partition.set_etud_group(etud, group):
return # pas de changement
# - log
@@ -706,7 +705,6 @@ def setGroups(
Ne peux pas modifier les groupes des partitions non éditables.
"""
- from app.scodoc import sco_formsemestre
def xml_error(msg, code=404):
data = (
@@ -716,26 +714,27 @@ def setGroups(
response.headers["Content-Type"] = scu.XML_MIMETYPE
return response
- partition = get_partition(partition_id)
- if not partition["groups_editable"] and (groupsToCreate or groupsToDelete):
+ partition: Partition = Partition.query.get(partition_id)
+ if not partition.groups_editable and (groupsToCreate or groupsToDelete):
msg = "setGroups: partition non editable"
log(msg)
return xml_error(msg, code=403)
- formsemestre_id = partition["formsemestre_id"]
- formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
- if not sco_permissions_check.can_change_groups(formsemestre_id):
+
+ if not sco_permissions_check.can_change_groups(partition.formsemestre.id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
log("***setGroups: partition_id=%s" % partition_id)
log("groupsLists=%s" % groupsLists)
log("groupsToCreate=%s" % groupsToCreate)
log("groupsToDelete=%s" % groupsToDelete)
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- if not sem["etat"]:
+
+ if not partition.formsemestre.etat:
raise AccessDenied("Modification impossible: semestre verrouillé")
groupsToDelete = [g for g in groupsToDelete.split(";") if g]
- etud_groups = formsemestre_get_etud_groupnames(formsemestre_id, attr="group_id")
+ etud_groups = formsemestre_get_etud_groupnames(
+ partition.formsemestre.id, attr="group_id"
+ )
for line in groupsLists.split("\n"): # for each group_id (one per line)
fs = line.split(";")
group_id = fs[0].strip()
@@ -748,15 +747,13 @@ def setGroups(
continue
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])
+ old_members_set = {etud.id for etud in group.etuds}
# Place dans ce groupe les etudiants indiqués:
for etudid_str in fs[1:-1]:
etudid = int(etudid_str)
if etudid in old_members_set:
- old_members_set.remove(
- etudid
- ) # a nouveau dans ce groupe, pas besoin de l'enlever
+ # était dans ce groupe, l'enlever
+ old_members_set.remove(etudid)
if (etudid not in etud_groups) or (
group_id != etud_groups[etudid].get(partition_id, "")
): # pas le meme groupe qu'actuel
@@ -765,7 +762,6 @@ def setGroups(
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
for etudid in old_members_set:
- log("removing %s from group %s" % (etudid, group_id))
ndb.SimpleQuery(
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
{"etudid": etudid, "group_id": group_id},
@@ -775,8 +771,8 @@ def setGroups(
cnx,
method="removeFromGroup",
etudid=etudid,
- msg="formsemestre_id=%s,partition_name=%s, group_name=%s"
- % (formsemestre_id, partition["partition_name"], group["group_name"]),
+ msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={
+ partition.partition_name}, group_name={group.group_name}""",
)
# Supprime les groupes indiqués comme supprimés:
@@ -799,7 +795,7 @@ def setGroups(
change_etud_group_in_partition(etudid, group.id)
# Update parcours
- formsemestre.update_inscriptions_parcours_from_groups()
+ partition.formsemestre.update_inscriptions_parcours_from_groups()
data = (
'
Semestre {formsemestre.titre_annee()}
", - """Les groupes existants seront effacés et remplacés par + f"""
Semestre {formsemestre.titre_annee()}
", +Les groupes existants seront effacés et remplacés par ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau des groupes (en utilisant la dernière moyenne générale disponible pour - chaque étudiant) et de maximiser la mixité de chaque groupe.
""", + chaque étudiant) et de maximiser la mixité de chaque groupe. + + """, ] tf = TrivialFormulator( @@ -1429,23 +1428,24 @@ def groups_auto_repartition(partition_id=None): # form submission log( "groups_auto_repartition( partition_id=%s partition_name=%s" - % (partition_id, partition["partition_name"]) + % (partition_id, partition.partition_name) ) groupNames = tf[2]["groupNames"] - group_names = sorted(set([x.strip() for x in groupNames.split(",")])) + group_names = sorted({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_id"]) + for group in partition.groups: + db.session.delete(group) + db.session.commit() # Crée les nouveaux groupes groups = [] for group_name in group_names: if group_name.strip(): - groups.append(create_group(partition_id, group_name)) + groups.append(partition.create_group(group_name)) # nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) identdict = nt.identdict # build: { civilite : liste etudids trie par niveau croissant } - civilites = set([x["civilite"] for x in identdict.values()]) + civilites = {x["civilite"] for x in identdict.values()} listes = {} for civilite in civilites: listes[civilite] = [ @@ -1460,14 +1460,17 @@ def groups_auto_repartition(partition_id=None): igroup = 0 nbgroups = len(groups) while n > 0: + log(f"n={n}") for civilite in civilites: + log(f"civilite={civilite}") if len(listes[civilite]): n -= 1 etudid = listes[civilite].pop()[1] group = groups[igroup] igroup = (igroup + 1) % nbgroups + log(f"in {etudid} in group {group.id}") change_etud_group_in_partition(etudid, group) - log("%s in group %s" % (etudid, group.id)) + log(f"{etudid} in group {group.id}") return flask.redirect(dest_url) @@ -1475,8 +1478,6 @@ def _get_prev_moy(etudid, formsemestre_id): """Donne la derniere moyenne generale calculee pour cette étudiant, ou 0 si on n'en trouve pas (nouvel inscrit,...). """ - from app.scodoc import sco_cursus_dut - info = sco_etud.get_etud_info(etudid=etudid, filled=True) if not info: raise ScoValueError("etudiant invalide: etudid=%s" % etudid) diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py index 224881bf87..dcc19f455c 100644 --- a/app/scodoc/sco_permissions_check.py +++ b/app/scodoc/sco_permissions_check.py @@ -142,7 +142,9 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl def can_change_groups(formsemestre_id: int) -> bool: - "Vrai si l'utilisateur peut changer les groupes dans ce semestre" + """Vrai si l'utilisateur peut changer les groupes dans ce semestre + Obsolete: utiliser FormSemestre.can_change_groups + """ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.etat: return False # semestre verrouillé