# -*- 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 # ############################################################################## """Semestres: validation semestre et UE dans parcours """ import time import flask from flask import flash, g, render_template, request, url_for import sqlalchemy as sa from app.models import Identite, Evaluation 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, UniteEns, ScolarNews, Scolog from app.models.notes import etud_has_notes_attente from app.models.validations import ( ScolarAutorisationInscription, ScolarFormSemestreValidation, ) from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.codes_cursus import * from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message from app.scodoc import sco_assiduites from app.scodoc import codes_cursus from app.scodoc import sco_cache from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_cursus from app.scodoc import sco_cursus_dut from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue from app.scodoc import sco_preferences from app.scodoc import sco_pv_dict from app.views import ScoData # ------------------------------------------------------------------------------------ def formsemestre_validation_etud_form( formsemestre_id=None, # required etudid=None, # one of etudid or etud_index is required etud_index=None, check=0, # opt: si true, propose juste une relecture du parcours dest_url=None, sortcol=None, read_only=True, ): """Formulaire de validation des décisions de jury (formations classiques)""" formsemestre: FormSemestre = FormSemestre.query.filter_by( id=formsemestre_id, dept_id=g.scodoc_dept_id ).first_or_404() etud = Identite.get_etud(etudid) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) T = nt.get_table_moyennes_triees() if not etudid and etud_index is None: raise ValueError("formsemestre_validation_etud_form: missing argument etudid") if etud_index is not None: etud_index = int(etud_index) # cherche l'etudid correspondant if etud_index < 0 or etud_index >= len(T): raise ValueError( "formsemestre_validation_etud_form: invalid etud_index value" ) etudid = T[etud_index][-1] else: # cherche index pour liens navigation etud_index = len(T) - 1 while etud_index >= 0 and T[etud_index][-1] != etudid: etud_index -= 1 if etud_index < 0: raise ValueError( "formsemestre_validation_etud_form: can't retreive etud_index !" ) # prev, next pour liens navigation etud_index_next = etud_index + 1 if etud_index_next >= len(T): etud_index_next = None etud_index_prev = etud_index - 1 if etud_index_prev < 0: etud_index_prev = None if read_only: check = True Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) if not Se.sem["etat"]: raise ScoValueError("validation: semestre verrouille") url_tableau = url_for( "notes.formsemestre_recapcomplet", scodoc_dept=g.scodoc_dept, mode_jury=1, formsemestre_id=formsemestre_id, selected_etudid=etudid, # va a la bonne ligne ) H = [] # Navigation suivant/precedent if etud_index_prev is not None: etud_prev = Identite.get_etud(T[etud_index_prev][-1]) url_prev = url_for( "notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etud_index=etud_index_prev, ) else: url_prev = None if etud_index_next is not None: etud_next = Identite.get_etud(T[etud_index_next][-1]) url_next = url_for( "notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, etud_index=etud_index_next, ) else: url_next = None footer = ["""") H.append('
') if not check: H.append( f"""

{etud.nomprenom}: validation { Se.parcours.SESSION_NAME_A}{Se.parcours.SESSION_NAME }

Parcours: {Se.get_cursus_descr()} """ ) else: H.append( f"""

Parcours de {etud.nomprenom}

