forked from ScoDoc/ScoDoc
547 lines
18 KiB
Python
547 lines
18 KiB
Python
##############################################################################
|
|
# 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 ""
|