1
0
forked from ScoDoc/ScoDoc

merge Scodoc/master -> iziram/assiduites_corrigee

This commit is contained in:
iziram 2023-07-10 10:41:59 +02:00
commit 531ac1cb0c
16 changed files with 249 additions and 151 deletions

View File

@ -12,6 +12,7 @@ from operator import attrgetter
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import login_required
from sqlalchemy.exc import IntegrityError
import app import app
from app import db, log 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.models.groups import group_membership
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu 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}: 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") return json_error(404, "etud non inscrit au formsemestre du groupe")
sco_groups.change_etud_group_in_partition( try:
etudid, group_id, group.partition.to_dict() 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} return {"group_id": group_id, "etudid": etudid}

View File

@ -865,10 +865,15 @@ class DecisionsProposeesAnnee(DecisionsProposees):
self.etud.id, self.formsemestre.id self.etud.id, self.formsemestre.id
) )
for dec_ue in self.decisions_ues.values(): for dec_ue in self.decisions_ues.values():
if dec_ue.formsemestre.id == self.formsemestre.id: if (
dec_ue
and self.formsemestre
and dec_ue.formsemestre.id == self.formsemestre.id
):
dec_ue.erase() dec_ue.erase()
else: else:
for dec_ue in self.decisions_ues.values(): for dec_ue in self.decisions_ues.values():
if dec_ue:
dec_ue.erase() dec_ue.erase()
if self.formsemestre_impair: if self.formsemestre_impair:

View File

@ -827,16 +827,32 @@ class BonusStMalo(BonusIUTRennes1):
class BonusLaRocheSurYon(BonusSportAdditif): class BonusLaRocheSurYon(BonusSportAdditif):
"""Bonus IUT de La Roche-sur-Yon """Bonus IUT de La Roche-sur-Yon
Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points <p>
sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE. <b>La note saisie s'applique directement</b>: si on saisit 0,2, un bonus de 0,2 points est appliqué
aux moyennes.
La valeur maximale du bonus est 1 point. Il est appliqué sur les moyennes d'UEs en BUT,
ou sur la moyenne générale dans les autres formations.
</p>
<p>Pour les <b>semestres antérieurs à janvier 2023</b>: si une note de bonus est saisie,
l'étudiant est gratifié de 0,2 points sur sa moyenne générale ou, en BUT, sur la
moyenne de chaque UE.
</p>
""" """
name = "bonus_larochesuryon" name = "bonus_larochesuryon"
displayed_name = "IUT de La Roche-sur-Yon" displayed_name = "IUT de La Roche-sur-Yon"
seuil_moy_gen = 0.0 seuil_moy_gen = 0.0
seuil_comptage = 0.0 seuil_comptage = 0.0
proportion_point = 1e10 # le moindre point sature le bonus
bonus_max = 0.2 # à 0.2 def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus, avec réglage différent suivant la date"""
if self.formsemestre.date_debut > datetime.date(2022, 12, 31):
self.proportion_point = 1.0
self.bonus_max = 1
else: # ancienne règle
self.proportion_point = 1e10 # le moindre point sature le bonus
self.bonus_max = 0.2 # à 0.2
super().compute_bonus(sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan)
class BonusLaRochelle(BonusSportAdditif): class BonusLaRochelle(BonusSportAdditif):

View File

@ -381,7 +381,11 @@ class ResultatsSemestre(ResultatsCache):
was_capitalized = False was_capitalized = False
if etudid in self.validations.ue_capitalisees.index: if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue) ue_cap = self._get_etud_ue_cap(etudid, ue)
if ue_cap and not np.isnan(ue_cap["moy_ue"]): if (
ue_cap
and (ue_cap["moy_ue"] is not None)
and not np.isnan(ue_cap["moy_ue"])
):
was_capitalized = True was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"] moy_ue = ue_cap["moy_ue"]

View File

@ -575,6 +575,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