{Se.get_cursus_descr()}""" ) H.append( f"""
{etud.photo_html(title="fiche de " + etud.nomprenom)}
""" ) etud_etat = nt.get_etud_etat(etudid) if etud_etat == scu.DEMISSION: H.append('
Etudiant démissionnaire
') if etud_etat == scu.DEF: H.append('
Etudiant défaillant
') if etud_etat != scu.INSCRIT: H.append( f"""
Impossible de statuer sur cet étudiant: il est démissionnaire ou défaillant (voir sa fiche)
""" ) return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H + footer), ) H.append( formsemestre_recap_parcours_table( Se, etudid, with_links=(check and not read_only) ) ) if check: if not dest_url: dest_url = url_tableau H.append(f'') return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H + footer), ) decision_jury = Se.nt.get_etud_decision_sem(etudid) # Bloque si note en attente if etud_has_notes_attente(etudid, formsemestre_id): H.append( tf_error_message( f"""Impossible de statuer sur cet étudiant: il a des notes en attente dans des évaluations de ce semestre (voir tableau de bord) """ ) ) return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H + footer), ) evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud( formsemestre, etud ) if evaluations_a_debloquer: links_evals = [ f"""{e.description} en {e.moduleimpl.module.code}""" for e in evaluations_a_debloquer ] H.append( tf_error_message( f"""Impossible de statuer sur cet étudiant: il a des notes dans des évaluations qui seront débloquées plus tard: voir {", ".join(links_evals)} """ ) ) return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H + footer), ) # Infos si pas de semestre précédent if not Se.prev_formsemestre: if Se.cur_sem.semestre_id == 1: H.append("

Premier semestre (pas de précédent)

") else: H.append("

Pas de semestre précédent !

") else: if not Se.prev_decision: H.append( tf_error_message( f"""Le jury n'a pas statué sur le semestre précédent ! (le faire maintenant) """ ) ) if decision_jury: H.append( f"""Supprimer décision existante """ ) return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H), ) # Infos sur decisions déjà saisies if decision_jury: if decision_jury["assidu"]: ass = "assidu" else: ass = "non assidu" H.append("

Décision existante du %(event_date)s: %(code)s" % decision_jury) H.append(" (%s)" % ass) autorisations = ScolarAutorisationInscription.query.filter_by( etudid=etudid, origin_formsemestre_id=formsemestre_id ).all() if autorisations: H.append(f". Autorisé{etud.e} à s'inscrire en ") H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".") H.append("

") # Cas particulier pour ATJ: corriger precedent avant de continuer if Se.prev_formsemestre and Se.prev_decision and Se.prev_decision["code"] == ATJ: H.append( f"""

La décision du semestre précédent est en attente à cause d\'un problème d\'assiduité.

Vous devez la corriger avant de continuer ce jury. Soit vous considérez que le problème d'assiduité n'est pas réglé et choisissez de ne pas valider le semestre précédent (échec), soit vous entrez une décision sans prendre en compte l'assiduité.

