forked from ScoDoc/ScoDoc
1687 lines
60 KiB
Python
1687 lines
60 KiB
Python
# -*- mode: python -*-
|
|
# -*- coding: utf-8 -*-
|
|
|
|
##############################################################################
|
|
#
|
|
# Gestion scolarite IUT
|
|
#
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
# Emmanuel Viennet emmanuel.viennet@gmail.com
|
|
#
|
|
##############################################################################
|
|
|
|
"""Gestion des groupes, nouvelle mouture (juin/nov 2009)
|
|
"""
|
|
import collections
|
|
import time
|
|
|
|
from xml.etree import ElementTree
|
|
from xml.etree.ElementTree import Element
|
|
|
|
import flask
|
|
from flask import g, request
|
|
from flask import url_for, make_response
|
|
from sqlalchemy.sql import text
|
|
|
|
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
|
|
from app.models import SHORT_STR_LEN
|
|
from app.models.groups import GroupDescr, Partition
|
|
import app.scodoc.sco_utils as scu
|
|
import app.scodoc.notesdb as ndb
|
|
from app.scodoc.scolog import logdb
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_cache
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_cursus
|
|
from app.scodoc import sco_etud
|
|
from app.scodoc.sco_etud import etud_sort_key
|
|
from app.scodoc import sco_xml
|
|
from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
|
|
|
|
|
partitionEditor = ndb.EditableTable(
|
|
"partition",
|
|
"partition_id",
|
|
(
|
|
"partition_id",
|
|
"formsemestre_id",
|
|
"partition_name",
|
|
"compute_ranks",
|
|
"numero",
|
|
"bul_show_rank",
|
|
"show_in_lists",
|
|
"editable",
|
|
),
|
|
input_formators={
|
|
"bul_show_rank": bool,
|
|
"show_in_lists": bool,
|
|
"editable": bool,
|
|
},
|
|
)
|
|
|
|
groupEditor = ndb.EditableTable(
|
|
"group_descr",
|
|
"group_id",
|
|
("group_id", "partition_id", "group_name", "numero", "edt_id"),
|
|
)
|
|
|
|
group_list = groupEditor.list
|
|
|
|
|
|
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.*
|
|
FROM group_descr gd, partition p
|
|
WHERE gd.id=%(group_id)s
|
|
AND p.id = gd.partition_id
|
|
""",
|
|
{"group_id": group_id},
|
|
)
|
|
if not r:
|
|
raise ScoValueError(f"Groupe inexistant ! (id {group_id})")
|
|
return r[0]
|
|
|
|
|
|
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_id": group_id},
|
|
)
|
|
# delete group:
|
|
ndb.SimpleQuery(
|
|
"DELETE FROM group_descr WHERE id=%(group_id)s", {"group_id": group_id}
|
|
)
|
|
|
|
|
|
def get_partition(partition_id): # OBSOLETE
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT p.id AS partition_id, p.*
|
|
FROM partition p
|
|
WHERE p.id = %(partition_id)s
|
|
""",
|
|
{"partition_id": partition_id},
|
|
)
|
|
if not r:
|
|
raise ScoValueError(f"Partition inconnue (déjà supprimée ?) ({partition_id})")
|
|
return r[0]
|
|
|
|
|
|
def get_partitions_list(formsemestre_id, with_default=True) -> list[dict]:
|
|
"""Liste des partitions pour ce semestre (list of dicts),
|
|
triées par numéro, avec la partition par défaut en fin de liste.
|
|
OBSOLETE: utiliser FormSemestre.get_partitions_list
|
|
"""
|
|
partitions = ndb.SimpleDictFetch(
|
|
"""SELECT p.id AS partition_id, p.*
|
|
FROM partition p
|
|
WHERE formsemestre_id=%(formsemestre_id)s
|
|
ORDER BY numero""",
|
|
{"formsemestre_id": formsemestre_id},
|
|
)
|
|
# Move 'all' at end of list (for menus)
|
|
R = [p for p in partitions if p["partition_name"] is not None]
|
|
if with_default:
|
|
R += [p for p in partitions if p["partition_name"] is None]
|
|
return R
|
|
|
|
|
|
def get_default_partition(formsemestre_id):
|
|
"""Get partition for 'all' students (this one always exists, with NULL name)"""
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT p.id AS partition_id, p.* FROM partition p
|
|
WHERE formsemestre_id=%(formsemestre_id)s
|
|
AND partition_name is NULL
|
|
""",
|
|
{"formsemestre_id": formsemestre_id},
|
|
)
|
|
if len(r) != 1:
|
|
raise ScoException(
|
|
"inconsistent partition: %d with NULL name for formsemestre_id=%s"
|
|
% (len(r), formsemestre_id)
|
|
)
|
|
return r[0]
|
|
|
|
|
|
def get_formsemestre_groups(formsemestre_id, with_default=False):
|
|
"""Returns ( partitions, { partition_id : { etudid : group } } )."""
|
|
partitions = get_partitions_list(formsemestre_id, with_default=with_default)
|
|
partitions_etud_groups = {} # { partition_id : { etudid : group } }
|
|
for partition in partitions:
|
|
pid = partition["partition_id"]
|
|
partitions_etud_groups[pid] = get_etud_groups_in_partition(pid)
|
|
return partitions, partitions_etud_groups
|
|
|
|
|
|
def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict:
|
|
"""{ etudid : { partition_id : group_id } }"""
|
|
infos = ndb.SimpleDictFetch(
|
|
"""SELECT etudid, p.id AS partition_id, gd.id AS group_id
|
|
FROM group_descr gd, group_membership gm, partition p
|
|
WHERE gd.partition_id = p.id
|
|
AND gm.group_id = gd.id
|
|
AND p.formsemestre_id = %(formsemestre_id)s
|
|
""",
|
|
{"formsemestre_id": formsemestre_id},
|
|
)
|
|
# -> {'etudid': 16483, 'group_id': 5317, 'partition_id': 2264},
|
|
d = collections.defaultdict(lambda: {})
|
|
for i in infos:
|
|
d[i["etudid"]][i["partition_id"]] = i["group_id"]
|
|
return d
|
|
|
|
|
|
def get_partition_groups(partition): # OBSOLETE !
|
|
"""List of groups in this partition (list of dicts).
|
|
Some groups may be empty."""
|
|
return ndb.SimpleDictFetch(
|
|
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
|
|
FROM group_descr gd, partition p
|
|
WHERE gd.partition_id=%(partition_id)s
|
|
AND gd.partition_id=p.id
|
|
ORDER BY gd.numero
|
|
""",
|
|
partition,
|
|
)
|
|
|
|
|
|
def get_default_group(formsemestre_id, fix_if_missing=False) -> int:
|
|
"""Returns group_id for default ('tous') group
|
|
XXX remplacé par formsemestre.get_default_group
|
|
"""
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT gd.id AS group_id
|
|
FROM group_descr gd, partition p
|
|
WHERE p.formsemestre_id=%(formsemestre_id)s
|
|
AND p.partition_name is NULL
|
|
AND p.id = gd.partition_id
|
|
""",
|
|
{"formsemestre_id": formsemestre_id},
|
|
)
|
|
if len(r) == 0 and fix_if_missing:
|
|
# No default group (problem during sem creation)
|
|
# Try to create it
|
|
log(
|
|
f"""*** Warning: get_default_group(formsemestre_id={formsemestre_id
|
|
}): default group missing, recreating it"""
|
|
)
|
|
try:
|
|
partition_id = get_default_partition(formsemestre_id)["partition_id"]
|
|
except ScoException:
|
|
log(f"creating default partition for {formsemestre_id}")
|
|
partition_id = partition_create(
|
|
formsemestre_id, default=True, redirect=False
|
|
)
|
|
group = create_group(partition_id, default=True)
|
|
return group.id
|
|
# debug check
|
|
if len(r) != 1:
|
|
log(f"invalid group structure for {formsemestre_id}: {len(r)}")
|
|
group_id = r[0]["group_id"]
|
|
return group_id
|
|
|
|
|
|
def get_sem_groups(formsemestre_id):
|
|
"""Returns groups for this sem (in all partitions)."""
|
|
return ndb.SimpleDictFetch(
|
|
"""SELECT gd.id AS group_id, p.id AS partition_id, gd.*, p.*
|
|
FROM group_descr gd, partition p
|
|
WHERE p.formsemestre_id=%(formsemestre_id)s
|
|
AND p.id = gd.partition_id
|
|
""",
|
|
{"formsemestre_id": formsemestre_id},
|
|
)
|
|
|
|
|
|
def get_group_members(group_id, etat=None):
|
|
"""Liste des etudiants d'un groupe.
|
|
Si etat, filtre selon l'état de l'inscription
|
|
Trié par nom_usuel (ou nom) puis prénom
|
|
"""
|
|
req = """SELECT i.id as etudid, i.*, a.*, gm.*, ins.etat
|
|
FROM identite i, adresse a, group_membership gm,
|
|
group_descr gd, partition p, notes_formsemestre_inscription ins
|
|
WHERE i.id = gm.etudid
|
|
and a.etudid = i.id
|
|
and ins.etudid = i.id
|
|
and ins.formsemestre_id = p.formsemestre_id
|
|
and p.id = gd.partition_id
|
|
and gd.id = gm.group_id
|
|
and gm.group_id=%(group_id)s
|
|
"""
|
|
if etat is not None:
|
|
req += " and ins.etat = %(etat)s"
|
|
|
|
r = ndb.SimpleDictFetch(req, {"group_id": group_id, "etat": etat})
|
|
|
|
for etud in r:
|
|
sco_etud.format_etud_ident(etud)
|
|
|
|
# tri selon nom_usuel ou nom, sans accents
|
|
r.sort(key=etud_sort_key)
|
|
|
|
if scu.CONFIG.ALLOW_NULL_PRENOM:
|
|
for x in r:
|
|
x["prenom"] = x["prenom"] or ""
|
|
|
|
return r
|
|
|
|
|
|
def get_group_infos(group_id, etat: str | None = None): # was _getlisteetud
|
|
"""legacy code: used by group_list and trombino.
|
|
etat: état de l'inscription."""
|
|
from app.scodoc import sco_formsemestre
|
|
|
|
cnx = ndb.GetDBConnexion()
|
|
group = get_group(group_id)
|
|
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
|
|
|
|
members = get_group_members(group_id, etat=etat)
|
|
# add human readable description of state:
|
|
nbdem = 0
|
|
for t in members:
|
|
if t["etat"] == scu.INSCRIT:
|
|
t["etath"] = "" # etudiant inscrit, ne l'indique pas dans la liste HTML
|
|
elif t["etat"] == scu.DEMISSION:
|
|
events = sco_etud.scolar_events_list(
|
|
cnx,
|
|
args={
|
|
"etudid": t["etudid"],
|
|
"formsemestre_id": group["formsemestre_id"],
|
|
},
|
|
)
|
|
for event in events:
|
|
event_type = event["event_type"]
|
|
if event_type == "DEMISSION":
|
|
t["date_dem"] = event["event_date"]
|
|
break
|
|
if "date_dem" in t:
|
|
t["etath"] = "démission le %s" % t["date_dem"]
|
|
else:
|
|
t["etath"] = "(dem.)"
|
|
nbdem += 1
|
|
elif t["etat"] == codes_cursus.DEF:
|
|
t["etath"] = "Défaillant"
|
|
else:
|
|
t["etath"] = t["etat"]
|
|
# Add membership for all partitions, 'partition_id' : group
|
|
for etud in members: # long: comment eviter ces boucles ?
|
|
etud_add_group_infos(etud, sem["formsemestre_id"])
|
|
|
|
if group["partition_name"] is None:
|
|
group_tit = "tous"
|
|
else:
|
|
group_tit = f"""{group["partition_name"]} {group["group_name"]}"""
|
|
|
|
return members, group, group_tit, sem, nbdem
|
|
|
|
|
|
def get_group_other_partitions(group):
|
|
"""Liste des partitions du même semestre que ce groupe,
|
|
sans celle qui contient ce groupe.
|
|
"""
|
|
other_partitions = [
|
|
p
|
|
for p in get_partitions_list(group["formsemestre_id"])
|
|
if p["partition_id"] != group["partition_id"] and p["partition_name"]
|
|
]
|
|
return other_partitions
|
|
|
|
|
|
def get_etud_groups(etudid: int, formsemestre_id: int, exclude_default=False):
|
|
"""Infos sur groupes de l'etudiant dans ce semestre
|
|
[ group + partition_name ]
|
|
"""
|
|
req = """SELECT p.id AS partition_id, p.*,
|
|
g.id AS group_id, g.numero as group_numero, g.group_name
|
|
FROM group_descr g, partition p, group_membership gm
|
|
WHERE gm.etudid=%(etudid)s
|
|
and gm.group_id = g.id
|
|
and g.partition_id = p.id
|
|
and p.formsemestre_id = %(formsemestre_id)s
|
|
"""
|
|
if exclude_default:
|
|
req += " and p.partition_name is not NULL"
|
|
groups = ndb.SimpleDictFetch(
|
|
req + " ORDER BY p.numero",
|
|
{"etudid": etudid, "formsemestre_id": formsemestre_id},
|
|
)
|
|
return _sortgroups(groups)
|
|
|
|
|
|
def get_etud_main_group(etudid: int, formsemestre_id: int):
|
|
"""Return main group (the first one) for etud, or default one if no groups"""
|
|
groups = get_etud_groups(etudid, formsemestre_id, exclude_default=True)
|
|
if groups:
|
|
return groups[0]
|
|
else:
|
|
return get_group(get_default_group(formsemestre_id))
|
|
|
|
|
|
def formsemestre_get_main_partition(formsemestre_id):
|
|
"""Return main partition (the first one) for sem, or default one if no groups
|
|
(rappel: default == tous, main == principale (groupes TD habituellement)
|
|
"""
|
|
return get_partitions_list(formsemestre_id, with_default=True)[0]
|
|
|
|
|
|
def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
|
|
"""Recupere les groupes de tous les etudiants d'un semestre
|
|
{ etudid : { partition_id : group_name }} (attr=group_name or group_id)
|
|
"""
|
|
infos = ndb.SimpleDictFetch(
|
|
"""SELECT
|
|
i.etudid AS etudid,
|
|
p.id AS partition_id,
|
|
gd.group_name,
|
|
gd.id AS group_id
|
|
FROM
|
|
notes_formsemestre_inscription i,
|
|
partition p,
|
|
group_descr gd,
|
|
group_membership gm
|
|
WHERE
|
|
i.formsemestre_id=%(formsemestre_id)s
|
|
and i.formsemestre_id = p.formsemestre_id
|
|
and p.id = gd.partition_id
|
|
and gm.etudid = i.etudid
|
|
and gm.group_id = gd.id
|
|
and p.partition_name is not NULL
|
|
""",
|
|
{"formsemestre_id": formsemestre_id},
|
|
)
|
|
R = {}
|
|
for info in infos:
|
|
if info["etudid"] in R:
|
|
R[info["etudid"]][info["partition_id"]] = info[attr]
|
|
else:
|
|
R[info["etudid"]] = {info["partition_id"]: info[attr]}
|
|
return R
|
|
|
|
|
|
def get_etud_formsemestre_groups(
|
|
etud: Identite, formsemestre: FormSemestre, only_to_show=True
|
|
) -> list[GroupDescr]:
|
|
"""Liste les groupes auxquels est inscrit.
|
|
Si only_to_show (défaut vrai), ne donne que les groupes "visiables",
|
|
c'est à dire des partitions avec show_in_lists True.
|
|
"""
|
|
# Note: je n'ai pas réussi à construire une requete SQLAlechemy avec
|
|
# la Table d'association group_membership
|
|
cursor = db.session.execute(
|
|
text(
|
|
"""
|
|
SELECT g.id
|
|
FROM group_descr g, group_membership gm, partition p
|
|
WHERE gm.etudid = :etudid
|
|
AND gm.group_id = g.id
|
|
AND g.partition_id = p.id
|
|
AND p.formsemestre_id = :formsemestre_id
|
|
AND p.partition_name is not NULL
|
|
"""
|
|
+ (" and (p.show_in_lists is True) " if only_to_show else "")
|
|
+ """
|
|
ORDER BY p.numero
|
|
"""
|
|
),
|
|
{"etudid": etud.id, "formsemestre_id": formsemestre.id},
|
|
)
|
|
return [db.session.get(GroupDescr, group_id) for group_id in cursor]
|
|
|
|
|
|
# Ancienne fonction:
|
|
def etud_add_group_infos(
|
|
etud, formsemestre_id, sep=" ", only_to_show=False, with_default_partition=True
|
|
):
|
|
"""Add informations on partitions and group memberships to etud
|
|
(a dict with an etudid)
|
|
If only_to_show, restrict to partions such that show_in_lists is True.
|
|
If with_default_partition, does not discard partition whose name is None
|
|
etud['partitions'] = { partition_id : group + partition_name }
|
|
etud['groupes'] = "TDB, Gr2, TPB1"
|
|
etud['partitionsgroupes'] = "Groupes TD:TDB, Groupes TP:Gr2 (...)"
|
|
"""
|
|
etud[
|
|
"partitions"
|
|
] = collections.OrderedDict() # partition_id : group + partition_name
|
|
if not formsemestre_id:
|
|
etud["groupes"] = ""
|
|
return etud
|
|
infos = ndb.SimpleDictFetch(
|
|
"""SELECT p.partition_name, p.show_in_lists, g.*, g.id AS group_id
|
|
FROM group_descr g, partition p, group_membership gm WHERE gm.etudid=%(etudid)s
|
|
and gm.group_id = g.id
|
|
and g.partition_id = p.id
|
|
and p.formsemestre_id = %(formsemestre_id)s
|
|
"""
|
|
+ (" and (p.show_in_lists is True) " if only_to_show else "")
|
|
+ (" and (p.partition_name is not null) " if not with_default_partition else "")
|
|
+ """
|
|
ORDER BY p.numero
|
|
""",
|
|
{"etudid": etud["etudid"], "formsemestre_id": formsemestre_id},
|
|
)
|
|
|
|
for info in infos:
|
|
if info["partition_name"]:
|
|
etud["partitions"][info["partition_id"]] = info
|
|
|
|
# resume textuel des groupes:
|
|
etud["groupes"] = sep.join(
|
|
[gr["group_name"] for gr in infos if gr["group_name"] is not None]
|
|
)
|
|
etud["partitionsgroupes"] = sep.join(
|
|
[
|
|
(gr["partition_name"] or "") + ":" + gr["group_name"]
|
|
for gr in infos
|
|
if gr["group_name"] is not None
|
|
]
|
|
)
|
|
|
|
return etud
|
|
|
|
|
|
@cache.memoize(timeout=50) # seconds
|
|
def get_etud_groups_in_partition(partition_id):
|
|
"""Returns { etudid : group }, with all students in this partition"""
|
|
infos = ndb.SimpleDictFetch(
|
|
"""SELECT gd.id AS group_id, gd.*, etudid
|
|
FROM group_descr gd, group_membership gm
|
|
WHERE gd.partition_id = %(partition_id)s
|
|
AND gm.group_id = gd.id
|
|
""",
|
|
{"partition_id": partition_id},
|
|
)
|
|
R = {}
|
|
for i in infos:
|
|
R[i["etudid"]] = i
|
|
return R
|
|
|
|
|
|
def formsemestre_partition_list(formsemestre_id, fmt="xml"):
|
|
"""Get partitions and groups in this semestre
|
|
Supported formats: xml, json
|
|
"""
|
|
partitions = get_partitions_list(formsemestre_id, with_default=True)
|
|
# Ajoute les groupes
|
|
for p in partitions:
|
|
p["group"] = get_partition_groups(p)
|
|
return scu.sendResult(partitions, name="partition", fmt=fmt)
|
|
|
|
|
|
# Encore utilisé par groupmgr.js
|
|
def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
|
"""
|
|
Deprecated: use group_list
|
|
Liste des étudiants dans chaque groupe de cette partition.
|
|
<group partition_id="" partition_name="" group_id="" group_name="" groups_editable="">
|
|
<etud etuid="" sexe="" nom="" prenom="" civilite="" origin=""/>
|
|
</group>
|
|
<group ...>
|
|
...
|
|
"""
|
|
t0 = time.time()
|
|
partition = get_partition(partition_id)
|
|
formsemestre_id = partition["formsemestre_id"]
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
etuds_set = {ins.etudid for ins in formsemestre.inscriptions}
|
|
|
|
groups = get_partition_groups(partition)
|
|
# Build XML:
|
|
t1 = time.time()
|
|
doc = Element("ajax-response")
|
|
x_response = Element("response", type="object", id="MyUpdater")
|
|
doc.append(x_response)
|
|
for group in groups:
|
|
x_group = Element(
|
|
"group",
|
|
partition_id=str(partition_id),
|
|
partition_name=partition["partition_name"] or "",
|
|
groups_editable=str(int(partition["groups_editable"])),
|
|
group_id=str(group["group_id"]),
|
|
group_name=group["group_name"] or "",
|
|
)
|
|
x_response.append(x_group)
|
|
for e in get_group_members(group["group_id"]):
|
|
etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0]
|
|
x_group.append(
|
|
Element(
|
|
"etud",
|
|
etudid=str(e["etudid"]),
|
|
civilite=etud["civilite_str"] or "",
|
|
sexe=etud["civilite_str"] or "", # compat
|
|
nom=sco_etud.format_nom(etud["nom"] or ""),
|
|
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
|
origin=_comp_etud_origin(etud, formsemestre),
|
|
)
|
|
)
|
|
if e["etudid"] in etuds_set:
|
|
etuds_set.remove(e["etudid"]) # etudiant vu dans un groupe
|
|
|
|
# Ajoute les etudiants inscrits au semestre mais dans aucun groupe de cette partition:
|
|
if etuds_set:
|
|
x_group = Element(
|
|
"group",
|
|
partition_id=str(partition_id),
|
|
partition_name=partition["partition_name"] or "",
|
|
groups_editable=str(int(partition["groups_editable"])),
|
|
group_id="_none_",
|
|
group_name="",
|
|
)
|
|
doc.append(x_group)
|
|
for etudid in etuds_set:
|
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
x_group.append(
|
|
Element(
|
|
"etud",
|
|
etudid=str(etud["etudid"]),
|
|
sexe=etud["civilite_str"] or "",
|
|
nom=sco_etud.format_nom(etud["nom"] or ""),
|
|
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
|
origin=_comp_etud_origin(etud, formsemestre),
|
|
)
|
|
)
|
|
t2 = time.time()
|
|
log(f"XMLgetGroupsInPartition: {t2-t0} seconds ({t1-t0}+{t2-t1})")
|
|
# XML response:
|
|
data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
|
response = make_response(data)
|
|
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
|
return response
|
|
|
|
|
|
def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre):
|
|
"""breve description de l'origine de l'étudiant (sem. precedent)
|
|
(n'indique l'origine que si ce n'est pas le semestre precedent normal)
|
|
"""
|
|
# cherche le semestre suivant le sem. courant dans la liste
|
|
cur_sem_idx = None
|
|
for i in range(len(etud["sems"])):
|
|
if etud["sems"][i]["formsemestre_id"] == cur_formsemestre.id:
|
|
cur_sem_idx = i
|
|
break
|
|
|
|
if cur_sem_idx is None or (cur_sem_idx + 1) >= (len(etud["sems"]) - 1):
|
|
return "" # on pourrait indiquer le bac mais en general on ne l'a pas en debut d'annee
|
|
|
|
prev_sem = etud["sems"][cur_sem_idx + 1]
|
|
if prev_sem["semestre_id"] != (cur_formsemestre.semestre_id - 1):
|
|
return f" (S{prev_sem['semestre_id']})"
|
|
else:
|
|
return "" # parcours normal, ne le signale pas
|
|
|
|
|
|
def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE !
|
|
"""Inscrit l'étudiant au groupe.
|
|
Return True if ok, False si deja inscrit.
|
|
Warning:
|
|
- don't check if group_id exists (the caller should check).
|
|
- don't check if group's partition is editable
|
|
"""
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
args = {"etudid": etudid, "group_id": group_id}
|
|
# déjà inscrit ?
|
|
r = ndb.SimpleDictFetch(
|
|
"SELECT * FROM group_membership gm WHERE etudid=%(etudid)s and group_id=%(group_id)s",
|
|
args,
|
|
cursor=cursor,
|
|
)
|
|
if len(r):
|
|
return False
|
|
# inscrit
|
|
ndb.SimpleQuery(
|
|
"INSERT INTO group_membership (etudid, group_id) VALUES (%(etudid)s, %(group_id)s)",
|
|
args,
|
|
cursor=cursor,
|
|
)
|
|
return True
|
|
|
|
|
|
def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
|
|
"""Inscrit etud au groupe
|
|
(et le désinscrit d'autres groupes de cette partition)
|
|
Return True si changement, False s'il était déjà dans ce groupe.
|
|
"""
|
|
etud: Identite = Identite.query.get_or_404(etudid)
|
|
if not group.partition.set_etud_group(etud, group):
|
|
return # pas de changement
|
|
|
|
# - log
|
|
formsemestre: FormSemestre = group.partition.formsemestre
|
|
log(f"change_etud_group_in_partition: etudid={etudid} group={group}")
|
|
Scolog.logdb(
|
|
method="changeGroup",
|
|
etudid=etudid,
|
|
msg=f"""formsemestre_id={formsemestre.id}, partition_name={
|
|
group.partition.partition_name or ""}, group_name={group.group_name or ""}""",
|
|
commit=True,
|
|
)
|
|
|
|
# - Update parcours
|
|
if group.partition.partition_name == scu.PARTITION_PARCOURS:
|
|
formsemestre.update_inscriptions_parcours_from_groups(etudid=etudid)
|
|
|
|
# - invalidate cache
|
|
sco_cache.invalidate_formsemestre(
|
|
formsemestre_id=formsemestre.id
|
|
) # > change etud group
|
|
|
|
|
|
def setGroups(
|
|
partition_id,
|
|
groupsLists="", # members of each existing group
|
|
groupsToCreate="", # name and members of new groups
|
|
groupsToDelete="", # groups to delete
|
|
):
|
|
"""Affect groups (Ajax POST request): renvoie du XML
|
|
groupsLists: lignes de la forme "group_id;etudid;...\n"
|
|
groupsToCreate: lignes "group_name;etudid;...\n"
|
|
groupsToDelete: group_id;group_id;...
|
|
|
|
Ne peux pas modifier les groupes des partitions non éditables.
|
|
"""
|
|
|
|
def xml_error(msg, code=404):
|
|
data = (
|
|
f'<?xml version="1.0" encoding="utf-8"?><response>Error: {msg}</response>'
|
|
)
|
|
response = make_response(data, code)
|
|
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
|
return response
|
|
|
|
partition: Partition = db.session.get(Partition, partition_id)
|
|
if not partition.groups_editable and (groupsToCreate or groupsToDelete):
|
|
msg = "setGroups: partition non editable"
|
|
log(msg)
|
|
return xml_error(msg, code=403)
|
|
|
|
if not partition.formsemestre.etat:
|
|
raise AccessDenied("Modification impossible: semestre verrouillé")
|
|
if not partition.formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
log(
|
|
f"""***setGroups: partition={partition}
|
|
groupsLists={groupsLists}
|
|
groupsToCreate={groupsToCreate}
|
|
groupsToDelete={groupsToDelete}
|
|
"""
|
|
)
|
|
|
|
groupsToDelete = [g for g in groupsToDelete.split(";") if g]
|
|
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()
|
|
if not group_id:
|
|
continue
|
|
try:
|
|
group_id = int(group_id)
|
|
except ValueError:
|
|
log(f"setGroups: ignoring invalid group_id={group_id}")
|
|
continue
|
|
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
|
|
# Anciens membres du groupe:
|
|
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:
|
|
# é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
|
|
change_etud_group_in_partition(etudid, group)
|
|
# Retire les anciens membres:
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
for etudid in old_members_set:
|
|
ndb.SimpleQuery(
|
|
"DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s",
|
|
{"etudid": etudid, "group_id": group_id},
|
|
cursor=cursor,
|
|
)
|
|
logdb(
|
|
cnx,
|
|
method="removeFromGroup",
|
|
etudid=etudid,
|
|
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:
|
|
for group_id in groupsToDelete:
|
|
delete_group(group_id, partition_id=partition_id)
|
|
|
|
# Crée les nouveaux groupes
|
|
for line in groupsToCreate.split("\n"): # for each group_name (one per line)
|
|
fs = line.split(";")
|
|
group_name = fs[0].strip()
|
|
if not group_name:
|
|
continue
|
|
try:
|
|
group = create_group(partition_id, group_name)
|
|
except ScoValueError as exc:
|
|
msg = exc.args[0] if len(exc.args) > 0 else "erreur inconnue"
|
|
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)
|
|
|
|
# Update parcours
|
|
partition.formsemestre.update_inscriptions_parcours_from_groups()
|
|
|
|
data = (
|
|
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
|
)
|
|
response = make_response(data)
|
|
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
|
return response
|
|
|
|
|
|
def create_group(partition_id, group_name="", default=False) -> GroupDescr:
|
|
"""Create a new group in this partition.
|
|
If default, create default partition (with no name)
|
|
Obsolete: utiliser Partition.create_group
|
|
"""
|
|
partition = Partition.query.get_or_404(partition_id)
|
|
if not partition.formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
#
|
|
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(partition, 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 partition.groups]
|
|
if len(numeros) > 0:
|
|
new_numero = max(numeros) + 1
|
|
else:
|
|
new_numero = 0
|
|
group = GroupDescr(partition=partition, 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
|
|
|
|
|
|
def delete_group(group_id, partition_id=None):
|
|
"""form suppression d'un groupe.
|
|
(ne desinscrit pas les etudiants, change juste leur
|
|
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 = GroupDescr.query.get_or_404(group_id)
|
|
if partition_id:
|
|
if partition_id != group.partition_id:
|
|
raise ValueError("inconsistent partition/group")
|
|
if not group.partition.formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
log(f"delete_group: group={group} partition={group.partition}")
|
|
formsemestre = group.partition.formsemestre
|
|
group_delete(group.id)
|
|
formsemestre.update_inscriptions_parcours_from_groups()
|
|
|
|
|
|
def partition_create(
|
|
formsemestre_id,
|
|
partition_name="",
|
|
default=False,
|
|
numero=None,
|
|
redirect=True,
|
|
):
|
|
"""Create a new partition"""
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
if partition_name:
|
|
partition_name = str(partition_name).strip()
|
|
if default:
|
|
partition_name = None
|
|
if not partition_name and not default:
|
|
raise ScoValueError("Nom de partition invalide (vide)")
|
|
redirect = int(redirect)
|
|
# checkGroupName(partition_name)
|
|
if partition_name in [
|
|
p["partition_name"] for p in get_partitions_list(formsemestre_id)
|
|
]:
|
|
raise ScoValueError(
|
|
"Il existe déjà une partition %s dans ce semestre" % partition_name
|
|
)
|
|
|
|
cnx = ndb.GetDBConnexion()
|
|
if numero is None:
|
|
numero = (
|
|
ndb.SimpleQuery(
|
|
"SELECT MAX(id) FROM partition WHERE formsemestre_id=%(formsemestre_id)s",
|
|
{"formsemestre_id": formsemestre_id},
|
|
).fetchone()[0]
|
|
or 0
|
|
)
|
|
partition_id = partitionEditor.create(
|
|
cnx,
|
|
{
|
|
"formsemestre_id": formsemestre_id,
|
|
"partition_name": partition_name,
|
|
"numero": numero,
|
|
},
|
|
)
|
|
log("createPartition: created partition_id=%s" % partition_id)
|
|
#
|
|
if redirect:
|
|
return flask.redirect(
|
|
url_for(
|
|
"scolar.edit_partition_form",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre_id,
|
|
)
|
|
)
|
|
else:
|
|
return partition_id
|
|
|
|
|
|
def get_arrow_icons_tags():
|
|
"""returns html tags for arrows"""
|
|
#
|
|
arrow_up = scu.icontag("arrow_up", title="remonter")
|
|
arrow_down = scu.icontag("arrow_down", title="descendre")
|
|
arrow_none = scu.icontag("arrow_none", title="")
|
|
|
|
return arrow_up, arrow_down, arrow_none
|
|
|
|
|
|
def edit_partition_form(formsemestre_id=None):
|
|
"""Form to create/suppress partitions"""
|
|
# ad-hoc form
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
|
|
partitions = get_partitions_list(formsemestre_id)
|
|
arrow_up, arrow_down, arrow_none = get_arrow_icons_tags()
|
|
suppricon = scu.icontag(
|
|
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
|
|
)
|
|
#
|
|
H = [
|
|
html_sco_header.sco_header(
|
|
page_title="Partitions...",
|
|
javascripts=["js/edit_partition_form.js"],
|
|
),
|
|
# limite à SHORT_STR_LEN
|
|
r"""<script type="text/javascript">
|
|
function checkname() {
|
|
var val = document.editpart.partition_name.value.replace(/^\s+/, "").replace(/\s+$/, "");
|
|
if ((val.length > 0)&&(val.length < 32)) {
|
|
document.editpart.ok.disabled = false;
|
|
} else {
|
|
document.editpart.ok.disabled = true;
|
|
}
|
|
}
|
|
</script>
|
|
""",
|
|
f"""<h2>Partitions du semestre</h2>
|
|
<p class="help">
|
|
👉💡 vous pourriez essayer <a href="{
|
|
url_for("scolar.partition_editor",
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
|
}" class="stdlink">le nouvel éditeur</a>
|
|
</p>
|
|
|
|
<form name="editpart" id="editpart" method="POST" action="partition_create">
|
|
<div id="epmsg"></div>
|
|
<table>
|
|
<tr class="eptit">
|
|
<th></th><th></th><th></th><th>Partition</th><th>Groupes</th><th></th><th></th><th></th>
|
|
</tr>
|
|
""",
|
|
]
|
|
i = 0
|
|
for p in partitions:
|
|
if p["partition_name"] is not None:
|
|
H.append(
|
|
f"""<tr><td class="epnav"><a class="stdlink"
|
|
href="{url_for("scolar.partition_delete",
|
|
scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"])
|
|
}">{suppricon}</a> </td><td class="epnav">"""
|
|
)
|
|
if i != 0:
|
|
H.append(
|
|
f"""<a href="{url_for("scolar.partition_move",
|
|
scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"], after=0)
|
|
}">{arrow_up}</a>"""
|
|
)
|
|
H.append('</td><td class="epnav">')
|
|
if i < len(partitions) - 2:
|
|
H.append(
|
|
f"""<a href="{url_for("scolar.partition_move",
|
|
scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"], after=1)
|
|
}">{arrow_down}</a>"""
|
|
)
|
|
i += 1
|
|
H.append(
|
|
f"""</td>
|
|
<td>{p["partition_name"] or ""}</td>
|
|
<td>"""
|
|
)
|
|
lg = [
|
|
f"""{group["group_name"]} ({len(get_group_members(group["group_id"]))})"""
|
|
for group in get_partition_groups(p)
|
|
]
|
|
H.append(", ".join(lg))
|
|
H.append("""</td><td>""")
|
|
H.append(
|
|
f"""<a class="stdlink" href="{
|
|
url_for("scolar.affect_groups",
|
|
scodoc_dept=g.scodoc_dept,
|
|
partition_id=p["partition_id"])
|
|
}">répartir</a></td>
|
|
"""
|
|
)
|
|
H.append("""</td>""")
|
|
if p["groups_editable"]:
|
|
H.append(
|
|
f"""<td><a class="stdlink" href="{
|
|
url_for("scolar.partition_rename",
|
|
scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"])
|
|
}">renommer</a></td>"""
|
|
)
|
|
else:
|
|
H.append("""<td>non éditable</td>""")
|
|
# classement:
|
|
H.append('<td width="250px">')
|
|
if p["bul_show_rank"]:
|
|
checked = 'checked="1"'
|
|
else:
|
|
checked = ""
|
|
H.append(
|
|
'<div><input type="checkbox" class="rkbox" data-partition_id="%s" %s onchange="update_rk(this);"/>afficher rang sur bulletins</div>'
|
|
% (p["partition_id"], checked)
|
|
)
|
|
if p["show_in_lists"]:
|
|
checked = 'checked="1"'
|
|
else:
|
|
checked = ""
|
|
H.append(
|
|
f"""<div><input type="checkbox" class="rkbox" data-partition_id="{p['partition_id']
|
|
}" {checked} onchange="update_show_in_list(this);"/>Afficher ces groupes sur les tableaux et bulletins</div>"""
|
|
)
|
|
H.append("</td>")
|
|
#
|
|
H.append("</tr>")
|
|
H.append("</table>")
|
|
H.append(
|
|
f"""<div class="form_rename_partition">
|
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
|
|
<input type="hidden" name="redirect" value="1"/>
|
|
<input type="text" name="partition_name" size="12" onkeyup="checkname();"/>
|
|
<input type="submit" name="ok" disabled="1" value="Nouvelle partition"/>
|
|
"""
|
|
)
|
|
if formsemestre.formation.is_apc() and scu.PARTITION_PARCOURS not in (
|
|
p["partition_name"] for p in partitions
|
|
):
|
|
# propose création partition "Parcours"
|
|
H.append(
|
|
f"""
|
|
<div style="margin-top: 10px"><a class="stdlink" href="{
|
|
url_for("scolar.create_partition_parcours", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
|
}">Créer une partition avec un groupe par parcours (BUT)</a>
|
|
</div>
|
|
"""
|
|
)
|
|
H.append(
|
|
"""
|
|
</div>
|
|
</form>
|
|
"""
|
|
)
|
|
H.append(
|
|
"""<div class="help">
|
|
<p>Les partitions sont des découpages de l'ensemble des étudiants.
|
|
Par exemple, les "groupes de TD" sont une partition.
|
|
On peut créer autant de partitions que nécessaire.
|
|
</p>
|
|
<ul>
|
|
<li>Dans chaque partition, un nombre de groupes quelconque peuvent
|
|
être créés (suivre le lien "répartir").
|
|
<li>On peut faire afficher le classement de l'étudiant dans son
|
|
groupe d'une partition en cochant "afficher rang sur bulletins"
|
|
(ainsi, on peut afficher le classement en groupes de TD mais pas en
|
|
groupe de TP, si ce sont deux partitions).
|
|
</li>
|
|
<li>Décocher "Afficher ces groupes sur les tableaux et bulletins" pour ne pas que cette partition
|
|
apparaisse dans les noms de groupes
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
"""
|
|
)
|
|
return "\n".join(H) + html_sco_header.sco_footer()
|
|
|
|
|
|
def partition_set_attr(partition_id, attr, value):
|
|
"""Set partition attribute: bul_show_rank or show_in_lists"""
|
|
if attr not in {"bul_show_rank", "show_in_lists"}:
|
|
raise ValueError(f"invalid partition attribute: {attr}")
|
|
|
|
partition = Partition.query.get_or_404(partition_id)
|
|
if not partition.formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
|
|
log(f"partition_set_attr({partition_id}, {attr}, {value})")
|
|
value = int(value)
|
|
if getattr(partition, attr, None) != value:
|
|
setattr(partition, attr, value)
|
|
db.session.add(partition)
|
|
db.session.commit()
|
|
# invalid bulletin cache
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=partition.formsemestre.id)
|
|
return "enregistré"
|
|
|
|
|
|
def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
|
|
"""Suppress a partition (and all groups within).
|
|
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"]
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
|
|
if not partition["partition_name"] and not force:
|
|
raise ValueError("cannot suppress this partition")
|
|
redirect = int(redirect)
|
|
cnx = ndb.GetDBConnexion()
|
|
groups = get_partition_groups(partition)
|
|
|
|
if not dialog_confirmed:
|
|
if groups:
|
|
grnames = "(" + ", ".join([g["group_name"] or "" for g in groups]) + ")"
|
|
else:
|
|
grnames = ""
|
|
return scu.confirm_dialog(
|
|
"""<h2>Supprimer la partition "%s" ?</h2>
|
|
<p>Les groupes %s de cette partition seront supprimés</p>
|
|
"""
|
|
% (partition["partition_name"], grnames),
|
|
dest_url="",
|
|
cancel_url="edit_partition_form?formsemestre_id=%s" % formsemestre_id,
|
|
parameters={"redirect": redirect, "partition_id": partition_id},
|
|
)
|
|
|
|
log("partition_delete: partition_id=%s" % partition_id)
|
|
# 1- groups
|
|
for group in groups:
|
|
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(
|
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
|
)
|
|
|
|
|
|
def partition_move(partition_id, after=0, redirect=1):
|
|
"""Move before/after previous one (decrement/increment numero)"""
|
|
partition = get_partition(partition_id)
|
|
formsemestre_id = partition["formsemestre_id"]
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
#
|
|
redirect = int(redirect)
|
|
after = int(after) # 0: deplace avant, 1 deplace apres
|
|
if after not in (0, 1):
|
|
raise ValueError('invalid value for "after"')
|
|
others = get_partitions_list(formsemestre_id)
|
|
|
|
objs = (
|
|
Partition.query.filter_by(formsemestre_id=formsemestre_id)
|
|
.order_by(Partition.numero, Partition.partition_name)
|
|
.all()
|
|
)
|
|
if len({o.numero for o in objs}) != len(objs):
|
|
# il y a des numeros identiques !
|
|
scu.objects_renumber(db, objs)
|
|
|
|
if len(others) > 1:
|
|
pidx = [p["partition_id"] for p in others].index(partition_id)
|
|
# log("partition_move: after=%s pidx=%s" % (after, pidx))
|
|
neigh = None # partition to swap with
|
|
if after == 0 and pidx > 0:
|
|
neigh = others[pidx - 1]
|
|
elif after == 1 and pidx < len(others) - 1:
|
|
neigh = others[pidx + 1]
|
|
if neigh: #
|
|
# swap numero between partition and its neighbor
|
|
# log("moving partition %s" % partition_id)
|
|
cnx = ndb.GetDBConnexion()
|
|
# Si aucun numéro n'a été affecté, le met au minimum
|
|
min_numero = (
|
|
ndb.SimpleQuery(
|
|
"SELECT MIN(numero) FROM partition WHERE formsemestre_id=%(formsemestre_id)s",
|
|
{"formsemestre_id": formsemestre_id},
|
|
).fetchone()[0]
|
|
or 0
|
|
)
|
|
if neigh["numero"] is None:
|
|
neigh["numero"] = min_numero - 1
|
|
if partition["numero"] is None:
|
|
partition["numero"] = min_numero - 1 - after
|
|
partition["numero"], neigh["numero"] = neigh["numero"], partition["numero"]
|
|
partitionEditor.edit(cnx, partition)
|
|
partitionEditor.edit(cnx, neigh)
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
|
|
|
# redirect to partition edit page:
|
|
if redirect:
|
|
return flask.redirect(
|
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
|
)
|
|
|
|
|
|
def partition_rename(partition_id):
|
|
"""Form to rename a partition"""
|
|
partition = get_partition(partition_id)
|
|
formsemestre_id = partition["formsemestre_id"]
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
H = ["<h2>Renommer une partition</h2>"]
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
(
|
|
("partition_id", {"default": partition_id, "input_type": "hidden"}),
|
|
(
|
|
"partition_name",
|
|
{
|
|
"title": "Nouveau nom",
|
|
"default": partition["partition_name"],
|
|
"allow_null": False,
|
|
"size": 12,
|
|
"validator": lambda val, _: (len(val) < SHORT_STR_LEN)
|
|
and (val != scu.PARTITION_PARCOURS),
|
|
},
|
|
),
|
|
),
|
|
submitlabel="Renommer",
|
|
cancelbutton="Annuler",
|
|
)
|
|
if tf[0] == 0:
|
|
return (
|
|
html_sco_header.sco_header()
|
|
+ "\n".join(H)
|
|
+ "\n"
|
|
+ tf[1]
|
|
+ html_sco_header.sco_footer()
|
|
)
|
|
elif tf[0] == -1:
|
|
return flask.redirect(
|
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
|
)
|
|
else:
|
|
# form submission
|
|
return partition_set_name(partition_id, tf[2]["partition_name"])
|
|
|
|
|
|
def partition_set_name(partition_id, partition_name, redirect=1):
|
|
"""Set partition name"""
|
|
partition_name = str(partition_name).strip()
|
|
if not partition_name:
|
|
raise ValueError("partition name must be non empty")
|
|
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"]
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
# check unicity
|
|
r = ndb.SimpleDictFetch(
|
|
"""SELECT p.* FROM partition p
|
|
WHERE p.partition_name = %(partition_name)s
|
|
AND formsemestre_id = %(formsemestre_id)s
|
|
""",
|
|
{"partition_name": partition_name, "formsemestre_id": formsemestre_id},
|
|
)
|
|
if len(r) > 1 or (len(r) == 1 and r[0]["id"] != partition_id):
|
|
raise ScoValueError(
|
|
f"Partition {partition_name} déjà existante dans ce semestre !"
|
|
)
|
|
redirect = int(redirect)
|
|
cnx = ndb.GetDBConnexion()
|
|
partitionEditor.edit(
|
|
cnx, {"partition_id": partition_id, "partition_name": partition_name}
|
|
)
|
|
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre_id)
|
|
|
|
# redirect to partition edit page:
|
|
if redirect:
|
|
return flask.redirect(
|
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
|
)
|
|
|
|
|
|
def groups_auto_repartition(partition: Partition):
|
|
"""Réparti les etudiants dans des groupes dans une partition, en respectant le niveau
|
|
et la mixité.
|
|
"""
|
|
if not partition.groups_editable:
|
|
raise AccessDenied("Partition non éditable")
|
|
formsemestre = partition.formsemestre
|
|
# renvoie sur page édition partitions et groupes
|
|
dest_url = url_for(
|
|
"scolar.partition_editor",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre.id,
|
|
)
|
|
if not formsemestre.can_change_groups():
|
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
|
|
descr = [
|
|
("partition_id", {"input_type": "hidden"}),
|
|
(
|
|
"groupNames",
|
|
{
|
|
"size": 40,
|
|
"title": "Groupes à créer",
|
|
"allow_null": False,
|
|
"explanation": "noms des groupes à former, séparés par des virgules (les groupes existants seront effacés)",
|
|
},
|
|
),
|
|
]
|
|
|
|
H = [
|
|
html_sco_header.sco_header(
|
|
page_title="Répartition des groupes", formsemestre_id=formsemestre.id
|
|
),
|
|
f"""<h2>Répartition des groupes de {partition.partition_name}</h2>
|
|
<p>Semestre {formsemestre.titre_annee()}</p>
|
|
<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
|
|
des groupes (en utilisant la dernière moyenne générale disponible pour
|
|
chaque étudiant) et de maximiser la mixité de chaque groupe.
|
|
</p>
|
|
""",
|
|
]
|
|
|
|
tf = TrivialFormulator(
|
|
request.base_url,
|
|
scu.get_request_args(),
|
|
descr,
|
|
{},
|
|
cancelbutton="Annuler",
|
|
submitlabel="Créer et peupler les groupes",
|
|
name="tf",
|
|
)
|
|
if tf[0] == 0:
|
|
return "\n".join(H) + "\n" + tf[1] + html_sco_header.sco_footer()
|
|
elif tf[0] == -1:
|
|
return flask.redirect(dest_url)
|
|
else:
|
|
# form submission
|
|
log(f"groups_auto_repartition({partition})")
|
|
group_names = tf[2]["groupNames"]
|
|
group_names = sorted({x.strip() for x in group_names.split(",")})
|
|
# Détruit les groupes existant de cette partition
|
|
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(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 = {x["civilite"] for x in identdict.values()}
|
|
listes = {}
|
|
for civilite in civilites:
|
|
listes[civilite] = [
|
|
(_get_prev_moy(x["etudid"], formsemestre.id), x["etudid"])
|
|
for x in identdict.values()
|
|
if x["civilite"] == civilite
|
|
]
|
|
listes[civilite].sort()
|
|
log("listes[%s] = %s" % (civilite, listes[civilite]))
|
|
# affect aux groupes:
|
|
n = len(identdict)
|
|
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(f"{etudid} in group {group.id}")
|
|
return flask.redirect(dest_url)
|
|
|
|
|
|
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,...).
|
|
"""
|
|
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
|
if not info:
|
|
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
|
etud = info[0]
|
|
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
|
if Se.prev:
|
|
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
|
return nt.get_etud_moy_gen(etudid)
|
|
else:
|
|
return 0.0
|
|
|
|
|
|
def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
|
"""Crée une partition "apo_etapes" avec un groupe par étape Apogée.
|
|
Cette partition n'est crée que si plusieurs étapes différentes existent dans ce
|
|
semestre.
|
|
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(f"create_etapes_partition({formsemestre_id})")
|
|
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
|
args={"formsemestre_id": formsemestre_id}
|
|
)
|
|
etapes = {i["etape"] for i in ins if i["etape"]}
|
|
partitions = get_partitions_list(formsemestre_id, with_default=False)
|
|
partition = None
|
|
for p in partitions:
|
|
if p["partition_name"] == partition_name:
|
|
partition = p
|
|
break
|
|
if len(etapes) < 2 and not partition:
|
|
return # moins de deux étapes, pas de création
|
|
if partition:
|
|
pid = partition["partition_id"]
|
|
else:
|
|
pid = partition_create(
|
|
formsemestre_id, partition_name=partition_name, redirect=False
|
|
)
|
|
partition: Partition = db.session.get(Partition, pid)
|
|
groups = partition.groups
|
|
groups_by_names = {g.group_name: g for g in groups}
|
|
for etape in etapes:
|
|
if etape not in groups_by_names:
|
|
new_group = create_group(pid, etape)
|
|
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"]])
|
|
|
|
|
|
def do_evaluation_listeetuds_groups(
|
|
evaluation_id: int,
|
|
groups=None,
|
|
getallstudents: bool = False,
|
|
include_demdef: bool = False,
|
|
) -> list[tuple[int, str]]:
|
|
"""Donne la liste non triée des etudids inscrits à cette évaluation dans les
|
|
groupes indiqués.
|
|
Si getallstudents==True, donne tous les étudiants inscrits à cette
|
|
evaluation.
|
|
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
|
|
(sinon, par défaut, seulement les 'I')
|
|
|
|
Résultat: [ (etudid, etat) ], où etat='I', 'D', 'DEF'
|
|
"""
|
|
# nb: pour notes_table / do_evaluation_etat, getallstudents est vrai et
|
|
# include_demdef faux
|
|
fromtables = [
|
|
"notes_moduleimpl_inscription Im",
|
|
"notes_formsemestre_inscription Isem",
|
|
"notes_moduleimpl M",
|
|
"notes_evaluation E",
|
|
]
|
|
# construit condition sur les groupes
|
|
if not getallstudents:
|
|
if not groups:
|
|
return [] # no groups, so no students
|
|
rg = ["gm.group_id = '%(group_id)s'" % g for g in groups]
|
|
rq = """and Isem.etudid = gm.etudid
|
|
and gd.partition_id = p.id
|
|
and p.formsemestre_id = Isem.formsemestre_id
|
|
"""
|
|
r = rq + " AND (" + " or ".join(rg) + " )"
|
|
fromtables += ["group_membership gm", "group_descr gd", "partition p"]
|
|
else:
|
|
r = ""
|
|
|
|
# requete complete
|
|
req = (
|
|
"SELECT distinct Im.etudid, Isem.etat FROM "
|
|
+ ", ".join(fromtables)
|
|
+ """ WHERE Isem.etudid = Im.etudid
|
|
and Im.moduleimpl_id = M.id
|
|
and Isem.formsemestre_id = M.formsemestre_id
|
|
and E.moduleimpl_id = M.id
|
|
and E.id = %(evaluation_id)s
|
|
"""
|
|
)
|
|
if not include_demdef:
|
|
req += " and Isem.etat='I'"
|
|
req += r
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor()
|
|
cursor.execute(req, {"evaluation_id": evaluation_id})
|
|
return cursor.fetchall()
|
|
|
|
|
|
def do_evaluation_listegroupes(evaluation_id, include_default=False):
|
|
"""Donne la liste des groupes dans lesquels figurent des etudiants inscrits
|
|
au module/semestre auquel appartient cette evaluation.
|
|
Si include_default, inclue aussi le groupe par defaut ('tous')
|
|
[ group ]
|
|
"""
|
|
if include_default:
|
|
c = ""
|
|
else:
|
|
c = " AND p.partition_name is not NULL"
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor()
|
|
cursor.execute(
|
|
"""SELECT DISTINCT gd.id AS group_id
|
|
FROM group_descr gd, group_membership gm, partition p,
|
|
notes_moduleimpl m, notes_evaluation e
|
|
WHERE gm.group_id = gd.id
|
|
and gd.partition_id = p.id
|
|
and p.formsemestre_id = m.formsemestre_id
|
|
and m.id = e.moduleimpl_id
|
|
and e.id = %(evaluation_id)s
|
|
"""
|
|
+ c,
|
|
{"evaluation_id": evaluation_id},
|
|
)
|
|
group_ids = [x[0] for x in cursor]
|
|
return listgroups(group_ids)
|
|
|
|
|
|
def listgroups(group_ids):
|
|
cnx = ndb.GetDBConnexion()
|
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
|
groups = []
|
|
for group_id in group_ids:
|
|
cursor.execute(
|
|
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
|
|
FROM group_descr gd, partition p
|
|
WHERE p.id = gd.partition_id
|
|
AND gd.id = %(group_id)s
|
|
""",
|
|
{"group_id": group_id},
|
|
)
|
|
r = cursor.dictfetchall()
|
|
if r:
|
|
groups.append(r[0])
|
|
return _sortgroups(groups)
|
|
|
|
|
|
def _sortgroups(groups):
|
|
# Tri: place 'all' en tête, puis groupe par partition / nom de groupe
|
|
R = [g for g in groups if g["partition_name"] is None]
|
|
o = [g for g in groups if g["partition_name"] != None]
|
|
o.sort(key=lambda x: (x["numero"] or 0, x["group_name"]))
|
|
|
|
return R + o
|
|
|
|
|
|
def listgroups_filename(groups):
|
|
"""Build a filename representing groups"""
|
|
return "gr" + "+".join([g["group_name"] or "tous" for g in groups])
|
|
|
|
|
|
def listgroups_abbrev(groups):
|
|
"""Human readable abbreviation descring groups (eg "A / AB / B3")
|
|
Ne retient que les partitions avec show_in_lists
|
|
"""
|
|
return " / ".join(
|
|
[g["group_name"] for g in groups if g["group_name"] and g["show_in_lists"]]
|
|
)
|
|
|
|
|
|
# form_group_choice replaces formChoixGroupe
|
|
def form_group_choice(
|
|
formsemestre_id,
|
|
allow_none=True, # offre un choix vide dans chaque partition
|
|
select_default=True, # Le groupe par defaut est mentionné (hidden).
|
|
display_sem_title=False,
|
|
):
|
|
"""Partie de formulaire pour le choix d'un ou plusieurs groupes.
|
|
Variable : group_ids
|
|
"""
|
|
from app.scodoc import sco_formsemestre
|
|
|
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
if display_sem_title:
|
|
sem_title = "%s: " % sem["titremois"]
|
|
else:
|
|
sem_title = ""
|
|
#
|
|
H = ["""<table>"""]
|
|
for p in get_partitions_list(formsemestre_id):
|
|
if p["partition_name"] is None:
|
|
if select_default:
|
|
H.append(
|
|
'<input type="hidden" name="group_ids:list" value="%s"/>'
|
|
% get_partition_groups(p)[0]["group_id"]
|
|
)
|
|
else:
|
|
H.append("<tr><td>Groupe de %(partition_name)s</td><td>" % p)
|
|
H.append('<select name="group_ids:list">')
|
|
if allow_none:
|
|
H.append('<option value="">aucun</option>')
|
|
for group in get_partition_groups(p):
|
|
H.append(
|
|
'<option value="%s">%s %s</option>'
|
|
% (group["group_id"], sem_title, group["group_name"])
|
|
)
|
|
H.append("</select></td></tr>")
|
|
H.append("""</table>""")
|
|
return "\n".join(H)
|
|
|
|
|
|
def make_query_groups(group_ids):
|
|
if group_ids:
|
|
return "&".join(["group_ids%3Alist=" + str(group_id) for group_id in group_ids])
|
|
else:
|
|
return ""
|
|
|
|
|
|
class GroupIdInferer(object):
|
|
"""Sert à retrouver l'id d'un groupe dans un semestre donné
|
|
à partir de son nom.
|
|
Attention: il peut y avoir plusieurs groupes de même nom
|
|
dans des partitions différentes. Dans ce cas, prend le dernier listé.
|
|
On peut indiquer la partition en écrivant
|
|
partition_name:group_name
|
|
"""
|
|
|
|
def __init__(self, formsemestre_id):
|
|
groups = get_sem_groups(formsemestre_id)
|
|
self.name2group_id = {}
|
|
self.partitionname2group_id = {}
|
|
for group in groups:
|
|
self.name2group_id[group["group_name"]] = group["group_id"]
|
|
self.partitionname2group_id[
|
|
(group["partition_name"], group["group_name"])
|
|
] = group["group_id"]
|
|
|
|
def __getitem__(self, name):
|
|
"""Get group_id from group_name, or None is nonexistent.
|
|
The group name can be prefixed by the partition's name, using
|
|
syntax partition_name:group_name
|
|
"""
|
|
l = name.split(":", 1)
|
|
if len(l) > 1:
|
|
partition_name, group_name = l
|
|
else:
|
|
partition_name = None
|
|
group_name = name
|
|
if partition_name is None:
|
|
group_id = self.name2group_id.get(group_name, None)
|
|
if group_id is None and name[-2:] == ".0":
|
|
# si nom groupe numerique, excel ajoute parfois ".0" !
|
|
group_name = group_name[:-2]
|
|
group_id = self.name2group_id.get(group_name, None)
|
|
else:
|
|
group_id = self.partitionname2group_id.get(
|
|
(partition_name, group_name), None
|
|
)
|
|
return group_id
|