@ -8,11 +8,13 @@
"""ScoDoc models: Groups & partitions """ScoDoc models: Groups & partitions
""" """
from operator import attrgetter 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 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 AccessDenied, ScoValueError
class Partition(db.Model): class Partition(db.Model):
@ -117,6 +119,81 @@ class Partition(db.Model):
.first() .first()
) )
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
ou que l'étudiant n'est pas inscrit au semestre.
Return True si changement, False s'il était déjà dans ce groupe.
"""
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 etud.id not in (e.id for e in self.formsemestre.etuds):
raise ScoValueError(
f"""étudiant {etud.nomprenom} non inscrit au formsemestre du groupe {
group.group_name}"""
)
try:
existing_row = (
db.session.query(group_membership)
.filter_by(etudid=etud.id)
.join(GroupDescr)
.filter_by(partition_id=self.id)
.first()
)
if existing_row:
existing_group_id = existing_row[1]
if group.id == existing_group_id:
return False
# 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=etud.id, group_id=group.id
)
db.session.execute(new_row)
db.session.commit()
except IntegrityError:
db.session.rollback()
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): class GroupDescr(db.Model):
"""Description d'un groupe d'une partition""" """Description d'un groupe d'une partition"""

View File

@ -949,6 +949,7 @@ def do_formsemestre_validate_ue(
"ue_id": ue_id, "ue_id": ue_id,
"semestre_id": semestre_id, "semestre_id": semestre_id,
"is_external": is_external, "is_external": is_external,
"moy_ue": moy_ue,
} }
if date: if date:
args["event_date"] = date args["event_date"] = date
@ -965,12 +966,11 @@ def do_formsemestre_validate_ue(
cursor.execute("delete from scolar_formsemestre_validation where " + cond, args) cursor.execute("delete from scolar_formsemestre_validation where " + cond, args)
# insert # insert
args["code"] = code args["code"] = code
if code == ADM: if (code == ADM) and (moy_ue is None):
if moy_ue is None:
# stocke la moyenne d'UE capitalisée: # stocke la moyenne d'UE capitalisée:
ue_status = nt.get_etud_ue_status(etudid, ue_id) ue_status = nt.get_etud_ue_status(etudid, ue_id)
moy_ue = ue_status["moy"] if ue_status else "" moy_ue = ue_status["moy"] if ue_status else ""
args["moy_ue"] = moy_ue
log("formsemestre_validate_ue: create %s" % args) log("formsemestre_validate_ue: create %s" % args)
if code is not None: if code is not None:
scolar_formsemestre_validation_create(cnx, args) scolar_formsemestre_validation_create(cnx, args)

View File

@ -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) ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})") flash(f"UE créée (code {ue.ue_code})")
else: else:
if not tf[2]["numero"]:
tf[2]["numero"] = 0
do_ue_edit(tf[2]) do_ue_edit(tf[2])
flash("UE modifiée") flash("UE modifiée")

View File

@ -793,6 +793,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{tf[1]} {tf[1]}
""" """
elif tf[0] == -1: elif tf[0] == -1:
if formsemestre:
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_status", "notes.formsemestre_status",
@ -800,6 +801,8 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
) )
) )
else:
return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept))
else: else:
if tf[2]["gestion_compensation_lst"]: if tf[2]["gestion_compensation_lst"]:
tf[2]["gestion_compensation"] = True tf[2]["gestion_compensation"] = True

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,15 +44,14 @@ 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 from app.models import FormSemestre, Identite, Scolog
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN 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.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
@ -94,7 +92,7 @@ groupEditor = ndb.EditableTable(
group_list = groupEditor.list group_list = groupEditor.list
def get_group(group_id: int) -> dict: def get_group(group_id: int) -> dict: # OBSOLETE !
"""Returns group object, with partition""" """Returns group object, with partition"""
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
@ -124,7 +122,7 @@ def group_delete(group_id: int):
) )
def get_partition(partition_id): def get_partition(partition_id): # OBSOLETE
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* """SELECT p.id AS partition_id, p.*
FROM partition p FROM partition p
@ -200,7 +198,7 @@ def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict:
return d return d
def get_partition_groups(partition): def get_partition_groups(partition): # OBSOLETE !
"""List of groups in this partition (list of dicts). """List of groups in this partition (list of dicts).
Some groups may be empty.""" Some groups may be empty."""
return ndb.SimpleDictFetch( return ndb.SimpleDictFetch(
@ -637,7 +635,7 @@ def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre):
return "" # parcours normal, ne le signale pas 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. """Inscrit l'étudiant au groupe.
Return True if ok, False si deja inscrit. Return True if ok, False si deja inscrit.
Warning: Warning:
@ -664,55 +662,33 @@ def set_group(etudid: int, group_id: int) -> bool:
return True return True
def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None): def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> bool:
"""Inscrit etud au groupe de cette partition, """Inscrit etud au groupe
et le desinscrit 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.
""" """
log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id)) etud: Identite = Identite.query.get_or_404(etudid)
# 0- La partition if not group.partition.set_etud_group(etud, group):
group = get_group(group_id) return # pas de changement
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)
# 3- log # - log
formsemestre_id = partition["formsemestre_id"] formsemestre: FormSemestre = group.partition.formsemestre
cnx = ndb.GetDBConnexion() log(f"change_etud_group_in_partition: etudid={etudid} group={group}")
logdb( Scolog.logdb(
cnx,
method="changeGroup", method="changeGroup",
etudid=etudid, etudid=etudid,
msg="formsemestre_id=%s,partition_name=%s, group_name=%s" msg=f"""formsemestre_id={formsemestre.id}, partition_name={
% (formsemestre_id, partition["partition_name"], group["group_name"]), group.partition.partition_name or ""}, group_name={group.group_name or ""}""",
commit=True,
) )
cnx.commit()
# 5- Update parcours # - Update parcours
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) if group.partition.partition_name == scu.PARTITION_PARCOURS:
formsemestre.update_inscriptions_parcours_from_groups() formsemestre.update_inscriptions_parcours_from_groups()
# 6- invalidate cache # - invalidate cache
sco_cache.invalidate_formsemestre( sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre.id
) # > change etud group ) # > change etud group
@ -729,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 = (
@ -739,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()
@ -769,26 +745,23 @@ def setGroups(
except ValueError: except ValueError:
log(f"setGroups: ignoring invalid group_id={group_id}") log(f"setGroups: ignoring invalid group_id={group_id}")
continue continue
group = get_group(group_id) group: GroupDescr = GroupDescr.query.get_or_404(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
change_etud_group_in_partition(etudid, group_id, partition) change_etud_group_in_partition(etudid, group)
# Retire les anciens membres: # Retire les anciens membres:
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},
@ -798,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:
@ -819,10 +792,10 @@ def setGroups(
return xml_error(msg, code=404) return xml_error(msg, code=404)
# Place dans ce groupe les etudiants indiqués: # Place dans ce groupe les etudiants indiqués:
for etudid in fs[1:-1]: 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 # 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>'
@ -835,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):
@ -856,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
@ -1400,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
@ -1427,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(
@ -1452,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
group_ids = [] groups = []
for group_name in group_names: for group_name in group_names:
if group_name.strip(): if group_name.strip():
group_ids.append(create_group(partition_id, group_name).id) 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] = [
@ -1481,16 +1458,19 @@ def groups_auto_repartition(partition_id=None):
# affect aux groupes: # affect aux groupes:
n = len(identdict) n = len(identdict)
igroup = 0 igroup = 0
nbgroups = len(group_ids) 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_id = group_ids[igroup] group = groups[igroup]
igroup = (igroup + 1) % nbgroups igroup = (igroup + 1) % nbgroups
change_etud_group_in_partition(etudid, group_id, partition) log(f"in {etudid} in group {group.id}")
log("%s in group %s" % (etudid, group_id)) change_etud_group_in_partition(etudid, group)
log(f"{etudid} in group {group.id}")
return flask.redirect(dest_url) return flask.redirect(dest_url)
@ -1498,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)
@ -1520,10 +1498,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 Si la partition existe déjà, ses groupes sont mis à jour (les groupes devenant
vides ne sont pas supprimés). vides ne sont pas supprimés).
""" """
# A RE-ECRIRE pour utiliser les modèles.
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
partition_name = str(partition_name) 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( ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
args={"formsemestre_id": formsemestre_id} args={"formsemestre_id": formsemestre_id}
) )
@ -1542,20 +1521,17 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
pid = partition_create( pid = partition_create(
formsemestre_id, partition_name=partition_name, redirect=False formsemestre_id, partition_name=partition_name, redirect=False
) )
partition = get_partition(pid) partition: Partition = Partition.query.get(pid)
groups = get_partition_groups(partition) groups = partition.groups
groups_by_names = {g["group_name"]: g for g in groups} groups_by_names = {g["group_name"]: g for g in groups}
for etape in etapes: for etape in etapes:
if not (etape in groups_by_names): if etape not in groups_by_names:
new_group = create_group(pid, etape) new_group = create_group(pid, etape)
g = get_group(new_group.id) # XXX transition: recupere old style dict groups_by_names[etape] = new_group
groups_by_names[etape] = g
# Place les etudiants dans les groupes # Place les etudiants dans les groupes
for i in ins: for i in ins:
if i["etape"]: if i["etape"]:
change_etud_group_in_partition( change_etud_group_in_partition(i["etudid"], groups_by_names[i["etape"]])
i["etudid"], groups_by_names[i["etape"]]["group_id"], partition
)
def do_evaluation_listeetuds_groups( def do_evaluation_listeetuds_groups(

View File

@ -639,10 +639,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
fields = adm_get_fields(titles, formsemestre_id) fields = adm_get_fields(titles, formsemestre_id)
idx_nom = None idx_nom = None
idx_prenom = None idx_prenom = None
for idx in fields: for idx, field in fields.items():
if fields[idx][0] == "nom": if field[0] == "nom":
idx_nom = idx idx_nom = idx
if fields[idx][0] == "prenom": if field[0] == "prenom":
idx_prenom = idx idx_prenom = idx
if (idx_nom is None) or (idx_prenom is None): if (idx_nom is None) or (idx_prenom is None):
log("fields indices=" + ", ".join([str(x) for x in fields])) log("fields indices=" + ", ".join([str(x) for x in fields]))
@ -664,21 +664,20 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom) # Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
nom = adm_normalize_string(line[idx_nom]) nom = adm_normalize_string(line[idx_nom])
prenom = adm_normalize_string(line[idx_prenom]) prenom = adm_normalize_string(line[idx_prenom])
if not (nom, prenom) in etuds_by_nomprenom: if (nom, prenom) not in etuds_by_nomprenom:
log( msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
"unable to find %s %s among members" % (line[idx_nom], line[idx_prenom]) diag.append(msg)
)
else: else:
etud = etuds_by_nomprenom[(nom, prenom)] etud = etuds_by_nomprenom[(nom, prenom)]
cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0] cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0]
# peuple les champs presents dans le tableau # peuple les champs presents dans le tableau
args = {} args = {}
for idx in fields: for idx, field in fields.items():
field_name, convertor = fields[idx] field_name, convertor = field
if field_name in modifiable_fields: if field_name in modifiable_fields:
try: try:
val = convertor(line[idx]) val = convertor(line[idx])
except ValueError: except ValueError as exc:
raise ScoFormatError( raise ScoFormatError(
'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"' 'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"'
% (nline, field_name, line[idx]), % (nline, field_name, line[idx]),
@ -687,7 +686,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
), ),
) ) from exc
if val is not None: # note: ne peut jamais supprimer une valeur if val is not None: # note: ne peut jamais supprimer une valeur
args[field_name] = val args[field_name] = val
if args: if args:
@ -723,7 +722,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
group = GroupDescr.query.get(group_id) group = GroupDescr.query.get(group_id)
if group.partition.groups_editable: if group.partition.groups_editable:
sco_groups.change_etud_group_in_partition( sco_groups.change_etud_group_in_partition(
args["etudid"], group_id args["etudid"], group
) )
else: else:
log("scolars_import_admission: partition non editable") log("scolars_import_admission: partition non editable")