""" ) if sortcol: H.append(f"""""") H.append("
") return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H), ) # Explication sur barres actuelles H.append('

L\'étudiant ') if Se.barre_moy_ok: H.append("a la moyenne générale, ") else: H.append("n'a pas la moyenne générale, ") H.append(Se.barres_ue_diag) # eg 'les UEs sont au dessus des barres' if (not Se.barre_moy_ok) and Se.can_compensate_with_prev: H.append(", et ce semestre peut se compenser avec le précédent") H.append(".

") # Décisions possibles rows_assidu = decisions_possible_rows( Se, True, subtitle="Étudiant assidu:", trclass="sfv_ass" ) rows_non_assidu = decisions_possible_rows( Se, False, subtitle="Si problème d'assiduité:", trclass="sfv_pbass" ) # s'il y a des decisions recommandees issues des regles: if rows_assidu or rows_non_assidu: H.append( """
""" % (etudid, formsemestre_id) ) if dest_url: H.append('' % dest_url) if sortcol: H.append('' % sortcol) H.append('

Décisions recommandées :

') H.append("") H.append(rows_assidu) if rows_non_assidu: H.append("") # spacer H.append(rows_non_assidu) H.append("
 
") H.append( '


' ) H.append("
") H.append(form_decision_manuelle(Se, formsemestre_id, etudid)) H.append( f"""""" ) H.append('

Formation ') if Se.sem["gestion_semestrielle"]: H.append("avec semestres décalés

") else: H.append("sans semestres décalés

") return render_template( "sco_page.j2", javascripts=["js/recap_parcours.js"], title=f"Parcours {etud.nomprenom}", content="\n".join(H + footer), ) def formsemestre_validation_etud( formsemestre_id=None, # required etudid=None, # required codechoice=None, # required desturl="", sortcol=None, ): """Enregistre validation""" etud = Identite.get_etud(etudid) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) # retrouve la decision correspondant au code: choices = Se.get_possible_choices(assiduite=True) choices += Se.get_possible_choices(assiduite=False) selected_choice = None for choice in choices: if choice.codechoice == codechoice: selected_choice = choice break if not selected_choice: raise ValueError(f"code choix invalide ! ({codechoice})") # Se.valide_decision(selected_choice) # enregistre return _redirect_valid_choice( formsemestre_id, etudid, Se, selected_choice, desturl, sortcol ) def formsemestre_validation_etud_manu( formsemestre_id=None, # required etudid=None, # required code_etat="", new_code_prev="", devenir="", # required (la decision manuelle) assidu=False, desturl="", sortcol=None, redirect=True, ): """Enregistre validation""" if assidu: assidu = True etud = Identite.get_etud(etudid) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) if code_etat in Se.parcours.UNUSED_CODES: raise ScoValueError("code decision invalide dans ce parcours") # Si code ADC, extrait le semestre utilisé: if code_etat[:3] == ADC: formsemestre_id_utilise_pour_compenser = code_etat.split("_")[1] if not formsemestre_id_utilise_pour_compenser: formsemestre_id_utilise_pour_compenser = ( None # compense avec semestre hors ScoDoc ) code_etat = ADC else: formsemestre_id_utilise_pour_compenser = None # Construit le choix correspondant: choice = sco_cursus_dut.DecisionSem( code_etat=code_etat, new_code_prev=new_code_prev, devenir=devenir, assiduite=assidu, formsemestre_id_utilise_pour_compenser=formsemestre_id_utilise_pour_compenser, ) # Se.valide_decision(choice) # enregistre if redirect: return _redirect_valid_choice( formsemestre_id, etudid, Se, choice, desturl, sortcol ) def _redirect_valid_choice(formsemestre_id, etudid, Se, choice, desturl, sortcol): adr = "formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&check=1" % ( formsemestre_id, etudid, ) if sortcol: adr += "&sortcol=" + str(sortcol) # if desturl: # desturl += "&desturl=" + desturl return flask.redirect(adr) # Si le precedent a été modifié, demande relecture du parcours. # sinon renvoie au listing general, def _dispcode(c): if not c: return "" return c def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""): "Liste HTML des decisions possibles" choices = Se.get_possible_choices(assiduite=assiduite) if not choices: return "" prev_title = "" if Se.prev_formsemestre: if Se.prev_formsemestre.semestre_id >= 0: prev_title = "%s%d" % ( Se.parcours.SESSION_ABBRV, Se.prev_formsemestre.semestre_id, ) else: prev_title = "Prec." if Se.cur_sem.semestre_id >= 0: cur_title = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.cur_sem.semestre_id) else: cur_title = Se.parcours.SESSION_NAME H = [ '%s' % (trclass, subtitle) ] if Se.prev_formsemestre: H.append(f"Code {prev_title}") H.append(f"Code {cur_title}Devenir") for ch in choices: H.append( """""" % (trclass, ch.rule_id, ch.codechoice) ) H.append("%s " % ch.explication) if Se.prev_formsemestre: H.append('%s' % _dispcode(ch.new_code_prev)) H.append( '%s%s' % (_dispcode(ch.code_etat), Se.explique_devenir(ch.devenir)) ) H.append("") return "\n".join(H) def formsemestre_recap_parcours_table( situation_etud_cursus: sco_cursus_dut.SituationEtudCursus, etudid, with_links=False, with_all_columns=True, sem_info=None, show_details=False, ): """Tableau HTML recap parcours Si with_links, ajoute liens pour modifier decisions (colonne de droite) sem_info = { formsemestre_id : txt } permet d'ajouter des informations associées à chaque semestre with_all_columns: si faux, pas de colonne "assiduité". """ sem_info = sem_info or {} H = [] linktmpl = '%s' minuslink = linktmpl % scu.icontag("minus_img", border="0", alt="-") pluslink = linktmpl % scu.icontag("plus_img", border="0", alt="+") if show_details: sd = " recap_show_details" plusminus = minuslink else: sd = " recap_hide_details" plusminus = pluslink H.append( f""" """ ) # titres des UE H.append("" * situation_etud_cursus.nb_max_ue) # if with_links: H.append("") H.append("") num_sem = 0 for formsemestre in situation_etud_cursus.formsemestres: is_prev = situation_etud_cursus.prev_formsemestre and ( situation_etud_cursus.prev_formsemestre.id == formsemestre.id ) is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id num_sem += 1 url_status = url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ) dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid]) pv = dpv["decisions"][0] decision_sem = pv["decision_sem"] decisions_ue = pv["decisions_ue"] if with_all_columns and decision_sem and not decision_sem["assidu"]: ass = " (non ass.)" else: ass = "" nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) if is_cur: type_sem = "*" # now unused class_sem = "sem_courant" elif is_prev: type_sem = "p" class_sem = "sem_precedent" else: type_sem = "" class_sem = "sem_autre" if ( formsemestre.formation.formation_code != situation_etud_cursus.formation.formation_code ): class_sem += " sem_autre_formation" bgcolor = ( formsemestre.bul_bgcolor if formsemestre.bul_bgcolor else "background-color: rgb(255,255,240)" ) # 1ere ligne: titre sem, decision, acronymes UE H.append('' % (class_sem, formsemestre.id)) if is_cur: pm = "" elif is_prev: pm = minuslink % formsemestre.id else: pm = plusminus % formsemestre.id inscr = formsemestre.etuds_inscriptions.get(etudid) parcours_name = "" if inscr and nt.is_apc: if inscr.parcour: parcours_name = ( f' {inscr.parcour.code}' ) else: # si l'étudiant n'est pas inscrit à un parcours mais que le semestre a plus d'UE # signale un éventuel problème: if len(nt.formsemestre.get_ues()) > len( nt.etud_ues_ids(etudid) ): # XXX sans dispenses parcours_name = f""" {scu.EMO_WARNING} pas de parcours """ H.append( f""" """) if nt.is_apc: H.append('') elif decision_sem: H.append( f"""""" ) else: H.append("") H.append(f"""""") # abs # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé) ues = list(nt.etud_ues(etudid)) # nb: en BUT, les UE "dispensées" sont incluses cnx = ndb.GetDBConnexion() etud_ue_status = {ue.id: nt.get_etud_ue_status(etudid, ue.id) for ue in ues} if not nt.is_apc: # formations classiques: filtre UE sur inscriptions (et garde UE capitalisées) ues = [ ue for ue in ues if etud_est_inscrit_ue(cnx, etudid, formsemestre.id, ue.id) or etud_ue_status[ue.id]["is_capitalized"] ] for ue in ues: H.append(f"""""") if len(ues) < situation_etud_cursus.nb_max_ue: H.append( f"""""" ) # indique le semestre compensé par celui ci: if decision_sem and decision_sem["compense_formsemestre_id"]: csem = sco_formsemestre.get_formsemestre( decision_sem["compense_formsemestre_id"] ) H.append(f"""""") else: H.append("") if with_links: H.append("") H.append("") # 2eme ligne: notes H.append(f"""""") H.append( f"""""" ) if is_prev: default_sem_info = '[sem. précédent]' else: default_sem_info = "" if not formsemestre.etat: # locked lockicon = scu.icontag("lock32_img", title="verrouillé", border="0") default_sem_info += lockicon if ( formsemestre.formation.formation_code != situation_etud_cursus.formation.formation_code ): default_sem_info += ( f"""Autre formation: {formsemestre.formation.formation_code}""" ) H.append( f""" """ ) # Moy Gen (sous le code decision) H.append( f"""""" ) # Absences (nb d'abs non just. dans ce semestre) nbabsnj = sco_assiduites.formsemestre_get_assiduites_count( etudid, formsemestre )[0] H.append(f"""""") # UEs for ue in ues: if decisions_ue and ue.id in decisions_ue: code = decisions_ue[ue.id]["code"] else: code = "" ue_status = etud_ue_status[ue.id] moy_ue = ue_status["moy"] if ue_status else "" explanation_ue = [] # list of strings if code == ADM: class_ue = "ue_adm" elif code == CMP: class_ue = "ue_cmp" else: class_ue = "ue" if ue_status and ue_status["is_external"]: # validation externe explanation_ue.append("UE externe.") if ue_status and ue_status["is_capitalized"]: class_ue += " ue_capitalized" explanation_ue.append( f"""Capitalisée le {ue_status["event_date"] or "?"}.""" ) # Dispense BUT ? if (etudid, ue.id) in nt.dispense_ues: moy_ue_txt = ( "❎" if (ue_status and ue_status["is_capitalized"]) else "⭕" ) explanation_ue.append("non inscrit (dispense)") else: moy_ue_txt = scu.fmt_note(moy_ue) H.append( f"""""" ) if len(ues) < situation_etud_cursus.nb_max_ue: H.append( f"""""" ) H.append("") if with_links: H.append( f"""""" ) H.append("") # 3eme ligne: ECTS if ( sco_preferences.get_preference("bul_show_ects", formsemestre.id) or nt.parcours.ECTS_ONLY ): etud_ects_infos = nt.get_etud_ects_pot(etudid) # ECTS potentiels H.append( f"""""" ) # Total ECTS (affiché sous la moyenne générale) H.append( f""" """ ) # ECTS validables dans chaque UE for ue in ues: ue_status = nt.get_etud_ue_status(etudid, ue.id) if ue_status: ects = ue_status["ects"] ects_pot = ue_status["ects_pot"] H.append( f"""""" ) else: H.append("""""") H.append("") H.append("
{ scu.icontag("plus18_img", width=18, height=18, border=0, title="", alt="+") } Semestre Etat Abs
{num_sem}{pm} {formsemestre.mois_debut()} {formsemestre.titre_annee()}{parcours_name} """ ) if nt.is_apc: H.append( f"""jury""" ) H.append("""BUT{ decision_sem["code"]}en cours{ass}{ue.acronyme}compense S{csem["semestre_id"]}
 {formsemestre.mois_fin()} {sem_info.get(formsemestre.id, default_sem_info)}{scu.fmt_note(nt.get_etud_moy_gen(etudid))}{nbabsnj}{moy_ue_txt}modifier
  ECTS: { pv.get("sum_ects",0):2.3g} / {etud_ects_infos["ects_total"]:2.3g} {ects:2.3g}
