# -*- mode: python -*- # -*- coding: utf-8 -*- ############################################################################## # # Gestion scolarite IUT # # Copyright (c) 1999 - 2024 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@viennet.net # ############################################################################## """Form. pour inscription rapide des etudiants d'un semestre dans un autre Utilise les autorisations d'inscription délivrées en jury. """ import datetime from operator import itemgetter from flask import url_for, g, render_template, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import db, log from app.models import Formation, FormSemestre, GroupDescr, Identite from app.scodoc.gen_tables import GenTable from app.scodoc import sco_cache from app.scodoc import codes_cursus from app.scodoc import sco_etud from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_preferences from app.scodoc import sco_pv_dict from app.scodoc.sco_exceptions import ScoValueError def _list_authorized_etuds_by_sem( formsemestre: FormSemestre, ignore_jury=False ) -> tuple[dict[int, dict], list[dict], dict[int, Identite]]: """Liste des etudiants autorisés à s'inscrire dans sem. delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible. ignore_jury: si vrai, considère tous les étudiants comme autorisés, même s'ils n'ont pas de décision de jury. """ src_sems = _list_source_sems(formsemestre) inscrits = list_inscrits(formsemestre.id) r = {} candidats = {} # etudid : etud (tous les etudiants candidats) nb = 0 # debug src_formsemestre: FormSemestre for src_formsemestre in src_sems: if ignore_jury: # liste de tous les inscrits au semestre (sans dems) etud_list = list_inscrits(src_formsemestre.id).values() else: # liste des étudiants autorisés par le jury à s'inscrire ici etud_list = _list_etuds_from_sem(src_formsemestre, formsemestre) liste_filtree = [] for e in etud_list: # Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src auth_used = False # autorisation deja utilisée ? etud = Identite.get_etud(e["etudid"]) for inscription in etud.inscriptions(): if inscription.formsemestre.date_debut >= src_formsemestre.date_fin: auth_used = True if not auth_used: candidats[e["etudid"]] = etud liste_filtree.append(e) nb += 1 r[src_formsemestre.id] = { "etuds": liste_filtree, "infos": { "id": src_formsemestre.id, "title": src_formsemestre.titre_annee(), "title_target": url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=src_formsemestre.id, ), "filename": "etud_autorises", }, } # ajoute attribut inscrit qui indique si l'étudiant est déjà inscrit dans le semestre dest. for e in r[src_formsemestre.id]["etuds"]: e["inscrit"] = e["etudid"] in inscrits # Ajoute liste des etudiants actuellement inscrits for e in inscrits.values(): e["inscrit"] = True r[formsemestre.id] = { "etuds": list(inscrits.values()), "infos": { "id": formsemestre.id, "title": "Semestre cible: " + formsemestre.titre_annee(), "title_target": url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ), "comment": " actuellement inscrits dans ce semestre", "help": "Ces étudiants sont actuellement inscrits dans ce semestre. Si vous les décochez, il seront désinscrits.", "filename": "etud_inscrits", }, } return r, inscrits, candidats def list_inscrits(formsemestre_id: int, with_dems=False) -> list[dict]: """Étudiants déjà inscrits à ce semestre { etudid : etud } """ if not with_dems: ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits( formsemestre_id ) # optimized else: args = {"formsemestre_id": formsemestre_id} ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(args=args) inscr = {} for i in ins: etudid = i["etudid"] inscr[etudid] = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] return inscr def _list_etuds_from_sem(src: FormSemestre, dst: FormSemestre) -> list[dict]: """Liste des étudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" target_semestre_id = dst.semestre_id dpv = sco_pv_dict.dict_pvjury(src.id) if not dpv: return [] etuds = [ x["identite"] for x in dpv["decisions"] if target_semestre_id in [a["semestre_id"] for a in x["autorisations"]] ] return etuds def list_inscrits_date(formsemestre: FormSemestre): """Liste les etudiants inscrits à la date de début de formsemestre dans n'importe quel semestre du même département SAUF formsemestre """ cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( """SELECT ins.etudid FROM notes_formsemestre_inscription ins, notes_formsemestre S WHERE ins.formsemestre_id = S.id AND S.id != %(formsemestre_id)s AND S.date_debut <= %(date_debut_iso)s AND S.date_fin >= %(date_debut_iso)s AND S.dept_id = %(dept_id)s """, { "formsemestre_id": formsemestre.id, "date_debut_iso": formsemestre.date_debut.isoformat(), "dept_id": formsemestre.dept_id, }, ) return [x[0] for x in cursor.fetchall()] def do_inscrit( formsemestre: FormSemestre, etudids, inscrit_groupes=False, inscrit_parcours=False ): """Inscrit ces etudiants dans ce semestre (la liste doit avoir été vérifiée au préalable) En option: - Si inscrit_groupes, inscrit aux mêmes groupes que dans le semestre origine (toutes partitions, y compris parcours) - Si inscrit_parcours, inscrit au même groupe de parcours (mais ignore les autres partitions) (si les deux sont vrais, inscrit_parcours n'a pas d'effet) """ # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr formsemestre.setup_parcours_groups() log(f"do_inscrit (inscrit_groupes={inscrit_groupes}): {etudids}") for etudid in etudids: sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( formsemestre.id, etudid, etat=scu.INSCRIT, method="formsemestre_inscr_passage", ) if inscrit_groupes or inscrit_parcours: # Inscription dans les mêmes groupes que ceux du semestre d'origine, # s'ils existent. # (mise en correspondance à partir du nom du groupe, sans tenir compte # du nom de la partition: évidemment, cela ne marche pas si on a les # même noms de groupes dans des partitions différentes) etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] # recherche le semestre origine (il serait plus propre de l'avoir conservé!) if len(etud["sems"]) < 2: continue prev_formsemestre = etud["sems"][1] sco_groups.etud_add_group_infos( etud, prev_formsemestre["formsemestre_id"] if prev_formsemestre else None, ) cursem_groups_by_name = { g["group_name"]: g for g in sco_groups.get_sem_groups(formsemestre.id) if g["group_name"] } # forme la liste des groupes présents dans les deux semestres: partition_groups = [] # [ partition+group ] (ds nouveau sem.) for partition_id in etud["partitions"]: prev_group_name = etud["partitions"][partition_id]["group_name"] if prev_group_name in cursem_groups_by_name: new_group = cursem_groups_by_name[prev_group_name] partition_groups.append(new_group) # Inscrit aux groupes for partition_group in partition_groups: group: GroupDescr = db.session.get( GroupDescr, partition_group["group_id"] ) if inscrit_groupes or ( group.partition.partition_name == scu.PARTITION_PARCOURS and inscrit_parcours ): sco_groups.change_etud_group_in_partition(etudid, group) def do_desinscrit( formsemestre: FormSemestre, etudids: list[int], check_has_dec_jury=True ): "désinscrit les étudiants indiqués du formsemestre" log(f"do_desinscrit: {etudids}") for etudid in etudids: sco_formsemestre_inscriptions.do_formsemestre_desinscription( etudid, formsemestre.id, check_has_dec_jury=check_has_dec_jury ) def _list_source_sems(formsemestre: FormSemestre) -> list[FormSemestre]: """Liste des semestres sources formsemestre est le semestre destination """ # liste des semestres du même type de cursus terminant # pas trop loin de la date de début du semestre destination date_fin_min = formsemestre.date_debut - datetime.timedelta(days=275) date_fin_max = formsemestre.date_debut + datetime.timedelta(days=45) return ( FormSemestre.query.filter( FormSemestre.dept_id == formsemestre.dept_id, # saute le semestre destination: FormSemestre.id != formsemestre.id, # et les semestres de formations speciales (monosemestres): FormSemestre.semestre_id != codes_cursus.NO_SEMESTRE_ID, # semestre pas trop dans le futur FormSemestre.date_fin <= date_fin_max, # ni trop loin dans le passé FormSemestre.date_fin >= date_fin_min, ) .join(Formation) .filter_by(type_parcours=formsemestre.formation.type_parcours) ).all() # view, GET, POST def formsemestre_inscr_passage( formsemestre_id, etuds: str | list[int] | list[str] | int | None = None, inscrit_groupes=False, inscrit_parcours=False, submitted=False, dialog_confirmed=False, ignore_jury=False, ) -> str: """Page Form. pour inscription des etudiants d'un semestre dans un autre (donné par formsemestre_id). Permet de selectionner parmi les etudiants autorisés à s'inscrire. Principe: - trouver liste d'etud, par semestre - afficher chaque semestre "boites" avec cases à cocher - si l'étudiant est déjà inscrit, le signaler (gras, nom de groupes): il peut être désinscrit - on peut choisir les groupes TD, TP, TA - seuls les étudiants non inscrits changent (de groupe) - les étudiants inscrit qui se trouvent décochés sont désinscrits - Confirmation: indiquer les étudiants inscrits et ceux désinscrits, le total courant. """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) inscrit_groupes = int(inscrit_groupes) inscrit_parcours = int(inscrit_parcours) ignore_jury = int(ignore_jury) # -- check lock if not formsemestre.etat: raise ScoValueError("opération impossible: semestre verrouille") H = [] etuds = [] if etuds is None else etuds if isinstance(etuds, str): # string, vient du form de confirmation etuds = [int(x) for x in etuds.split(",") if x] elif isinstance(etuds, int): etuds = [etuds] elif etuds and isinstance(etuds[0], str): etuds = [int(x) for x in etuds] auth_etuds_by_sem, inscrits, candidats = _list_authorized_etuds_by_sem( formsemestre, ignore_jury=ignore_jury ) etuds_set = set(etuds) candidats_set = set(candidats) inscrits_set = set(inscrits) candidats_non_inscrits = candidats_set - inscrits_set inscrits_ailleurs = set(list_inscrits_date(formsemestre)) def set_to_sorted_etud_list(etudset) -> list[Identite]: etuds = [candidats[etudid] for etudid in etudset] etuds.sort(key=lambda e: e.sort_key) return etuds if submitted: a_inscrire = etuds_set.intersection(candidats_set) - inscrits_set a_desinscrire = inscrits_set - etuds_set else: a_inscrire = a_desinscrire = [] if not submitted: H += _build_page( formsemestre, auth_etuds_by_sem, inscrits, candidats_non_inscrits, inscrits_ailleurs, inscrit_groupes=inscrit_groupes, inscrit_parcours=inscrit_parcours, ignore_jury=ignore_jury, with_apo_cols=False, ) else: if not dialog_confirmed: # Confirmation if a_inscrire: H.append("
Confirmer ?
" if todo else "", add_headers=False, cancel_url="formsemestre_inscr_passage?formsemestre_id=" + str(formsemestre_id), OK="Effectuer l'opération" if todo else "", parameters={ "formsemestre_id": formsemestre_id, "etuds": ",".join([str(x) for x in etuds]), "inscrit_groupes": inscrit_groupes, "inscrit_parcours": inscrit_parcours, "ignore_jury": ignore_jury, "submitted": 1, }, ) ) else: # check decisions jury ici pour éviter de recontruire le cache # après chaque desinscription sco_formsemestre_inscriptions.check_if_has_decision_jury( formsemestre, a_desinscrire ) # check decisions jury ici pour éviter de recontruire le cache # après chaque desinscription sco_formsemestre_inscriptions.check_if_has_decision_jury( formsemestre, a_desinscrire ) with sco_cache.DeferredSemCacheManager(): # Inscription des étudiants au nouveau semestre: do_inscrit( formsemestre, a_inscrire, inscrit_groupes=inscrit_groupes, inscrit_parcours=inscrit_parcours, ) # Désinscriptions: do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False) H.append( f"""Cette page permet d'inscrire des étudiants dans le semestre destination {formsemestre.titre_annee()}, et d'en désincrire si besoin.
Les étudiants sont groupés par semestre d'origine. Ceux qui sont en caractères gras sont déjà inscrits dans le semestre destination. Ceux qui sont en gras et en rouge sont inscrits dans un autre semestre.
Au départ, les étudiants déjà inscrits sont sélectionnés; vous pouvez ajouter d'autres étudiants à inscrire dans le semestre destination.
Si vous dé-selectionnez un étudiant déjà inscrit (en gras), il sera désinscrit.
Le bouton inscrire aux mêmes groupes ne prend en compte que les groupes qui existent dans les deux semestres: pensez à créer les partitions et groupes que vous souhaitez conserver avant d'inscrire les étudiants.
Les parcours de BUT sont gérés comme des groupes de la partition parcours: si on conserve les groupes, on conserve les parcours (là aussi, pensez à les cocher dans modifier le semestre avant de faire passer les étudiants).
Aucune action ne sera effectuée si vous n'appuyez pas sur le bouton "Appliquer les modifications" !