diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py index 8e595ead7..5120aa05c 100644 --- a/app/scodoc/codes_cursus.py +++ b/app/scodoc/codes_cursus.py @@ -85,17 +85,6 @@ UE_ELECTIVE = 4 # UE "élective" dans certains cursus (UCAC?, ISCID) UE_PROFESSIONNELLE = 5 # UE "professionnelle" (ISCID, ...) UE_OPTIONNELLE = 6 # UE non fondamentales (ILEPS, ...) - -def ue_is_fondamentale(ue_type): - return ue_type in (UE_STANDARD, UE_STAGE_LP, UE_PROFESSIONNELLE) - - -def ue_is_professionnelle(ue_type): - return ( - ue_type == UE_PROFESSIONNELLE - ) # NB: les UE_PROFESSIONNELLE sont à la fois fondamentales et pro - - UE_TYPE_NAME = { UE_STANDARD: "Standard", UE_SPORT: "Sport/Culture (points bonus)", @@ -104,8 +93,6 @@ UE_TYPE_NAME = { UE_ELECTIVE: "Elective (ISCID)", UE_PROFESSIONNELLE: "Professionnelle (ISCID)", UE_OPTIONNELLE: "Optionnelle", - # UE_FONDAMENTALE : '"Fondamentale" (eg UCAC)', - # UE_OPTIONNELLE : '"Optionnelle" (UCAC)' } # Couleurs RGB (dans [0.,1.]) des UE pour les bulletins: diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index e3474ebf2..7ee6439a7 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -191,7 +191,23 @@ def do_formsemestre_inscription_edit(args=None, formsemestre_id=None): ) # > modif inscription semestre -def do_formsemestre_desinscription(etudid, formsemestre_id): +def check_if_has_decision_jury( + formsemestre: FormSemestre, etudids: list[int] | set[int] +): + "raise exception if one of the etuds has a decision in formsemestre" + nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) + for etudid in etudids: + if nt.etud_has_decision(etudid): + etud = Identite.query.get(etudid) + raise ScoValueError( + f"""désinscription impossible: l'étudiant {etud.nomprenom} a + une décision de jury (la supprimer avant si nécessaire)""" + ) + + +def do_formsemestre_desinscription( + etudid, formsemestre_id: int, check_has_dec_jury=True +): """Désinscription d'un étudiant. Si semestre extérieur et dernier inscrit, suppression de ce semestre. """ @@ -204,13 +220,8 @@ def do_formsemestre_desinscription(etudid, formsemestre_id): raise ScoValueError("désinscription impossible: semestre verrouille") # -- Si decisions de jury, désinscription interdite - nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) - - if nt.etud_has_decision(etudid): - raise ScoValueError( - f"""désinscription impossible: l'étudiant {etud.nomprenom} a - une décision de jury (la supprimer avant si nécessaire)""" - ) + if check_has_dec_jury: + check_if_has_decision_jury(formsemestre, [etudid]) insem = do_formsemestre_inscription_list( args={"formsemestre_id": formsemestre_id, "etudid": etudid} diff --git a/app/scodoc/sco_inscr_passage.py b/app/scodoc/sco_inscr_passage.py index f97e0d440..4f881022f 100644 --- a/app/scodoc/sco_inscr_passage.py +++ b/app/scodoc/sco_inscr_passage.py @@ -36,13 +36,14 @@ 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.models import Formation, FormSemestre, GroupDescr +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 from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_groups from app.scodoc import sco_preferences @@ -50,62 +51,69 @@ from app.scodoc import sco_pv_dict from app.scodoc.sco_exceptions import ScoValueError -def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False): +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(sem, delai=delai) - inscrits = list_inscrits(sem["formsemestre_id"]) + src_sems = _list_source_sems(formsemestre) + inscrits = list_inscrits(formsemestre.id) r = {} candidats = {} # etudid : etud (tous les etudiants candidats) nb = 0 # debug - for src in src_sems: + src_formsemestre: FormSemestre + for src_formsemestre in src_sems: if ignore_jury: # liste de tous les inscrits au semestre (sans dems) - liste = list_inscrits(src["formsemestre_id"]).values() + etud_list = list_inscrits(formsemestre.id).values() else: # liste des étudiants autorisés par le jury à s'inscrire ici - liste = list_etuds_from_sem(src, sem) + etud_list = _list_etuds_from_sem(src_formsemestre, formsemestre) liste_filtree = [] - for e in liste: + 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 = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0] - for isem in etud["sems"]: - if ndb.DateDMYtoISO(isem["date_debut"]) >= ndb.DateDMYtoISO( - src["date_fin"] - ): + 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"]] = { + r[src_formsemestre.id] = { "etuds": liste_filtree, "infos": { - "id": src["formsemestre_id"], - "title": src["titreannee"], - "title_target": "formsemestre_status?formsemestre_id=%s" - % src["formsemestre_id"], + "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"]: + 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[sem["formsemestre_id"]] = { + r[formsemestre.id] = { "etuds": list(inscrits.values()), "infos": { - "id": sem["formsemestre_id"], - "title": "Semestre cible: " + sem["titreannee"], - "title_target": "formsemestre_status?formsemestre_id=%s" - % sem["formsemestre_id"], + "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", @@ -115,7 +123,7 @@ def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False): return r, inscrits, candidats -def list_inscrits(formsemestre_id, with_dems=False): +def list_inscrits(formsemestre_id: int, with_dems=False) -> list[dict]: """Étudiants déjà inscrits à ce semestre { etudid : etud } """ @@ -133,28 +141,27 @@ def list_inscrits(formsemestre_id, with_dems=False): return inscr -def list_etuds_from_sem(src, dst) -> list[dict]: - """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst.""" - target = dst["semestre_id"] - dpv = sco_pv_dict.dict_pvjury(src["formsemestre_id"]) +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 in [a["semestre_id"] for a in x["autorisations"]] + if target_semestre_id in [a["semestre_id"] for a in x["autorisations"]] ] return etuds -def list_inscrits_date(sem): - """Liste les etudiants inscrits dans n'importe quel semestre - du même département - SAUF sem à la date de début de sem. +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) - sem["date_debut_iso"] = ndb.DateDMYtoISO(sem["date_debut"]) cursor.execute( """SELECT ins.etudid FROM @@ -166,12 +173,18 @@ def list_inscrits_date(sem): AND S.date_fin >= %(date_debut_iso)s AND S.dept_id = %(dept_id)s """, - sem, + { + "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(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): +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: @@ -181,12 +194,11 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): (si les deux sont vrais, inscrit_parcours n'a pas d'effet) """ # TODO à ré-écrire pour utiliser les modèles, notamment GroupDescr - formsemestre: FormSemestre = db.session.get(FormSemestre, sem["formsemestre_id"]) 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( - sem["formsemestre_id"], + formsemestre.id, etudid, etat=scu.INSCRIT, method="formsemestre_inscr_passage", @@ -210,7 +222,7 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): cursem_groups_by_name = { g["group_name"]: g - for g in sco_groups.get_sem_groups(sem["formsemestre_id"]) + for g in sco_groups.get_sem_groups(formsemestre.id) if g["group_name"] } @@ -234,53 +246,46 @@ def do_inscrit(sem, etudids, inscrit_groupes=False, inscrit_parcours=False): sco_groups.change_etud_group_in_partition(etudid, group) -def do_desinscrit(sem: dict, etudids: list[int]): +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, sem["formsemestre_id"] + etudid, formsemestre.id, check_has_dec_jury=check_has_dec_jury ) -def list_source_sems(sem, delai=None) -> list[dict]: +def _list_source_sems(formsemestre: FormSemestre) -> list[FormSemestre]: """Liste des semestres sources - sem est le semestre destination + formsemestre est le semestre destination """ - # liste des semestres débutant a moins - # de delai (en jours) de la date de fin du semestre d'origine. - sems = sco_formsemestre.do_formsemestre_list() - othersems = [] - d, m, y = [int(x) for x in sem["date_debut"].split("/")] - date_debut_dst = datetime.date(y, m, d) - - delais = datetime.timedelta(delai) - for s in sems: - if s["formsemestre_id"] == sem["formsemestre_id"]: - continue # saute le semestre destination - if s["date_fin"]: - d, m, y = [int(x) for x in s["date_fin"].split("/")] - date_fin = datetime.date(y, m, d) - if date_debut_dst - date_fin > delais: - continue # semestre trop ancien - if date_fin > date_debut_dst: - continue # semestre trop récent - # Elimine les semestres de formations speciales (sans parcours) - if s["semestre_id"] == codes_cursus.NO_SEMESTRE_ID: - continue - # - formation: Formation = Formation.query.get_or_404(s["formation_id"]) - parcours = codes_cursus.get_cursus_from_code(formation.type_parcours) - if not parcours.ALLOW_SEM_SKIP: - if s["semestre_id"] < (sem["semestre_id"] - 1): - continue - othersems.append(s) - return othersems + # 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=[], + etuds: str | list[int] | list[str] | int | None = None, inscrit_groupes=False, inscrit_parcours=False, submitted=False, @@ -300,36 +305,41 @@ def formsemestre_inscr_passage( - 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) - sem = sco_formsemestre.get_formsemestre(formsemestre_id) # -- check lock - if not sem["etat"]: + if not formsemestre.etat: raise ScoValueError("opération impossible: semestre verrouille") - header = html_sco_header.sco_header(page_title="Passage des étudiants") + header = html_sco_header.sco_header( + page_title="Passage des étudiants", + init_qtip=True, + javascripts=["js/etud_info.js"], + ) footer = html_sco_header.sco_footer() H = [header] + etuds = [] if etuds is None else etuds if isinstance(etuds, str): - # list de strings, vient du form de confirmation + # 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( - sem, ignore_jury=ignore_jury + 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(sem)) + inscrits_ailleurs = set(list_inscrits_date(formsemestre)) - def set_to_sorted_etud_list(etudset): + def set_to_sorted_etud_list(etudset) -> list[Identite]: etuds = [candidats[etudid] for etudid in etudset] - etuds.sort(key=itemgetter("nom")) + etuds.sort(key=lambda e: e.sort_key) return etuds if submitted: @@ -340,7 +350,7 @@ def formsemestre_inscr_passage( if not submitted: H += _build_page( - sem, + formsemestre, auth_etuds_by_sem, inscrits, candidats_non_inscrits, @@ -355,30 +365,31 @@ def formsemestre_inscr_passage( if a_inscrire: H.append("
Confirmer ?
" if todo else "", add_headers=False, cancel_url="formsemestre_inscr_passage?formsemestre_id=" @@ -395,16 +406,26 @@ def formsemestre_inscr_passage( ) ) 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( - sem, + formsemestre, a_inscrire, inscrit_groupes=inscrit_groupes, inscrit_parcours=inscrit_parcours, ) # Désinscriptions: - do_desinscrit(sem, a_desinscrire) + do_desinscrit(formsemestre, a_desinscrire, check_has_dec_jury=False) H.append( f"""Cette page permet d'inscrire des étudiants dans le semestre destination {sem['titreannee']}, + url_for("notes.formsemestre_status", + scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id ) + }">{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 + 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 @@ -555,7 +576,7 @@ def formsemestre_inscr_passage_help(sem: dict): conserve les groupes, on conserve les parcours (là aussi, pensez à les cocher dans modifier le semestre avant de faire passer les étudiants). @@ -656,25 +677,24 @@ def etuds_select_boxes( H.append("