# -*- 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, request import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import db, log from app.comp import res_sem from app.comp.res_compat import NotesTableCompat from app.models import Formation, FormSemestre, GroupDescr, Identite 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_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 = [ html_sco_header.sco_header( page_title="Passage des étudiants", init_qtip=True, javascripts=["js/etud_info.js"], ) ] footer = html_sco_header.sco_footer() 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, ) else: if not dialog_confirmed: # Confirmation if a_inscrire: H.append("

Étudiants à inscrire

    ") for etud in set_to_sorted_etud_list(a_inscrire): H.append(f"
  1. {etud.nomprenom}
  2. ") H.append("
") a_inscrire_en_double = inscrits_ailleurs.intersection(a_inscrire) if a_inscrire_en_double: H.append("

dont étudiants déjà inscrits:

") if a_desinscrire: H.append("

Étudiants à désinscrire

    ") a_desinscrire_ident = sorted( (db.session.get(Identite, eid) for eid in a_desinscrire), key=lambda x: x.sort_key, ) for etud in a_desinscrire_ident: H.append(f'
  1. {etud.nomprenom}
  2. ') H.append("
") todo = a_inscrire or a_desinscrire if not todo: H.append("""

Il n'y a rien à modifier !

""") H.append( scu.confirm_dialog( dest_url=( "formsemestre_inscr_passage" if todo else "formsemestre_status" ), message="

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"""

Opération effectuée