Améliore code gestion des groupes et corrige qq bugs

This commit is contained in:
Emmanuel Viennet 2023-07-08 16:35:32 +02:00
parent d88e41b83e
commit 8b37f661d6
4 changed files with 101 additions and 59 deletions

View File

@ -573,6 +573,17 @@ class FormSemestre(db.Model):
user 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): def can_edit_jury(self, user: User = None):
"""Vrai si utilisateur (par def. current) peut saisir decision de jury """Vrai si utilisateur (par def. current) peut saisir decision de jury
dans ce semestre: vérifie permission et verrouillage. dans ce semestre: vérifie permission et verrouillage.

View File

@ -10,11 +10,11 @@
from operator import attrgetter from operator import attrgetter
from sqlalchemy.exc import IntegrityError 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 SHORT_STR_LEN
from app.models import GROUPNAME_STR_LEN from app.models import GROUPNAME_STR_LEN
from app.scodoc import sco_utils as scu 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): class Partition(db.Model):
@ -119,7 +119,7 @@ class Partition(db.Model):
.first() .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. """Affect etudid to group_id in given partition.
Raises IntegrityError si conflit, Raises IntegrityError si conflit,
or ValueError si ce group_id n'est pas dans cette partition 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): if not group.id in (g.id for g in self.groups):
raise ScoValueError( 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( 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: try:
existing_row = ( existing_row = (
db.session.query(group_membership) db.session.query(group_membership)
.filter_by(etudid=etudid) .filter_by(etudid=etud.id)
.join(GroupDescr) .join(GroupDescr)
.filter_by(partition_id=self.id) .filter_by(partition_id=self.id)
.first() .first()
) )
existing_group_id = existing_row[1]
if existing_row: if existing_row:
existing_group_id = existing_row[1]
if group.id == existing_group_id: if group.id == existing_group_id:
return False return False
update_row = ( # Fait le changement avec l'ORM sinon risque élevé de blocage
group_membership.update() existing_group = GroupDescr.query.get(existing_group_id)
.where( db.session.commit()
group_membership.c.etudid == etudid, group.etuds.append(etud)
group_membership.c.group_id == existing_group_id, existing_group.etuds.remove(etud)
) db.session.add(etud)
.values(group_id=group.id) db.session.add(existing_group)
) db.session.add(group)
db.session.execute(update_row)
else: else:
new_row = group_membership.insert().values( 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.execute(new_row)
db.session.commit() db.session.commit()
@ -166,6 +167,33 @@ class Partition(db.Model):
raise raise
return True 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): class GroupDescr(db.Model):
"""Description d'un groupe d'une partition""" """Description d'un groupe d'une partition"""

View File

@ -34,7 +34,6 @@ Optimisation possible:
""" """
import collections import collections
import operator
import time import time
from xml.etree import ElementTree from xml.etree import ElementTree
@ -45,7 +44,7 @@ from flask import g, request
from flask import url_for, make_response from flask import url_for, make_response
from sqlalchemy.sql import text from sqlalchemy.sql import text
from app import db from app import cache, db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, Scolog 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 from app.models.groups import GroupDescr, Partition, group_membership
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log, cache
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache 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) (et le désinscrit d'autres groupes de cette partition)
Return True si changement, False s'il était déjà dans ce groupe. 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 return # pas de changement
# - log # - log
@ -706,7 +705,6 @@ def setGroups(
Ne peux pas modifier les groupes des partitions non éditables. Ne peux pas modifier les groupes des partitions non éditables.
""" """
from app.scodoc import sco_formsemestre
def xml_error(msg, code=404): def xml_error(msg, code=404):
data = ( data = (
@ -716,26 +714,27 @@ def setGroups(
response.headers["Content-Type"] = scu.XML_MIMETYPE response.headers["Content-Type"] = scu.XML_MIMETYPE
return response return response
partition = get_partition(partition_id) partition: Partition = Partition.query.get(partition_id)
if not partition["groups_editable"] and (groupsToCreate or groupsToDelete): if not partition.groups_editable and (groupsToCreate or groupsToDelete):
msg = "setGroups: partition non editable" msg = "setGroups: partition non editable"
log(msg) log(msg)
return xml_error(msg, code=403) 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(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 !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
log("***setGroups: partition_id=%s" % partition_id) log("***setGroups: partition_id=%s" % partition_id)
log("groupsLists=%s" % groupsLists) log("groupsLists=%s" % groupsLists)
log("groupsToCreate=%s" % groupsToCreate) log("groupsToCreate=%s" % groupsToCreate)
log("groupsToDelete=%s" % groupsToDelete) 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é") raise AccessDenied("Modification impossible: semestre verrouillé")
groupsToDelete = [g for g in groupsToDelete.split(";") if g] 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) for line in groupsLists.split("\n"): # for each group_id (one per line)
fs = line.split(";") fs = line.split(";")
group_id = fs[0].strip() group_id = fs[0].strip()
@ -748,15 +747,13 @@ def setGroups(
continue continue
group: GroupDescr = GroupDescr.query.get(group_id) group: GroupDescr = GroupDescr.query.get(group_id)
# Anciens membres du groupe: # Anciens membres du groupe:
old_members = get_group_members(group_id) old_members_set = {etud.id for etud in group.etuds}
old_members_set = set([x["etudid"] for x in old_members])
# Place dans ce groupe les etudiants indiqués: # Place dans ce groupe les etudiants indiqués:
for etudid_str in fs[1:-1]: for etudid_str in fs[1:-1]:
etudid = int(etudid_str) etudid = int(etudid_str)
if etudid in old_members_set: if etudid in old_members_set:
old_members_set.remove( # était dans ce groupe, l'enlever
etudid old_members_set.remove(etudid)
) # a nouveau dans ce groupe, pas besoin de l'enlever
if (etudid not in etud_groups) or ( if (etudid not in etud_groups) or (
group_id != etud_groups[etudid].get(partition_id, "") group_id != etud_groups[etudid].get(partition_id, "")
): # pas le meme groupe qu'actuel ): # pas le meme groupe qu'actuel
@ -765,7 +762,6 @@ def setGroups(
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
for etudid in old_members_set: for etudid in old_members_set:
log("removing %s from group %s" % (etudid, group_id))
ndb.SimpleQuery( ndb.SimpleQuery(
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s", "DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
{"etudid": etudid, "group_id": group_id}, {"etudid": etudid, "group_id": group_id},
@ -775,8 +771,8 @@ def setGroups(
cnx, cnx,
method="removeFromGroup", method="removeFromGroup",
etudid=etudid, etudid=etudid,
msg="formsemestre_id=%s,partition_name=%s, group_name=%s" msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={
% (formsemestre_id, partition["partition_name"], group["group_name"]), partition.partition_name}, group_name={group.group_name}""",
) )
# Supprime les groupes indiqués comme supprimés: # Supprime les groupes indiqués comme supprimés:
@ -799,7 +795,7 @@ def setGroups(
change_etud_group_in_partition(etudid, group.id) change_etud_group_in_partition(etudid, group.id)
# Update parcours # Update parcours
formsemestre.update_inscriptions_parcours_from_groups() partition.formsemestre.update_inscriptions_parcours_from_groups()
data = ( data = (
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>' '<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
@ -812,6 +808,7 @@ def setGroups(
def create_group(partition_id, group_name="", default=False) -> GroupDescr: def create_group(partition_id, group_name="", default=False) -> GroupDescr:
"""Create a new group in this partition. """Create a new group in this partition.
If default, create default partition (with no name) If default, create default partition (with no name)
Obsolete: utiliser Partition.create_group
""" """
partition = Partition.query.get_or_404(partition_id) partition = Partition.query.get_or_404(partition_id)
if not sco_permissions_check.can_change_groups(partition.formsemestre_id): if not sco_permissions_check.can_change_groups(partition.formsemestre_id):
@ -833,7 +830,7 @@ def create_group(partition_id, group_name="", default=False) -> GroupDescr:
group = GroupDescr(partition=partition, group_name=group_name, numero=new_numero) group = GroupDescr(partition=partition, group_name=group_name, numero=new_numero)
db.session.add(group) db.session.add(group)
db.session.commit() db.session.commit()
log("create_group: created group_id={group.id}") log(f"create_group: created group_id={group.id}")
# #
return group return group
@ -1377,11 +1374,11 @@ def groups_auto_repartition(partition_id=None):
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau """Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité. et la mixité.
""" """
partition = get_partition(partition_id) partition: Partition = Partition.query.get_or_404(partition_id)
if not partition["groups_editable"]: if not partition.groups_editable:
raise AccessDenied("Partition non éditable") raise AccessDenied("Partition non éditable")
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = partition.formsemestre
# renvoie sur page édition groupes # renvoie sur page édition groupes
dest_url = url_for( dest_url = url_for(
"scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id "scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
@ -1404,12 +1401,14 @@ def groups_auto_repartition(partition_id=None):
H = [ H = [
html_sco_header.sco_header(page_title="Répartition des groupes"), html_sco_header.sco_header(page_title="Répartition des groupes"),
"<h2>Répartition des groupes de %s</h2>" % partition["partition_name"], f"""<h2>Répartition des groupes de {partition.partition_name}</h2>
f"<p>Semestre {formsemestre.titre_annee()}</p>", <p>Semestre {formsemestre.titre_annee()}</p>",
"""<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par <p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau 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 des groupes (en utilisant la dernière moyenne générale disponible pour
chaque étudiant) et de maximiser la mixité de chaque groupe.</p>""", chaque étudiant) et de maximiser la mixité de chaque groupe.
</p>
""",
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
@ -1429,23 +1428,24 @@ def groups_auto_repartition(partition_id=None):
# form submission # form submission
log( log(
"groups_auto_repartition( partition_id=%s partition_name=%s" "groups_auto_repartition( partition_id=%s partition_name=%s"
% (partition_id, partition["partition_name"]) % (partition_id, partition.partition_name)
) )
groupNames = tf[2]["groupNames"] 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 # Détruit les groupes existant de cette partition
for old_group in get_partition_groups(partition): for group in partition.groups:
group_delete(old_group["group_id"]) db.session.delete(group)
db.session.commit()
# Crée les nouveaux groupes # Crée les nouveaux groupes
groups = [] groups = []
for group_name in group_names: for group_name in group_names:
if group_name.strip(): 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) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
identdict = nt.identdict identdict = nt.identdict
# build: { civilite : liste etudids trie par niveau croissant } # 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 = {} listes = {}
for civilite in civilites: for civilite in civilites:
listes[civilite] = [ listes[civilite] = [
@ -1460,14 +1460,17 @@ def groups_auto_repartition(partition_id=None):
igroup = 0 igroup = 0
nbgroups = len(groups) nbgroups = len(groups)
while n > 0: while n > 0:
log(f"n={n}")
for civilite in civilites: for civilite in civilites:
log(f"civilite={civilite}")
if len(listes[civilite]): if len(listes[civilite]):
n -= 1 n -= 1
etudid = listes[civilite].pop()[1] etudid = listes[civilite].pop()[1]
group = groups[igroup] group = groups[igroup]
igroup = (igroup + 1) % nbgroups igroup = (igroup + 1) % nbgroups
log(f"in {etudid} in group {group.id}")
change_etud_group_in_partition(etudid, group) 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) 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, """Donne la derniere moyenne generale calculee pour cette étudiant,
ou 0 si on n'en trouve pas (nouvel inscrit,...). 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) info = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not info: if not info:
raise ScoValueError("etudiant invalide: etudid=%s" % etudid) raise ScoValueError("etudiant invalide: etudid=%s" % etudid)

View File

@ -142,7 +142,9 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
def can_change_groups(formsemestre_id: int) -> bool: 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) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.etat: if not formsemestre.etat:
return False # semestre verrouillé return False # semestre verrouillé