############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """Jury BUT: affichage/formulaire """ import re import numpy as np import flask from flask import flash, render_template, url_for from flask import g, request from app import db from app.but import jury_but from app.but.jury_but import ( DecisionsProposeesAnnee, DecisionsProposeesRCUE, DecisionsProposeesUE, ) from app.comp import res_sem from app.comp.res_but import ResultatsSemestreBUT from app.models import ( ApcNiveau, FormSemestre, FormSemestreInscription, Identite, UniteEns, ScolarAutorisationInscription, ScolarFormSemestreValidation, ) from app.models.config import ScoDocSiteConfig from app.scodoc import html_sco_header from app.scodoc.sco_exceptions import ScoValueError from app.scodoc import sco_preferences from app.scodoc import sco_utils as scu def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str: """Affichage des décisions annuelles BUT Si pas read_only, menus sélection codes jury. """ H = [] if deca.jury_annuel: H.append( f""" <div class="but_section_annee"> <div> <b>Décision de jury pour l'année :</b> { _gen_but_select("code_annee", deca.codes, deca.code_valide, disabled=True, klass="manual") } <span>({deca.code_valide or 'non'} enregistrée)</span> </div> </div> """ ) formsemestre_1 = deca.formsemestre_impair formsemestre_2 = deca.formsemestre_pair # Ordonne selon les dates des 2 semestres considérés (pour les redoublants à cheval): reverse_semestre = ( deca.formsemestre_pair and deca.formsemestre_impair and deca.formsemestre_pair.date_debut < deca.formsemestre_impair.date_debut ) if reverse_semestre: formsemestre_1, formsemestre_2 = formsemestre_2, formsemestre_1 H.append( f""" <div class="titre_niveaux"> <b>Niveaux de compétences et unités d'enseignement du BUT{deca.annee_but}</b> </div> <div class="but_explanation">{deca.explanation}</div> <div class="but_annee"> <div class="titre"></div> <div class="titre">{"S" +str(formsemestre_1.semestre_id) if formsemestre_1 else "-"} <span class="avertissement_redoublement">{formsemestre_1.annee_scolaire_str() if formsemestre_1 else ""}</span> </div> <div class="titre">{"S"+str(formsemestre_2.semestre_id) if formsemestre_2 else "-"} <span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str() if formsemestre_2 else ""}</span> </div> <div class="titre">RCUE</div> """ ) for niveau in deca.niveaux_competences: H.append( f"""<div class="but_niveau_titre"> <div title="{niveau.competence.titre_long}">{niveau.competence.titre}</div> </div>""" ) dec_rcue = deca.decisions_rcue_by_niveau.get(niveau.id) # peut être None ues = [ ue for ue in deca.ues_impair if ue.niveau_competence and ue.niveau_competence.id == niveau.id ] ue_impair = ues[0] if ues else None ues = [ ue for ue in deca.ues_pair if ue.niveau_competence and ue.niveau_competence.id == niveau.id ] ue_pair = ues[0] if ues else None # Les UEs à afficher, # qui seront toujours en readonly sur le formsemestre de l'année précédente du redoublant ues_ro = [ ( ue_impair, (deca.a_cheval and deca.formsemestre_id != deca.formsemestre_impair.id), ), ( ue_pair, deca.a_cheval and deca.formsemestre_id != deca.formsemestre_pair.id, ), ] # Ordonne selon les dates des 2 semestres considérés: if reverse_semestre: ues_ro[0], ues_ro[1] = ues_ro[1], ues_ro[0] # Colonnes d'UE: for ue, ue_read_only in ues_ro: if ue: H.append( _gen_but_niveau_ue( ue, deca.decisions_ues[ue.id], disabled=read_only or ue_read_only, annee_prec=ue_read_only, niveau_id=ue.niveau_competence.id, ) ) else: H.append("""<div class="niveau_vide"></div>""") # Colonne RCUE H.append(_gen_but_rcue(dec_rcue, niveau)) H.append("</div>") # but_annee return "\n".join(H) def _gen_but_select( name: str, codes: list[str], code_valide: str, disabled: bool = False, klass: str = "", data: dict = {}, ) -> str: "Le menu html select avec les codes" # if disabled: # mauvaise idée car le disabled est traité en JS # return f"""<div class="but_code {klass}">{code_valide}</div>""" options_htm = "\n".join( [ f"""<option value="{code}" {'selected' if code == code_valide else ''} class="{'recorded' if code == code_valide else ''}" >{code}</option>""" for code in codes ] ) return f"""<select required name="{name}" class="but_code {klass}" data-orig_code="{code_valide or (codes[0] if codes else '')}" data-orig_recorded="{code_valide or ''}" onchange="change_menu_code(this);" {"disabled" if disabled else ""} {" ".join( f'data-{k}="{v}"' for (k,v) in data.items() )} >{options_htm}</select> """ def _gen_but_niveau_ue( ue: UniteEns, dec_ue: DecisionsProposeesUE, disabled: bool = False, annee_prec: bool = False, niveau_id: int = None, ) -> str: if dec_ue.ue_status and dec_ue.ue_status["is_capitalized"]: moy_ue_str = f"""<span class="ue_cap">{ scu.fmt_note(dec_ue.moy_ue_with_cap)}</span>""" scoplement = f"""<div class="scoplement"> <div> <b>UE {ue.acronyme} capitalisée </b> <span>le {dec_ue.ue_status["event_date"].strftime("%d/%m/%Y")} </span> </div> <div>UE en cours { "sans notes" if np.isnan(dec_ue.moy_ue) else ("avec moyenne <b>" + scu.fmt_note(dec_ue.moy_ue) + "</b>") } </div> </div> """ else: moy_ue_str = f"""<span>{scu.fmt_note(dec_ue.moy_ue)}</span>""" if dec_ue.code_valide: scoplement = f"""<div class="scoplement"> <div>Code {dec_ue.code_valide} enregistré le {dec_ue.validation.event_date.strftime("%d/%m/%Y")} à {dec_ue.validation.event_date.strftime("%Hh%M")} </div> </div> """ else: scoplement = "" return f"""<div class="but_niveau_ue { 'recorded' if dec_ue.code_valide is not None else ''} {'annee_prec' if annee_prec else ''} "> <div title="{ue.titre}">{ue.acronyme}</div> <div class="but_note with_scoplement"> <div>{moy_ue_str}</div> {scoplement} </div> <div class="but_code">{ _gen_but_select("code_ue_"+str(ue.id), dec_ue.codes, dec_ue.code_valide, disabled=disabled, klass=f"code_ue ue_rcue_{niveau_id}" if not disabled else "" ) }</div> </div>""" def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str: if dec_rcue is None: return """ <div class="but_niveau_rcue niveau_vide with_scoplement"> <div></div> <div class="scoplement">Pas de RCUE (UE non capitalisée ?)</div> </div> """ scoplement = ( f"""<div class="scoplement">{ dec_rcue.validation.to_html() }</div>""" if dec_rcue.validation else "" ) # Déjà enregistré ? niveau_rcue_class = "" if dec_rcue.code_valide is not None and dec_rcue.codes: if dec_rcue.code_valide == dec_rcue.codes[0]: niveau_rcue_class = "recorded" else: niveau_rcue_class = "recorded_different" return f""" <div class="but_niveau_rcue {niveau_rcue_class} "> <div class="but_note with_scoplement"> <div>{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div> {scoplement} </div> <div class="but_code"> {_gen_but_select("code_rcue_"+str(niveau.id), dec_rcue.codes, dec_rcue.code_valide, disabled=True, klass="manual code_rcue", data = { "niveau_id" : str(niveau.id)} )} </div> </div> """ def jury_but_semestriel( formsemestre: FormSemestre, etud: Identite, read_only: bool, navigation_div: str = "", ) -> str: """Page: formulaire saisie décision d'UE d'un semestre BUT isolé (pas jury annuel).""" res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre) parcour, ues = jury_but.list_ue_parcour_etud(formsemestre, etud, res) inscription_etat = etud.inscription_etat(formsemestre.id) semestre_terminal = ( formsemestre.semestre_id >= formsemestre.formation.get_cursus().NB_SEM ) autorisations_passage = ScolarAutorisationInscription.query.filter_by( etudid=etud.id, origin_formsemestre_id=formsemestre.id, ).all() # Par défaut: autorisé à passer dans le semestre suivant si sem. impair, # ou si décision déjà enregistrée: est_autorise_a_passer = (formsemestre.semestre_id % 2) or ( formsemestre.semestre_id + 1 ) in (a.semestre_id for a in autorisations_passage) decisions_ues = { ue.id: DecisionsProposeesUE(etud, formsemestre, ue, inscription_etat) for ue in ues } for dec_ue in decisions_ues.values(): dec_ue.compute_codes() if request.method == "POST": if not read_only: for key in request.form: code = request.form[key] # Codes d'UE code_match = re.match(r"^code_ue_(\d+)$", key) if code_match: ue_id = int(code_match.group(1)) dec_ue = decisions_ues.get(ue_id) if not dec_ue: raise ScoValueError(f"UE invalide ue_id={ue_id}") dec_ue.record(code) db.session.commit() flash("codes enregistrés") if not semestre_terminal: if request.form.get("autorisation_passage"): if not formsemestre.semestre_id + 1 in ( a.semestre_id for a in autorisations_passage ): ScolarAutorisationInscription.delete_autorisation_etud( etud.id, formsemestre.id ) ScolarAutorisationInscription.autorise_etud( etud.id, formsemestre.formation.formation_code, formsemestre.id, formsemestre.semestre_id + 1, ) db.session.commit() flash( f"""autorisation de passage en S{formsemestre.semestre_id + 1 } enregistrée""" ) else: if est_autorise_a_passer: ScolarAutorisationInscription.delete_autorisation_etud( etud.id, formsemestre.id ) db.session.commit() flash( f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée" ) return flask.redirect( url_for( "notes.formsemestre_validation_but", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, etudid=etud.id, ) ) # GET if formsemestre.semestre_id % 2 == 0: warning = f"""<div class="warning"> Cet étudiant de S{formsemestre.semestre_id} ne peut pas passer en jury BUT annuel car il lui manque le semestre précédent. </div>""" else: warning = "" H = [ html_sco_header.sco_header( page_title=f"Validation BUT S{formsemestre.semestre_id}", formsemestre_id=formsemestre.id, etudid=etud.id, cssstyles=("css/jury_but.css",), javascripts=("js/jury_but.js",), ), f""" <div class="jury_but"> <div> <div class="bull_head"> <div> <div class="titre_parcours">Jury BUT S{formsemestre.id} - Parcours {(parcour.libelle if parcour else False) or "non spécifié"} </div> <div class="nom_etud">{etud.nomprenom}</div> </div> <div class="bull_photo"><a href="{ url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id) }">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a> </div> </div> <h3>Jury sur un semestre BUT isolé (ne concerne que les UEs)</h3> {warning} </div> <form method="post" id="jury_but"> """, ] erase_span = "" if not read_only: # Requête toutes les validations (pas seulement celles du deca courant), # au cas où: changement d'architecture, saisie en mode classique, ... validations = ScolarFormSemestreValidation.query.filter_by( etudid=etud.id, formsemestre_id=formsemestre.id ).all() if validations: erase_span = f"""<a href="{ url_for("notes.formsemestre_jury_but_erase", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, etudid=etud.id, only_one_sem=1) }" class="stdlink">effacer les décisions enregistrées</a>""" else: erase_span = ( "Cet étudiant n'a aucune décision enregistrée pour ce semestre." ) H.append( f""" <div class="but_section_annee"> </div> <div><b>Unités d'enseignement de S{formsemestre.semestre_id}:</b></div> """ ) if not ues: H.append( """<div class="warning">Aucune UE ! Vérifiez votre programme de formation, et l'association UEs / Niveaux de compétences</div>""" ) else: H.append( """ <div class="but_annee"> <div class="titre"></div> <div class="titre"></div> <div class="titre"></div> <div class="titre"></div> """ ) for ue in ues: dec_ue = decisions_ues[ue.id] H.append("""<div class="but_niveau_titre"><div></div></div>""") H.append( _gen_but_niveau_ue( ue, dec_ue, disabled=read_only, ) ) H.append( """<div style=""></div> <div class=""></div>""" ) H.append("</div>") # but_annee div_autorisations_passage = ( f""" <div class="but_autorisations_passage"> <span>Autorisé à passer en :</span> { ", ".join( ["S" + str(a.semestre_id or '') for a in autorisations_passage ] )} </div> """ if autorisations_passage else """<div class="but_autorisations_passage but_explanation">pas d'autorisations de passage enregistrées.</div>""" ) H.append(div_autorisations_passage) if read_only: H.append( f"""<div class="but_explanation"> {"Vous n'avez pas la permission de modifier ces décisions." if formsemestre.etat else "Semestre verrouillé."} Les champs entourés en vert sont enregistrés. </div> """ ) else: if formsemestre.semestre_id < formsemestre.formation.get_cursus().NB_SEM: H.append( f""" <div class="but_settings"> <input type="checkbox" name="autorisation_passage" value="1" { "checked" if est_autorise_a_passer else ""}> <em>autoriser à passer dans le semestre S{formsemestre.semestre_id+1}</em> </input> </div> """ ) else: H.append("""<div class="help">dernier semestre de la formation.</div>""") H.append( f""" <div class="but_buttons"> <span><input type="submit" value="Enregistrer ces décisions"></span> <span>{erase_span}</span> </div> """ ) H.append(navigation_div) H.append("</div>") H.append( render_template( "but/documentation_codes_jury.j2", nom_univ=f"""Export {sco_preferences.get_preference("InstituteName") or sco_preferences.get_preference("UnivName") or "Apogée"}""", codes=ScoDocSiteConfig.get_codes_apo_dict(), ) ) return "\n".join(H) # ------------- def infos_fiche_etud_html(etudid: int) -> str: """Section html pour fiche etudiant provisoire pour BUT 2022 """ etud: Identite = Identite.query.get_or_404(etudid) inscriptions = ( FormSemestreInscription.query.join(FormSemestreInscription.formsemestre) .filter( FormSemestreInscription.etudid == etud.id, ) .order_by(FormSemestre.date_debut) ) formsemestres_but = [ i.formsemestre for i in inscriptions if i.formsemestre.formation.is_apc() ] if len(formsemestres_but) == 0: return "" # temporaire quick & dirty: affiche le dernier try: deca = DecisionsProposeesAnnee(etud, formsemestres_but[-1]) return f"""<div class="infos_but"> {show_etud(deca, read_only=True)} </div> """ except ScoValueError: pass return ""