View File

@ -36,13 +36,12 @@ from flask import url_for, g, request
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log 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.gen_tables import GenTable
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
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups 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) (la liste doit avoir été vérifiée au préalable)
En option: inscrit aux mêmes groupes que dans le semestre origine 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: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"])
formsemestre.setup_parcours_groups() formsemestre.setup_parcours_groups()
log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}")
@ -220,11 +220,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
# Inscrit aux groupes # Inscrit aux groupes
for partition_group in partition_groups: for partition_group in partition_groups:
sco_groups.change_etud_group_in_partition( group: GroupDescr = GroupDescr.query.get(partition_group["group_id"])
etudid, sco_groups.change_etud_group_in_partition(etudid, group)
partition_group["group_id"],
partition_group,
)
def do_desinscrit(sem, etudids): def do_desinscrit(sem, etudids):

View File

@ -84,6 +84,8 @@ def SU(s: str) -> str:
s = html.unescape(s) s = html.unescape(s)
# Remplace les <br> par des <br/> # Remplace les <br> par des <br/>
s = re.sub(r"<br\s*>", "<br/>", s) s = re.sub(r"<br\s*>", "<br/>", s)
# And substitute unicode characters not supported by ReportLab
s = s.replace("", "-")
return s return s

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é

View File

@ -2339,14 +2339,14 @@ def formsemestre_validation_but(
formsemestre: FormSemestre = FormSemestre.query.filter_by( formsemestre: FormSemestre = FormSemestre.query.filter_by(
id=formsemestre_id, dept_id=g.scodoc_dept_id id=formsemestre_id, dept_id=g.scodoc_dept_id
).first_or_404() ).first_or_404()
etud = Identite.get_etud(etudid)
nb_etuds = formsemestre.etuds.count()
# la route ne donne pas le type d'etudid pour pouvoir construire des URLs # la route ne donne pas le type d'etudid pour pouvoir construire des URLs
# provisoires avec NEXT et PREV # provisoires avec NEXT et PREV
try: try:
etudid = int(etudid) etudid = int(etudid)
except ValueError: except ValueError as exc:
abort(404, "invalid etudid") raise ScoValueError("adresse invalide") from exc
etud = Identite.get_etud(etudid)
nb_etuds = formsemestre.etuds.count()
read_only = not formsemestre.can_edit_jury() read_only = not formsemestre.can_edit_jury()
# --- Navigation # --- Navigation

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.97" SCOVERSION = "9.4.99"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"