diff --git a/app/api/partitions.py b/app/api/partitions.py index 1f5ef8a1e..4d752b65a 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -12,6 +12,7 @@ from operator import attrgetter from flask import g, request from flask_json import as_json from flask_login import login_required +from sqlalchemy.exc import IntegrityError import app from app import db, log @@ -23,6 +24,7 @@ from app.models import GroupDescr, Partition, Scolog from app.models.groups import group_membership from app.scodoc import sco_cache from app.scodoc import sco_groups +from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu @@ -182,10 +184,12 @@ def set_etud_group(etudid: int, group_id: int): if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return json_error(404, "etud non inscrit au formsemestre du groupe") - sco_groups.change_etud_group_in_partition( - etudid, group_id, group.partition.to_dict() - ) - + try: + sco_groups.change_etud_group_in_partition(etudid, group) + except ScoValueError as exc: + return json_error(404, exc.args[0]) + except IntegrityError: + return json_error(404, "échec de l'enregistrement") return {"group_id": group_id, "etudid": etudid} diff --git a/app/but/jury_but.py b/app/but/jury_but.py index 02f18dffa..fc4faf20e 100644 --- a/app/but/jury_but.py +++ b/app/but/jury_but.py @@ -865,11 +865,16 @@ class DecisionsProposeesAnnee(DecisionsProposees): self.etud.id, self.formsemestre.id ) 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() else: for dec_ue in self.decisions_ues.values(): - dec_ue.erase() + if dec_ue: + dec_ue.erase() if self.formsemestre_impair: ScolarAutorisationInscription.delete_autorisation_etud( diff --git a/app/comp/bonus_spo.py b/app/comp/bonus_spo.py index d3e8db03e..b5b4e966b 100644 --- a/app/comp/bonus_spo.py +++ b/app/comp/bonus_spo.py @@ -827,16 +827,32 @@ class BonusStMalo(BonusIUTRennes1): class BonusLaRocheSurYon(BonusSportAdditif): """Bonus IUT de La Roche-sur-Yon - 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. +

+ La note saisie s'applique directement: 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. +

+

Pour les semestres antérieurs à janvier 2023: 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. +

""" name = "bonus_larochesuryon" displayed_name = "IUT de La Roche-sur-Yon" seuil_moy_gen = 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): diff --git a/app/comp/res_common.py b/app/comp/res_common.py index e59172170..54fd0de03 100644 --- a/app/comp/res_common.py +++ b/app/comp/res_common.py @@ -381,7 +381,11 @@ class ResultatsSemestre(ResultatsCache): was_capitalized = False if etudid in self.validations.ue_capitalisees.index: 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 if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): moy_ue = ue_cap["moy_ue"] diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index d43bb09bd..2a11749b9 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -575,6 +575,17 @@ class FormSemestre(db.Model): user ) + def can_change_groups(self, user: User = None) -> bool: + """Vrai si l'utilisateur (par def. current) peut changer les groupes dans + ce semestre: vérifie permission et verrouillage. + """ + if not self.etat: + return False # semestre verrouillé + user = user or current_user + if user.has_permission(Permission.ScoEtudChangeGroups): + return True # typiquement admin, chef dept + return self.est_responsable(user) + def can_edit_jury(self, user: User = None): """Vrai si utilisateur (par def. current) peut saisir decision de jury dans ce semestre: vérifie permission et verrouillage. diff --git a/app/models/groups.py b/app/models/groups.py index 1d07ed214..8e9d5b620 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -8,11 +8,13 @@ """ScoDoc models: Groups & partitions """ from operator import attrgetter +from sqlalchemy.exc import IntegrityError -from app import db +from app import db, log from app.models import SHORT_STR_LEN from app.models import GROUPNAME_STR_LEN from app.scodoc import sco_utils as scu +from app.scodoc.sco_exceptions import AccessDenied, ScoValueError class Partition(db.Model): @@ -117,6 +119,81 @@ class Partition(db.Model): .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): """Description d'un groupe d'une partition""" diff --git a/app/scodoc/sco_cursus_dut.py b/app/scodoc/sco_cursus_dut.py index 9eab3da6b..007c602c5 100644 --- a/app/scodoc/sco_cursus_dut.py +++ b/app/scodoc/sco_cursus_dut.py @@ -949,6 +949,7 @@ def do_formsemestre_validate_ue( "ue_id": ue_id, "semestre_id": semestre_id, "is_external": is_external, + "moy_ue": moy_ue, } if date: args["event_date"] = date @@ -965,12 +966,11 @@ def do_formsemestre_validate_ue( cursor.execute("delete from scolar_formsemestre_validation where " + cond, args) # insert args["code"] = code - if code == ADM: - if moy_ue is None: - # stocke la moyenne d'UE capitalisée: - ue_status = nt.get_etud_ue_status(etudid, ue_id) - moy_ue = ue_status["moy"] if ue_status else "" - args["moy_ue"] = moy_ue + if (code == ADM) and (moy_ue is None): + # stocke la moyenne d'UE capitalisée: + ue_status = nt.get_etud_ue_status(etudid, ue_id) + moy_ue = ue_status["moy"] if ue_status else "" + log("formsemestre_validate_ue: create %s" % args) if code is not None: scolar_formsemestre_validation_create(cnx, args) diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index 90f01f04e..4d7aa131b 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -546,6 +546,8 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No ue = UniteEns.query.get(ue_id) flash(f"UE créée (code {ue.ue_code})") else: + if not tf[2]["numero"]: + tf[2]["numero"] = 0 do_ue_edit(tf[2]) flash("UE modifiée") diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 67410b5c1..9c8466ed2 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -793,13 +793,16 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N {tf[1]} """ elif tf[0] == -1: - return redirect( - url_for( - "notes.formsemestre_status", - scodoc_dept=g.scodoc_dept, - formsemestre_id=formsemestre.id, + if formsemestre: + return redirect( + url_for( + "notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, + formsemestre_id=formsemestre.id, + ) ) - ) + else: + return redirect(url_for("notes.index_html", scodoc_dept=g.scodoc_dept)) else: if tf[2]["gestion_compensation_lst"]: tf[2]["gestion_compensation"] = True diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index 8b1da28ad..ab3e014d5 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -34,7 +34,6 @@ Optimisation possible: """ import collections -import operator import time from xml.etree import ElementTree @@ -45,15 +44,14 @@ from flask import g, request from flask import url_for, make_response from sqlalchemy.sql import text -from app import db +from app import cache, db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat -from app.models import FormSemestre, Identite +from app.models import FormSemestre, Identite, Scolog from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN -from app.models.groups import GroupDescr, Partition +from app.models.groups import GroupDescr, Partition, group_membership import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb -from app import log, cache from app.scodoc.scolog import logdb from app.scodoc import html_sco_header from app.scodoc import sco_cache @@ -94,7 +92,7 @@ groupEditor = ndb.EditableTable( group_list = groupEditor.list -def get_group(group_id: int) -> dict: +def get_group(group_id: int) -> dict: # OBSOLETE ! """Returns group object, with partition""" r = ndb.SimpleDictFetch( """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* @@ -124,7 +122,7 @@ def group_delete(group_id: int): ) -def get_partition(partition_id): +def get_partition(partition_id): # OBSOLETE r = ndb.SimpleDictFetch( """SELECT p.id AS partition_id, p.* FROM partition p @@ -200,7 +198,7 @@ def get_formsemestre_etuds_groups(formsemestre_id: int) -> dict: return d -def get_partition_groups(partition): +def get_partition_groups(partition): # OBSOLETE ! """List of groups in this partition (list of dicts). Some groups may be empty.""" return ndb.SimpleDictFetch( @@ -637,7 +635,7 @@ def _comp_etud_origin(etud: dict, cur_formsemestre: FormSemestre): return "" # parcours normal, ne le signale pas -def set_group(etudid: int, group_id: int) -> bool: +def set_group(etudid: int, group_id: int) -> bool: # OBSOLETE ! """Inscrit l'étudiant au groupe. Return True if ok, False si deja inscrit. Warning: @@ -664,55 +662,33 @@ def set_group(etudid: int, group_id: int) -> bool: return True -def change_etud_group_in_partition(etudid: int, group_id: int, partition: dict = None): - """Inscrit etud au groupe de cette partition, - et le desinscrit d'autres groupes de cette partition. +def change_etud_group_in_partition(etudid: int, group: GroupDescr) -> 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. """ - log("change_etud_group_in_partition: etudid=%s group_id=%s" % (etudid, group_id)) - # 0- La partition - group = get_group(group_id) - if partition: - # verifie que le groupe est bien dans cette partition: - if group["partition_id"] != partition["partition_id"]: - raise ValueError( - "inconsistent group/partition (group_id=%s, partition_id=%s)" - % (group_id, partition["partition_id"]) - ) - else: - partition = get_partition(group["partition_id"]) - # 1- Supprime membership dans cette partition - ndb.SimpleQuery( - """DELETE FROM group_membership gm - WHERE EXISTS - (SELECT 1 FROM group_descr gd - WHERE gm.etudid = %(etudid)s - AND gm.group_id = gd.id - AND gd.partition_id = %(partition_id)s) - """, - {"etudid": etudid, "partition_id": partition["partition_id"]}, - ) - # 2- associe au nouveau groupe - set_group(etudid, group_id) + etud: Identite = Identite.query.get_or_404(etudid) + if not group.partition.set_etud_group(etud, group): + return # pas de changement - # 3- log - formsemestre_id = partition["formsemestre_id"] - cnx = ndb.GetDBConnexion() - logdb( - cnx, + # - log + formsemestre: FormSemestre = group.partition.formsemestre + log(f"change_etud_group_in_partition: etudid={etudid} group={group}") + Scolog.logdb( method="changeGroup", etudid=etudid, - msg="formsemestre_id=%s,partition_name=%s, group_name=%s" - % (formsemestre_id, partition["partition_name"], group["group_name"]), + msg=f"""formsemestre_id={formsemestre.id}, partition_name={ + group.partition.partition_name or ""}, group_name={group.group_name or ""}""", + commit=True, ) - cnx.commit() - # 5- Update parcours - formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) - formsemestre.update_inscriptions_parcours_from_groups() + # - Update parcours + if group.partition.partition_name == scu.PARTITION_PARCOURS: + formsemestre.update_inscriptions_parcours_from_groups() - # 6- invalidate cache + # - invalidate cache sco_cache.invalidate_formsemestre( - formsemestre_id=formsemestre_id + formsemestre_id=formsemestre.id ) # > change etud group @@ -729,7 +705,6 @@ def setGroups( Ne peux pas modifier les groupes des partitions non éditables. """ - from app.scodoc import sco_formsemestre def xml_error(msg, code=404): data = ( @@ -739,26 +714,27 @@ def setGroups( response.headers["Content-Type"] = scu.XML_MIMETYPE return response - partition = get_partition(partition_id) - if not partition["groups_editable"] and (groupsToCreate or groupsToDelete): + partition: Partition = Partition.query.get(partition_id) + if not partition.groups_editable and (groupsToCreate or groupsToDelete): msg = "setGroups: partition non editable" log(msg) return xml_error(msg, code=403) - formsemestre_id = partition["formsemestre_id"] - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) - if not sco_permissions_check.can_change_groups(formsemestre_id): + + if not sco_permissions_check.can_change_groups(partition.formsemestre.id): raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") log("***setGroups: partition_id=%s" % partition_id) log("groupsLists=%s" % groupsLists) log("groupsToCreate=%s" % groupsToCreate) log("groupsToDelete=%s" % groupsToDelete) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) - if not sem["etat"]: + + if not partition.formsemestre.etat: raise AccessDenied("Modification impossible: semestre verrouillé") groupsToDelete = [g for g in groupsToDelete.split(";") if g] - etud_groups = formsemestre_get_etud_groupnames(formsemestre_id, attr="group_id") + etud_groups = formsemestre_get_etud_groupnames( + partition.formsemestre.id, attr="group_id" + ) for line in groupsLists.split("\n"): # for each group_id (one per line) fs = line.split(";") group_id = fs[0].strip() @@ -769,26 +745,23 @@ def setGroups( except ValueError: log(f"setGroups: ignoring invalid group_id={group_id}") continue - group = get_group(group_id) + group: GroupDescr = GroupDescr.query.get_or_404(group_id) # Anciens membres du groupe: - old_members = get_group_members(group_id) - old_members_set = set([x["etudid"] for x in old_members]) + old_members_set = {etud.id for etud in group.etuds} # Place dans ce groupe les etudiants indiqués: for etudid_str in fs[1:-1]: etudid = int(etudid_str) if etudid in old_members_set: - old_members_set.remove( - etudid - ) # a nouveau dans ce groupe, pas besoin de l'enlever + # était dans ce groupe, l'enlever + old_members_set.remove(etudid) if (etudid not in etud_groups) or ( group_id != etud_groups[etudid].get(partition_id, "") ): # pas le meme groupe qu'actuel - change_etud_group_in_partition(etudid, group_id, partition) + change_etud_group_in_partition(etudid, group) # Retire les anciens membres: cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) for etudid in old_members_set: - log("removing %s from group %s" % (etudid, group_id)) ndb.SimpleQuery( "DELETE FROM group_membership WHERE etudid=%(etudid)s and group_id=%(group_id)s", {"etudid": etudid, "group_id": group_id}, @@ -798,8 +771,8 @@ def setGroups( cnx, method="removeFromGroup", etudid=etudid, - msg="formsemestre_id=%s,partition_name=%s, group_name=%s" - % (formsemestre_id, partition["partition_name"], group["group_name"]), + msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={ + partition.partition_name}, group_name={group.group_name}""", ) # Supprime les groupes indiqués comme supprimés: @@ -819,10 +792,10 @@ def setGroups( return xml_error(msg, code=404) # Place dans ce groupe les etudiants indiqués: for etudid in fs[1:-1]: - change_etud_group_in_partition(etudid, group.id, partition) + change_etud_group_in_partition(etudid, group.id) # Update parcours - formsemestre.update_inscriptions_parcours_from_groups() + partition.formsemestre.update_inscriptions_parcours_from_groups() data = ( 'Groupes enregistrés' @@ -835,6 +808,7 @@ def setGroups( 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 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) db.session.add(group) db.session.commit() - log("create_group: created group_id={group.id}") + log(f"create_group: created group_id={group.id}") # 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 et la mixité. """ - partition = get_partition(partition_id) - if not partition["groups_editable"]: + partition: Partition = Partition.query.get_or_404(partition_id) + if not partition.groups_editable: raise AccessDenied("Partition non éditable") - formsemestre_id = partition["formsemestre_id"] - formsemestre = FormSemestre.get_formsemestre(formsemestre_id) + formsemestre_id = partition.formsemestre_id + formsemestre = partition.formsemestre # renvoie sur page édition groupes dest_url = url_for( "scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id @@ -1427,12 +1401,14 @@ def groups_auto_repartition(partition_id=None): H = [ html_sco_header.sco_header(page_title="Répartition des groupes"), - "