") return "\n".join(H) def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None): """Formulaire pour saisie décision manuelle""" H = [ """
""" % (etudid, formsemestre_id) ] if desturl: H.append('' % desturl) if sortcol: H.append('' % sortcol) H.append( '

Décisions manuelles : (vérifiez bien votre choix !)

' ) # Choix code semestre: codes = sorted(codes_cursus.CODES_JURY_SEM) # fortuitement, cet ordre convient bien ! H.append( '") # Choix code semestre precedent: if Se.prev_formsemestre: H.append( '") # Choix code devenir codes = list(codes_cursus.DEVENIR_EXPL.keys()) codes.sort() # fortuitement, cet ordre convient aussi bien ! if Se.sem["semestre_id"] == -1: allowed_codes = codes_cursus.DEVENIRS_MONO else: allowed_codes = set(codes_cursus.DEVENIRS_STD) # semestres decales ? if Se.sem["gestion_semestrielle"]: allowed_codes = allowed_codes.union(codes_cursus.DEVENIRS_DEC) # n'autorise les codes NEXT2 que si semestres décalés et s'il ne manque qu'un semestre avant le n+2 if Se.can_jump_to_next2(): allowed_codes = allowed_codes.union(codes_cursus.DEVENIRS_NEXT2) H.append( '") H.append( '' ) H.append( """
Code semestre:
Code semestre précédent:
Devenir:
assidu
Supprimer décision existante
""" % (etudid, formsemestre_id) ) return "\n".join(H) # ----------- def formsemestre_validation_auto(formsemestre_id: int): "Formulaire saisie automatisée des décisions d'un semestre" return render_template( "sco_page.j2", title="Saisie automatique des décisions", content=f"""