Répartition des groupes de %s

" % partition["partition_name"], - f"

Semestre {formsemestre.titre_annee()}

", - """

Les groupes existants seront effacés et remplacés par + f"""

Répartition des groupes de {partition.partition_name}

+

Semestre {formsemestre.titre_annee()}

", +

Les groupes existants seront effacés et remplacés par ceux créés ici. La répartition aléatoire tente d'uniformiser le niveau des groupes (en utilisant la dernière moyenne générale disponible pour - chaque étudiant) et de maximiser la mixité de chaque groupe.

""", + chaque étudiant) et de maximiser la mixité de chaque groupe. +

+ """, ] tf = TrivialFormulator( @@ -1452,23 +1428,24 @@ def groups_auto_repartition(partition_id=None): # form submission log( "groups_auto_repartition( partition_id=%s partition_name=%s" - % (partition_id, partition["partition_name"]) + % (partition_id, partition.partition_name) ) groupNames = tf[2]["groupNames"] - group_names = sorted(set([x.strip() for x in groupNames.split(",")])) + group_names = sorted({x.strip() for x in groupNames.split(",")}) # Détruit les groupes existant de cette partition - for old_group in get_partition_groups(partition): - group_delete(old_group["group_id"]) + for group in partition.groups: + db.session.delete(group) + db.session.commit() # Crée les nouveaux groupes - group_ids = [] + groups = [] for group_name in group_names: if group_name.strip(): - group_ids.append(create_group(partition_id, group_name).id) + groups.append(partition.create_group(group_name)) # nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) identdict = nt.identdict # build: { civilite : liste etudids trie par niveau croissant } - civilites = set([x["civilite"] for x in identdict.values()]) + civilites = {x["civilite"] for x in identdict.values()} listes = {} for civilite in civilites: listes[civilite] = [ @@ -1481,16 +1458,19 @@ def groups_auto_repartition(partition_id=None): # affect aux groupes: n = len(identdict) igroup = 0 - nbgroups = len(group_ids) + nbgroups = len(groups) while n > 0: + log(f"n={n}") for civilite in civilites: + log(f"civilite={civilite}") if len(listes[civilite]): n -= 1 etudid = listes[civilite].pop()[1] - group_id = group_ids[igroup] + group = groups[igroup] igroup = (igroup + 1) % nbgroups - change_etud_group_in_partition(etudid, group_id, partition) - log("%s in group %s" % (etudid, group_id)) + 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) @@ -1498,8 +1478,6 @@ def _get_prev_moy(etudid, formsemestre_id): """Donne la derniere moyenne generale calculee pour cette étudiant, ou 0 si on n'en trouve pas (nouvel inscrit,...). """ - from app.scodoc import sco_cursus_dut - info = sco_etud.get_etud_info(etudid=etudid, filled=True) if not info: raise ScoValueError("etudiant invalide: etudid=%s" % etudid) @@ -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 vides ne sont pas supprimés). """ + # A RE-ECRIRE pour utiliser les modèles. from app.scodoc import sco_formsemestre_inscriptions partition_name = str(partition_name) - log("create_etapes_partition(%s)" % formsemestre_id) + log(f"create_etapes_partition({formsemestre_id})") ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id} ) @@ -1542,20 +1521,17 @@ def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"): pid = partition_create( formsemestre_id, partition_name=partition_name, redirect=False ) - partition = get_partition(pid) - groups = get_partition_groups(partition) + partition: Partition = Partition.query.get(pid) + groups = partition.groups groups_by_names = {g["group_name"]: g for g in groups} for etape in etapes: - if not (etape in groups_by_names): + if etape not in groups_by_names: new_group = create_group(pid, etape) - g = get_group(new_group.id) # XXX transition: recupere old style dict - groups_by_names[etape] = g + groups_by_names[etape] = new_group # Place les etudiants dans les groupes for i in ins: if i["etape"]: - change_etud_group_in_partition( - i["etudid"], groups_by_names[i["etape"]]["group_id"], partition - ) + change_etud_group_in_partition(i["etudid"], groups_by_names[i["etape"]]) def do_evaluation_listeetuds_groups( diff --git a/app/scodoc/sco_import_etuds.py b/app/scodoc/sco_import_etuds.py index e7eb8dceb..3141c412f 100644 --- a/app/scodoc/sco_import_etuds.py +++ b/app/scodoc/sco_import_etuds.py @@ -639,10 +639,10 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None fields = adm_get_fields(titles, formsemestre_id) idx_nom = None idx_prenom = None - for idx in fields: - if fields[idx][0] == "nom": + for idx, field in fields.items(): + if field[0] == "nom": idx_nom = idx - if fields[idx][0] == "prenom": + if field[0] == "prenom": idx_prenom = idx if (idx_nom is None) or (idx_prenom is None): 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) nom = adm_normalize_string(line[idx_nom]) prenom = adm_normalize_string(line[idx_prenom]) - if not (nom, prenom) in etuds_by_nomprenom: - log( - "unable to find %s %s among members" % (line[idx_nom], line[idx_prenom]) - ) + if (nom, prenom) not in etuds_by_nomprenom: + msg = f"""Étudiant {line[idx_nom]} {line[idx_prenom]} inexistant""" + diag.append(msg) else: etud = etuds_by_nomprenom[(nom, prenom)] cur_adm = sco_etud.admission_list(cnx, args={"etudid": etud["etudid"]})[0] # peuple les champs presents dans le tableau args = {} - for idx in fields: - field_name, convertor = fields[idx] + for idx, field in fields.items(): + field_name, convertor = field if field_name in modifiable_fields: try: val = convertor(line[idx]) - except ValueError: + except ValueError as exc: raise ScoFormatError( 'scolars_import_admission: valeur invalide, ligne %d colonne %s: "%s"' % (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, formsemestre_id=formsemestre_id, ), - ) + ) from exc if val is not None: # note: ne peut jamais supprimer une valeur args[field_name] = val if args: @@ -723,7 +722,7 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None group = GroupDescr.query.get(group_id) if group.partition.groups_editable: sco_groups.change_etud_group_in_partition( - args["etudid"], group_id + args["etudid"], group ) else: log("scolars_import_admission: partition non editable") diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index 1ebf679fb..50260e270 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -36,13 +36,12 @@ from flask import url_for, g, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log -from app.models import Formation, FormSemestre +from app.models import Formation, FormSemestre, GroupDescr from app.scodoc.gen_tables import GenTable from app.scodoc import html_sco_header from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_etud -from app.scodoc import sco_formations from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups @@ -177,6 +176,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False): (la liste doit avoir été vérifiée au préalable) En option: inscrit aux mêmes groupes que dans le semestre origine """ + # TODO à ré-écrire pour utiliser le smodèle, notamment GroupDescr formsemestre: FormSemestre = FormSemestre.query.get(sem["formsemestre_id"]) formsemestre.setup_parcours_groups() log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") @@ -220,11 +220,8 @@ def do_inscrit(sem, etudids, inscrit_groupes=False): # Inscrit aux groupes for partition_group in partition_groups: - sco_groups.change_etud_group_in_partition( - etudid, - partition_group["group_id"], - partition_group, - ) + group: GroupDescr = GroupDescr.query.get(partition_group["group_id"]) + sco_groups.change_etud_group_in_partition(etudid, group) def do_desinscrit(sem, etudids): diff --git a/app/scodoc/sco_pdf.py b/app/scodoc/sco_pdf.py index ffa11d259..841f8b3fa 100755 --- a/app/scodoc/sco_pdf.py +++ b/app/scodoc/sco_pdf.py @@ -84,6 +84,8 @@ def SU(s: str) -> str: s = html.unescape(s) # Remplace les
par des
s = re.sub(r"", "
", s) + # And substitute unicode characters not supported by ReportLab + s = s.replace("‐", "-") return s diff --git a/app/scodoc/sco_permissions_check.py b/app/scodoc/sco_permissions_check.py index 224881bf8..dcc19f455 100644 --- a/app/scodoc/sco_permissions_check.py +++ b/app/scodoc/sco_permissions_check.py @@ -142,7 +142,9 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl def can_change_groups(formsemestre_id: int) -> bool: - "Vrai si l'utilisateur peut changer les groupes dans ce semestre" + """Vrai si l'utilisateur peut changer les groupes dans ce semestre + Obsolete: utiliser FormSemestre.can_change_groups + """ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) if not formsemestre.etat: return False # semestre verrouillé diff --git a/app/views/notes.py b/app/views/notes.py index 4bc79ebae..4ee25a900 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -2339,14 +2339,14 @@ def formsemestre_validation_but( formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).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 # provisoires avec NEXT et PREV try: etudid = int(etudid) - except ValueError: - abort(404, "invalid etudid") + except ValueError as exc: + raise ScoValueError("adresse invalide") from exc + etud = Identite.get_etud(etudid) + nb_etuds = formsemestre.etuds.count() read_only = not formsemestre.can_edit_jury() # --- Navigation diff --git a/sco_version.py b/sco_version.py index 0bd507e8e..6ef51420b 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.4.97" +SCOVERSION = "9.4.99" SCONAME = "ScoDoc"