Saisie automatique des décisions du semestre

Il est donc vivement conseillé de relire soigneusement les décisions à l'issue de cette procédure !

Le calcul prend quelques minutes, soyez patients !

""", ) def do_formsemestre_validation_auto(formsemestre_id): "Saisie automatisee des decisions d'un semestre" sem = sco_formsemestre.get_formsemestre(formsemestre_id) next_semestre_id = sem["semestre_id"] + 1 formsemestre = FormSemestre.get_formsemestre(formsemestre_id) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) etudids = nt.get_etudids() nb_valid = 0 conflicts = [] # liste des etudiants avec decision differente déjà saisie with sco_cache.DeferredSemCacheManager(): for etudid in etudids: etud = Identite.get_etud(etudid) Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id) ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list( {"etudid": etudid, "formsemestre_id": formsemestre_id} )[0] # Conditions pour validation automatique: if ins["etat"] == scu.INSCRIT and ( ( (not Se.prev_formsemestre) or ( Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ) ) ) and Se.barre_moy_ok and Se.barres_ue_ok and not etud_has_notes_attente(etudid, formsemestre_id) ): # check: s'il existe une decision ou autorisation et qu'elles sont differentes, # warning (et ne fait rien) decision_sem = nt.get_etud_decision_sem(etudid) ok = True if decision_sem and decision_sem["code"] != ADM: ok = False conflicts.append(etud) autorisations = ScolarAutorisationInscription.query.filter_by( etudid=etudid, origin_formsemestre_id=formsemestre_id ).all() if len(autorisations) != 0: if ( len(autorisations) > 1 or autorisations[0].semestre_id != next_semestre_id ): if ok: conflicts.append(etud) ok = False # ok, valide ! if ok: formsemestre_validation_etud_manu( formsemestre_id, etudid, code_etat=ADM, devenir="NEXT", assidu=True, redirect=False, ) nb_valid += 1 log( f"do_formsemestre_validation_auto: {nb_valid} validations, {len(conflicts)} conflicts" ) ScolarNews.add( typ=ScolarNews.NEWS_JURY, obj=formsemestre.id, text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status() } ({nb_valid} décisions)""", url=url_for( "notes.formsemestre_status", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ), ) H = [ f"""

Saisie automatique des décisions du semestre {formsemestre.titre_annee()}

Opération effectuée.

{nb_valid} étudiants validés sur {len(etudids)}

""" ] if conflicts: H.append( f"""

Attention: {len(conflicts)} étudiants non modifiés car décisions différentes déja saisies :

") H.append( f"""continuer""" ) return render_template( "sco_page.j2", title="Saisie automatique", content="\n".join(H) ) def formsemestre_validation_suppress_etud(formsemestre_id, etudid): """Suppression des décisions de jury pour un étudiant/formsemestre. Efface toutes les décisions enregistrées concernant ce formsemestre et cet étudiant: code semestre, UEs, autorisations d'inscription """ log(f"formsemestre_validation_suppress_etud( {formsemestre_id}, {etudid})") # Validations jury classiques (semestres, UEs, autorisations) for v in ScolarFormSemestreValidation.query.filter_by( etudid=etudid, formsemestre_id=formsemestre_id ): db.session.delete(v) for v in ScolarAutorisationInscription.query.filter_by( etudid=etudid, origin_formsemestre_id=formsemestre_id ): db.session.delete(v) # Validations jury spécifiques BUT for v in ApcValidationRCUE.query.filter_by( etudid=etudid, formsemestre_id=formsemestre_id ): db.session.delete(v) for v in ApcValidationAnnee.query.filter_by( etudid=etudid, formsemestre_id=formsemestre_id ): db.session.delete(v) db.session.commit() sem = sco_formsemestre.get_formsemestre(formsemestre_id) _invalidate_etud_formation_caches( etudid, sem["formation_id"] ) # > suppr. decision jury (peut affecter de plusieurs semestres utilisant UE capitalisée) def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite): """Form. saisie UE validée hors ScoDoc (pour étudiants arrivant avec un UE antérieurement validée). """ formation: Formation = formsemestre.formation # Toutes les UEs non bonus de cette formation sont présentées # avec indice de semestre <= semestre courant ou NULL ues = formation.ues.filter( UniteEns.type != UE_SPORT, db.or_( UniteEns.semestre_idx == None, UniteEns.semestre_idx <= formsemestre.semestre_id, ), ).order_by(UniteEns.semestre_idx, UniteEns.numero) ue_names = ["Choisir..."] + [ f"""{('S'+str(ue.semestre_idx)+' : ') if ue.semestre_idx is not None else '' }{ue.acronyme} {ue.titre or ''} ({ue.ue_code or ""})""" for ue in ues ] ue_ids = [""] + [ue.id for ue in ues] form_descr = [ ("etudid", {"input_type": "hidden"}), ("formsemestre_id", {"input_type": "hidden"}), ( "ue_id", { "input_type": "menu", "title": "Unité d'Enseignement (UE)", "allow_null": False, "allowed_values": ue_ids, "labels": ue_names, }, ), ] if not formation.is_apc(): form_descr.append( ( "semestre_id", { "input_type": "menu", "title": "Indice du semestre", "explanation": "Facultatif: indice du semestre dans la formation", "allow_null": True, "allowed_values": [""] + [x for x in range(11)], "labels": ["-"] + list(range(11)), }, ) ) ue_codes = sorted(codes_cursus.CODES_JURY_UE) form_descr += [ ( "date", { "input_type": "date", "size": 9, "explanation": "j/m/a", "default": time.strftime(scu.DATE_FMT), }, ), ( "moy_ue", { "type": "float", "allow_null": False, "min_value": 0, "max_value": 20, "title": "Moyenne (/20) obtenue dans cette UE:", "explanation": "mettre 10 si non notée", }, ), ( "code_jury", { "input_type": "menu", "title": "Code jury", "explanation": " code donné par le jury (ADM si validée normalement)", "allow_null": True, "allowed_values": [""] + ue_codes, "labels": ["-"] + ue_codes, "default": ADM, }, ), ] tf = TrivialFormulator( request.base_url, scu.get_request_args(), form_descr, cancelbutton="Revenir au bulletin", submitlabel="Enregistrer validation d'UE", ) if tf[0] == 0: return render_template( "sco_page_dept.j2", title="Validation UE antérieure", javascripts=["js/validate_previous_ue.js"], cssstyles=["css/jury_delete_manual.css"], etudid=etud.id, formsemestre_id=formsemestre.id, sco=ScoData(etud=etud, formsemestre=formsemestre), content=f"""

Gestion des validations d'UEs antérieures de {etud.html_link_fiche()}

Utiliser cette page pour enregistrer des UEs validées antérieurement, dans un semestre hors ScoDoc.

Les UE validées dans ScoDoc sont automatiquement prises en compte.

Cette page est surtout utile pour les étudiants ayant suivi un début de cursus dans un autre établissement, ou qui ont suivi une UE à l'étranger ou dans un semestre géré sans ScoDoc.

Il est aussi nécessaire de valider les UEs antérieures en cas de changement de référentiel de compétence en cours de cursus (par exemple si un étudiant redouble et que le programme change de référentiel entre temps).

Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.

Notez que l'UE est validée, avec enregistrement immédiat de la décision et l'attribution des ECTS si le code jury est validant (ADM).

On ne peut valider ici que les UEs du cursus {formation.titre}

{_get_etud_ue_validations_html(etud, formsemestre)}
Enregistrer une UE antérieure
{tf[1]}
{check_formation_ues(formation)[0]} """, ) dest_url = url_for( "notes.formsemestre_validate_previous_ue", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, etudid=etud.id, ) if tf[0] == -1: return flask.redirect( url_for( "notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, etudid=etud.id, ) ) if tf[2].get("semestre_id"): semestre_id = int(tf[2]["semestre_id"]) else: semestre_id = None if tf[2]["code_jury"] not in CODES_JURY_UE: flash("Code UE invalide") return flask.redirect(dest_url) do_formsemestre_validate_previous_ue( formsemestre, etud.id, tf[2]["ue_id"], tf[2]["moy_ue"], tf[2]["date"], code=tf[2]["code_jury"], semestre_id=semestre_id, ) flash("Validation d'UE enregistrée") return flask.redirect(dest_url) def _get_etud_ue_validations_html(etud: Identite, formsemestre: FormSemestre) -> str: """HTML listant les validations d'UEs pour cet étudiant dans des formations de même code que celle du formsemestre indiqué. """ validations = get_etud_ue_validations(etud, formsemestre.formation) if not validations: return "" return render_template( "jury/ue_list_etud_validations.j2", edit_mode=True, etud=etud, titre_boite="Validations d'UEs dans cette formation", validations=validations, ) def get_etud_ue_validations( etud: Identite, formation: Formation ) -> list[ScolarFormSemestreValidation]: """Retourne les validations d'UEs pour cet étudiant dans des formations de même code que celle indiquée. """ return ( ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) .join(UniteEns) .join(Formation) .filter_by(formation_code=formation.formation_code) .order_by( sa.desc(UniteEns.semestre_idx), UniteEns.numero, UniteEns.acronyme, sa.desc(ScolarFormSemestreValidation.event_date), ) .all() ) def do_formsemestre_validate_previous_ue( formsemestre: FormSemestre, etudid, ue_id, moy_ue, date, code=ADM, semestre_id=None, ue_coefficient=None, ): """Enregistre (ou modifie) validation d'UE (obtenue hors ScoDoc). Si le coefficient est spécifié, modifie le coefficient de cette UE (utile seulement pour les semestres extérieurs). """ nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) ue: UniteEns = UniteEns.query.get_or_404(ue_id) cnx = ndb.GetDBConnexion() if ue_coefficient is not None: sco_formsemestre.do_formsemestre_uecoef_edit_or_create( cnx, formsemestre.id, ue_id, ue_coefficient ) else: sco_formsemestre.do_formsemestre_uecoef_delete(cnx, formsemestre.id, ue_id) sco_cursus_dut.do_formsemestre_validate_ue( cnx, nt, formsemestre.id, # "importe" cette UE dans le semestre (new 3/2015) etudid, ue_id, code, moy_ue=moy_ue, date=date, semestre_id=semestre_id, is_external=True, ) log(f"formsemestre_validate_previous_ue: etudid={etudid}, ue={ue}, code={code}") Scolog.logdb( method="formsemestre_validate_previous_ue", etudid=etudid, msg=f"Validation UE prec. {ue_id} {ue.acronyme}: {code}", commit=True, ) _invalidate_etud_formation_caches(etudid, formsemestre.formation_id) cnx.commit() def _invalidate_etud_formation_caches(etudid, formation_id): "Invalide tous les semestres de cette formation où l'etudiant est inscrit..." r = ndb.SimpleDictFetch( """SELECT sem.id FROM notes_formsemestre sem, notes_formsemestre_inscription i WHERE sem.formation_id = %(formation_id)s AND i.formsemestre_id = sem.id AND i.etudid = %(etudid)s """, {"etudid": etudid, "formation_id": formation_id}, ) for fsid in [s["id"] for s in r]: sco_cache.invalidate_formsemestre( formsemestre_id=fsid ) # > modif decision UE (inval tous semestres avec cet etudiant, ok mais conservatif) def check_formation_ues(formation: Formation) -> tuple[str, dict[int, list[UniteEns]]]: """Verifie que les UE d'une formation sont chacune utilisée dans un seul semestre_id Si ce n'est pas le cas, c'est probablement (mais pas forcément) une erreur de définition du programme: cette fonction retourne un bout de HTML à afficher pour prévenir l'utilisateur, ou '' si tout est ok. """ ue_multiples = {} # { ue_id : [ liste des formsemestre ] } for ue in formation.ues: # formsemestres utilisant cette ue ? sems = ndb.SimpleDictFetch( """SELECT DISTINCT sem.id AS formsemestre_id, sem.* FROM notes_formsemestre sem, notes_modules mod, notes_moduleimpl mi WHERE sem.formation_id = %(formation_id)s AND mod.id = mi.module_id AND mi.formsemestre_id = sem.id AND mod.ue_id = %(ue_id)s """, {"ue_id": ue.id, "formation_id": formation.id}, ) semestre_ids = {x["semestre_id"] for x in sems} if ( len(semestre_ids) > 1 ): # plusieurs semestres d'indices differents dans le cursus ue_multiples[ue.id] = sems if not ue_multiples: return "", {} # Genere message HTML: H = [ """
Attention: les UE suivantes de cette formation sont utilisées dans des semestres de rangs différents (eg S1 et S3).
Cela peut engendrer des problèmes pour la capitalisation des UE. Il serait préférable d'essayer de rectifier cette situation: soit modifier le programme de la formation (définir des UE dans chaque semestre), soit veiller à saisir le bon indice de semestre dans le menu lors de la validation d'une UE extérieure.
    """ ] for ue in formation.ues: if ue.id in ue_multiples: sems = [ sco_formsemestre.get_formsemestre(x["formsemestre_id"]) for x in ue_multiples[ue.id] ] slist = ", ".join( [ f"""{s['titreannee'] } (semestre {s['semestre_id']})""" for s in sems ] ) H.append(f"
  • {ue.acronyme} : {slist}
  • ") H.append("
") return "\n".join(H), ue_multiples