forked from ScoDoc/ScoDoc
1460 lines
54 KiB
Python
1460 lines
54 KiB
Python
# -*- 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 url_for, flash, g, request
|
|
from flask_login import current_user
|
|
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
|
|
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.scolog import logdb
|
|
from app.scodoc.codes_cursus import *
|
|
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
|
|
|
from app.scodoc import html_sco_header
|
|
from app.scodoc import sco_assiduites
|
|
from app.scodoc import codes_cursus
|
|
from app.scodoc import sco_cache
|
|
from app.scodoc import sco_edit_ue
|
|
from app.scodoc import sco_etud
|
|
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_photos
|
|
from app.scodoc import sco_preferences
|
|
from app.scodoc import sco_pv_dict
|
|
from app.scodoc.sco_permissions import Permission
|
|
|
|
|
|
# ------------------------------------------------------------------------------------
|
|
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"""
|
|
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
|
|
|
|
etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
Se = sco_cursus.get_situation_etud_cursus(etud_d, 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 = [
|
|
html_sco_header.sco_header(
|
|
page_title=f"Parcours {etud.nomprenom}",
|
|
javascripts=["js/recap_parcours.js"],
|
|
)
|
|
]
|
|
|
|
# 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 = ["""<div class="jury_footer"><span>"""]
|
|
if url_prev:
|
|
footer.append(
|
|
f'< <a class="stdlink" href="{url_prev}">{etud_prev.nomprenom}</a>'
|
|
)
|
|
footer.append(
|
|
f"""</span><span><a class="stdlink" href="{url_tableau}">retour à la liste</a></span><span>"""
|
|
)
|
|
if url_next:
|
|
footer.append(
|
|
f'<a class="stdlink" href="{url_next}">{etud_next.nomprenom}</a> >'
|
|
)
|
|
footer.append("</span></div>")
|
|
|
|
footer.append(html_sco_header.sco_footer())
|
|
|
|
H.append('<table style="width: 100%"><tr><td>')
|
|
if not check:
|
|
H.append(
|
|
f"""<h2 class="formsemestre">{etud.nomprenom}: validation {
|
|
Se.parcours.SESSION_NAME_A}{Se.parcours.SESSION_NAME
|
|
}</h2>Parcours: {Se.get_cursus_descr()}
|
|
"""
|
|
)
|
|
else:
|
|
H.append(
|
|
f"""<h2 class="formsemestre">Parcours de {etud.nomprenom}</h2>{Se.get_cursus_descr()}"""
|
|
)
|
|
|
|
H.append(
|
|
f"""</td><td style="text-align: right;"><a href="{
|
|
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a></td></tr>
|
|
</table>
|
|
"""
|
|
)
|
|
|
|
etud_etat = nt.get_etud_etat(etudid)
|
|
if etud_etat == scu.DEMISSION:
|
|
H.append('<div class="ue_warning"><span>Etudiant démissionnaire</span></div>')
|
|
if etud_etat == scu.DEF:
|
|
H.append('<div class="ue_warning"><span>Etudiant défaillant</span></div>')
|
|
if etud_etat != scu.INSCRIT:
|
|
H.append(
|
|
f"""
|
|
<div class="warning">
|
|
Impossible de statuer sur cet étudiant:
|
|
il est démissionnaire ou défaillant (voir <a href="{
|
|
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
|
}">sa fiche</a>)
|
|
</div>
|
|
"""
|
|
)
|
|
return "\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'<ul><li><a href="{dest_url}">Continuer</a></li></ul>')
|
|
|
|
return "\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
|
|
<a class="stdlink"
|
|
href="{
|
|
url_for( "notes.formsemestre_status",
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
|
}">tableau de bord</a>)
|
|
"""
|
|
)
|
|
)
|
|
return "\n".join(H + footer)
|
|
|
|
evaluations_a_debloquer = Evaluation.get_evaluations_blocked_for_etud(
|
|
formsemestre, etud
|
|
)
|
|
if evaluations_a_debloquer:
|
|
links_evals = [
|
|
f"""<a class="stdlink" href="{url_for(
|
|
'notes.evaluation_listenotes', scodoc_dept=g.scodoc_dept, evaluation_id=e.id
|
|
)}">{e.description} en {e.moduleimpl.module.code}</a>"""
|
|
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 "\n".join(H + footer)
|
|
|
|
# Infos si pas de semestre précédent
|
|
if not Se.prev:
|
|
if Se.sem["semestre_id"] == 1:
|
|
H.append("<p>Premier semestre (pas de précédent)</p>")
|
|
else:
|
|
H.append("<p>Pas de semestre précédent !</p>")
|
|
else:
|
|
if not Se.prev_decision:
|
|
H.append(
|
|
tf_error_message(
|
|
f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
|
|
url_for("notes.formsemestre_validation_etud_form",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=Se.prev["formsemestre_id"],
|
|
etudid=etudid)
|
|
}">le faire maintenant</a>)
|
|
"""
|
|
)
|
|
)
|
|
if decision_jury:
|
|
H.append(
|
|
f"""<a href="{
|
|
url_for("notes.formsemestre_validation_suppress_etud",
|
|
scodoc_dept=g.scodoc_dept,
|
|
etudid=etudid, formsemestre_id=formsemestre_id
|
|
)
|
|
}" class="stdlink">Supprimer décision existante</a>
|
|
"""
|
|
)
|
|
H.append(html_sco_header.sco_footer())
|
|
return "\n".join(H)
|
|
|
|
# Infos sur decisions déjà saisies
|
|
if decision_jury:
|
|
if decision_jury["assidu"]:
|
|
ass = "assidu"
|
|
else:
|
|
ass = "non assidu"
|
|
H.append("<p>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("</p>")
|
|
|
|
# Cas particulier pour ATJ: corriger precedent avant de continuer
|
|
if Se.prev_decision and Se.prev_decision["code"] == ATJ:
|
|
H.append(
|
|
"""<div class="sfv_warning"><p>La décision du semestre précédent est en
|
|
<b>attente</b> à cause d\'un <b>problème d\'assiduité<b>.</p>
|
|
<p>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é.</p>
|
|
<form method="get" action="formsemestre_validation_etud_form">
|
|
<input type="submit" value="Statuer sur le semestre précédent"/>
|
|
<input type="hidden" name="formsemestre_id" value="%s"/>
|
|
<input type="hidden" name="etudid" value="%s"/>
|
|
<input type="hidden" name="desturl" value="formsemestre_validation_etud_form?etudid=%s&formsemestre_id=%s"/>
|
|
"""
|
|
% (Se.prev["formsemestre_id"], etudid, etudid, formsemestre_id)
|
|
)
|
|
if sortcol:
|
|
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
|
|
H.append("</form></div>")
|
|
|
|
H.append(html_sco_header.sco_footer())
|
|
return "\n".join(H)
|
|
|
|
# Explication sur barres actuelles
|
|
H.append('<p class="sfv_explication">L\'étudiant ')
|
|
if Se.barre_moy_ok:
|
|
H.append("a la moyenne générale, ")
|
|
else:
|
|
H.append("<b>n'a pas</b> 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 <b>compenser</b> avec le précédent")
|
|
H.append(".</p>")
|
|
|
|
# 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(
|
|
"""<form method="get" action="formsemestre_validation_etud" id="formvalid" class="sfv_decisions">
|
|
<input type="hidden" name="etudid" value="%s"/>
|
|
<input type="hidden" name="formsemestre_id" value="%s"/>"""
|
|
% (etudid, formsemestre_id)
|
|
)
|
|
if dest_url:
|
|
H.append('<input type="hidden" name="desturl" value="%s"/>' % dest_url)
|
|
if sortcol:
|
|
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
|
|
|
|
H.append('<h3 class="sfv">Décisions <em>recommandées</em> :</h3>')
|
|
H.append("<table>")
|
|
H.append(rows_assidu)
|
|
if rows_non_assidu:
|
|
H.append("<tr><td> </td></tr>") # spacer
|
|
H.append(rows_non_assidu)
|
|
|
|
H.append("</table>")
|
|
H.append(
|
|
'<p><br></p><input type="submit" value="Valider ce choix" disabled="1" id="subut"/>'
|
|
)
|
|
H.append("</form>")
|
|
|
|
H.append(form_decision_manuelle(Se, formsemestre_id, etudid))
|
|
|
|
H.append(
|
|
f"""<div class="link_defaillance">Ou <a class="stdlink" href="{
|
|
url_for("scolar.form_def", scodoc_dept=g.scodoc_dept, etudid=etudid,
|
|
formsemestre_id=formsemestre_id)
|
|
}">déclarer l'étudiant comme défaillant dans ce semestre</a></div>"""
|
|
)
|
|
|
|
H.append('<p style="font-size: 50%;">Formation ')
|
|
if Se.sem["gestion_semestrielle"]:
|
|
H.append("avec semestres décalés</p>")
|
|
else:
|
|
H.append("sans semestres décalés</p>")
|
|
|
|
return "".join(H + footer)
|
|
|
|
|
|
def formsemestre_validation_etud(
|
|
formsemestre_id=None, # required
|
|
etudid=None, # required
|
|
codechoice=None, # required
|
|
desturl="",
|
|
sortcol=None,
|
|
):
|
|
"""Enregistre validation"""
|
|
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
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 = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
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 ""
|
|
TitlePrev = ""
|
|
if Se.prev:
|
|
if Se.prev["semestre_id"] >= 0:
|
|
TitlePrev = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.prev["semestre_id"])
|
|
else:
|
|
TitlePrev = "Prec."
|
|
|
|
if Se.sem["semestre_id"] >= 0:
|
|
TitleCur = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.sem["semestre_id"])
|
|
else:
|
|
TitleCur = Se.parcours.SESSION_NAME
|
|
|
|
H = [
|
|
'<tr class="%s titles"><th class="sfv_subtitle">%s</em></th>'
|
|
% (trclass, subtitle)
|
|
]
|
|
if Se.prev:
|
|
H.append("<th>Code %s</th>" % TitlePrev)
|
|
H.append("<th>Code %s</th><th>Devenir</th></tr>" % TitleCur)
|
|
for ch in choices:
|
|
H.append(
|
|
"""<tr class="%s"><td title="règle %s"><input type="radio" name="codechoice" value="%s" onClick="document.getElementById('subut').disabled=false;">"""
|
|
% (trclass, ch.rule_id, ch.codechoice)
|
|
)
|
|
H.append("%s </input></td>" % ch.explication)
|
|
if Se.prev:
|
|
H.append('<td class="centercell">%s</td>' % _dispcode(ch.new_code_prev))
|
|
H.append(
|
|
'<td class="centercell">%s</td><td>%s</td>'
|
|
% (_dispcode(ch.code_etat), Se.explique_devenir(ch.devenir))
|
|
)
|
|
H.append("</tr>")
|
|
|
|
return "\n".join(H)
|
|
|
|
|
|
def formsemestre_recap_parcours_table(
|
|
situation_etud_cursus: sco_cursus_dut.SituationEtudCursus,
|
|
etudid,
|
|
with_links=False,
|
|
with_all_columns=True,
|
|
a_url="",
|
|
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 = '<span onclick="toggle_vis(this);" class="toggle_sem sem_%%s">%s</span>'
|
|
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"""<table class="recap_parcours{sd}">
|
|
<tr>
|
|
<th><span onclick="toggle_all_sems(this);"
|
|
title="Ouvrir/fermer tous les semestres">{
|
|
scu.icontag("plus18_img", width=18, height=18, border=0, title="", alt="+")
|
|
}</span></th>
|
|
<th></th>
|
|
<th>Semestre</th>
|
|
<th>Etat</th>
|
|
<th>Abs</th>
|
|
"""
|
|
)
|
|
# titres des UE
|
|
H.append("<th></th>" * situation_etud_cursus.nb_max_ue)
|
|
#
|
|
if with_links:
|
|
H.append("<th></th>")
|
|
H.append("<th></th></tr>")
|
|
|
|
num_sem = 0
|
|
for sem in situation_etud_cursus.get_semestres():
|
|
is_prev = situation_etud_cursus.prev and (
|
|
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
|
|
)
|
|
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
|
|
num_sem += 1
|
|
|
|
dpv = sco_pv_dict.dict_pvjury(sem["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 = ""
|
|
|
|
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
|
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 sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
|
class_sem += " sem_autre_formation"
|
|
if sem["bul_bgcolor"]:
|
|
bgcolor = sem["bul_bgcolor"]
|
|
else:
|
|
bgcolor = "background-color: rgb(255,255,240)"
|
|
# 1ere ligne: titre sem, decision, acronymes UE
|
|
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, sem["formsemestre_id"]))
|
|
if is_cur:
|
|
pm = ""
|
|
elif is_prev:
|
|
pm = minuslink % sem["formsemestre_id"]
|
|
else:
|
|
pm = plusminus % sem["formsemestre_id"]
|
|
|
|
inscr = formsemestre.etuds_inscriptions.get(etudid)
|
|
parcours_name = ""
|
|
if inscr and nt.is_apc:
|
|
if inscr.parcour:
|
|
parcours_name = (
|
|
f' <span class="code_parcours">{inscr.parcour.code}</span>'
|
|
)
|
|
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"""
|
|
<span class="code_parcours no_parcours">{scu.EMO_WARNING} pas de parcours
|
|
</span>"""
|
|
|
|
H.append(
|
|
f"""
|
|
<td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
|
|
<td class="datedebut">{sem['mois_debut']}</td>
|
|
<td class="rcp_titre_sem"><a class="formsemestre_status_link"
|
|
href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
|
|
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
|
|
"""
|
|
)
|
|
if nt.is_apc:
|
|
H.append(
|
|
f"""<a class="stdlink jury_link" title="Validations du semestre BUT"
|
|
href="{ url_for("notes.formsemestre_validation_but",
|
|
scodoc_dept=g.scodoc_dept,
|
|
formsemestre_id=formsemestre.id,
|
|
etudid=etudid,
|
|
)
|
|
}">jury</a>"""
|
|
)
|
|
H.append("""</td>""")
|
|
|
|
if nt.is_apc:
|
|
H.append('<td class="rcp_but">BUT</td>')
|
|
elif decision_sem:
|
|
H.append(
|
|
f"""<td class="rcp_dec">{
|
|
decision_sem["code"]}</td>"""
|
|
)
|
|
else:
|
|
H.append("<td><em>en cours</em></td>")
|
|
H.append(f"""<td class="rcp_nonass">{ass}</td>""") # 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, sem["formsemestre_id"], ue.id)
|
|
or etud_ue_status[ue.id]["is_capitalized"]
|
|
]
|
|
|
|
for ue in ues:
|
|
H.append(f"""<td class="ue_acro"><span>{ue.acronyme}</span></td>""")
|
|
if len(ues) < situation_etud_cursus.nb_max_ue:
|
|
H.append(
|
|
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
|
|
)
|
|
# 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"""<td><em>compense S{csem["semestre_id"]}</em></td>""")
|
|
else:
|
|
H.append("<td></td>")
|
|
if with_links:
|
|
H.append("<td></td>")
|
|
H.append("</tr>")
|
|
# 2eme ligne: notes
|
|
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">""")
|
|
H.append(
|
|
f"""<td class="rcp_type_sem"
|
|
style="background-color:{bgcolor};"> </td>"""
|
|
)
|
|
if is_prev:
|
|
default_sem_info = '<span class="fontred">[sem. précédent]</span>'
|
|
else:
|
|
default_sem_info = ""
|
|
if not sem["etat"]: # locked
|
|
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
|
default_sem_info += lockicon
|
|
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
|
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
|
|
H.append(
|
|
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
|
|
% (sem["mois_fin"], sem_info.get(sem["formsemestre_id"], default_sem_info))
|
|
)
|
|
# Moy Gen (sous le code decision)
|
|
H.append(
|
|
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
|
)
|
|
# Absences (nb d'abs non just. dans ce semestre)
|
|
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
|
|
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
|
|
|
|
# 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"""<td class="{class_ue}" title="{
|
|
" ".join(explanation_ue)
|
|
}">{moy_ue_txt}</td>"""
|
|
)
|
|
if len(ues) < situation_etud_cursus.nb_max_ue:
|
|
H.append(
|
|
f"""<td colspan="{situation_etud_cursus.nb_max_ue - len(ues)}"></td>"""
|
|
)
|
|
|
|
H.append("<td></td>")
|
|
if with_links:
|
|
H.append(
|
|
'<td><a href="%sformsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">modifier</a></td>'
|
|
% (a_url, sem["formsemestre_id"], etudid)
|
|
)
|
|
|
|
H.append("</tr>")
|
|
# 3eme ligne: ECTS
|
|
if (
|
|
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
|
|
or nt.parcours.ECTS_ONLY
|
|
):
|
|
etud_ects_infos = nt.get_etud_ects_pot(etudid) # ECTS potentiels
|
|
H.append(
|
|
f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">
|
|
<td class="rcp_type_sem" style="background-color:{bgcolor};"> </td>
|
|
<td></td>"""
|
|
)
|
|
# Total ECTS (affiché sous la moyenne générale)
|
|
H.append(
|
|
f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
|
|
<td class="sem_ects">{pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}</td>
|
|
<td class="rcp_abs"></td>
|
|
"""
|
|
)
|
|
# 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"""<td class="ue"
|
|
title="{ects:2.2g}/{ects_pot:2.2g} ECTS">{ects:2.2g}</td>"""
|
|
)
|
|
else:
|
|
H.append("""<td class="ue"></td>""")
|
|
H.append("<td></td></tr>")
|
|
|
|
H.append("</table>")
|
|
return "\n".join(H)
|
|
|
|
|
|
def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None):
|
|
"""Formulaire pour saisie décision manuelle"""
|
|
H = [
|
|
"""
|
|
<script type="text/javascript">
|
|
function IsEmpty(aTextField) {
|
|
if ((aTextField.value.length==0) || (aTextField.value==null)) {
|
|
return true;
|
|
} else { return false; }
|
|
}
|
|
function check_sfv_form() {
|
|
if (IsEmpty(document.forms.formvalidmanu.code_etat)) {
|
|
alert('Choisir un code semestre !');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
</script>
|
|
|
|
<form method="get" action="formsemestre_validation_etud_manu" name="formvalidmanu" id="formvalidmanu" class="sfv_decisions sfv_decisions_manuelles" onsubmit="return check_sfv_form()">
|
|
<input type="hidden" name="etudid" value="%s"/>
|
|
<input type="hidden" name="formsemestre_id" value="%s"/>
|
|
"""
|
|
% (etudid, formsemestre_id)
|
|
]
|
|
if desturl:
|
|
H.append('<input type="hidden" name="desturl" value="%s"/>' % desturl)
|
|
if sortcol:
|
|
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
|
|
|
|
H.append(
|
|
'<h3 class="sfv">Décisions manuelles : <em>(vérifiez bien votre choix !)</em></h3><table>'
|
|
)
|
|
|
|
# Choix code semestre:
|
|
codes = sorted(codes_cursus.CODES_JURY_SEM)
|
|
# fortuitement, cet ordre convient bien !
|
|
|
|
H.append(
|
|
'<tr><td>Code semestre: </td><td><select name="code_etat"><option value="" selected>Choisir...</option>'
|
|
)
|
|
for cod in codes:
|
|
if cod in Se.parcours.UNUSED_CODES:
|
|
continue
|
|
if cod != ADC:
|
|
H.append(
|
|
'<option value="%s">%s (code %s)</option>'
|
|
% (cod, codes_cursus.CODES_EXPL[cod], cod)
|
|
)
|
|
elif Se.sem["gestion_compensation"]:
|
|
# traitement spécial pour ADC (compensation)
|
|
# ne propose que les semestres avec lesquels on peut compenser
|
|
# le code transmis est ADC_formsemestre_id
|
|
# on propose aussi une compensation sans utiliser de semestre, pour les cas ou le semestre
|
|
# précédent n'est pas géré dans ScoDoc (code ADC_)
|
|
# log(str(Se.sems))
|
|
for sem in Se.sems:
|
|
if sem["can_compensate"]:
|
|
H.append(
|
|
'<option value="%s_%s">Admis par compensation avec S%s (%s)</option>'
|
|
% (
|
|
cod,
|
|
sem["formsemestre_id"],
|
|
sem["semestre_id"],
|
|
sem["date_debut"],
|
|
)
|
|
)
|
|
if Se.could_be_compensated():
|
|
H.append(
|
|
'<option value="ADC_">Admis par compensation (avec un semestre hors ScoDoc)</option>'
|
|
)
|
|
H.append("</select></td></tr>")
|
|
|
|
# Choix code semestre precedent:
|
|
if Se.prev:
|
|
H.append(
|
|
'<tr><td>Code semestre précédent: </td><td><select name="new_code_prev"><option value="">Choisir une décision...</option>'
|
|
)
|
|
for cod in codes:
|
|
if cod == ADC: # ne propose pas ce choix
|
|
continue
|
|
if Se.prev_decision and cod == Se.prev_decision["code"]:
|
|
sel = "selected"
|
|
else:
|
|
sel = ""
|
|
H.append(
|
|
'<option value="%s" %s>%s (code %s)</option>'
|
|
% (cod, sel, codes_cursus.CODES_EXPL[cod], cod)
|
|
)
|
|
H.append("</select></td></tr>")
|
|
|
|
# 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(
|
|
'<tr><td>Devenir: </td><td><select name="devenir"><option value="" selected>Choisir...</option>'
|
|
)
|
|
for cod in codes:
|
|
if cod in allowed_codes: # or Se.sem['gestion_semestrielle'] == '1'
|
|
H.append('<option value="%s">%s</option>' % (cod, Se.explique_devenir(cod)))
|
|
H.append("</select></td></tr>")
|
|
|
|
H.append(
|
|
'<tr><td><input type="checkbox" name="assidu" checked="checked">assidu</input></td></tr>'
|
|
)
|
|
|
|
H.append(
|
|
"""</table>
|
|
<input type="submit" name="formvalidmanu_submit" value="Valider décision manuelle"/>
|
|
<span style="padding-left: 5em;"><a href="formsemestre_validation_suppress_etud?etudid=%s&formsemestre_id=%s" class="stdlink">Supprimer décision existante</a></span>
|
|
</form>
|
|
"""
|
|
% (etudid, formsemestre_id)
|
|
)
|
|
return "\n".join(H)
|
|
|
|
|
|
# -----------
|
|
def formsemestre_validation_auto(formsemestre_id):
|
|
"Formulaire saisie automatisee des decisions d'un semestre"
|
|
H = [
|
|
html_sco_header.html_sem_header("Saisie automatique des décisions du semestre"),
|
|
f"""
|
|
<ul>
|
|
<li>Seuls les étudiants qui obtiennent le semestre seront affectés (code ADM, moyenne générale et
|
|
toutes les barres, semestre précédent validé);</li>
|
|
<li>le semestre précédent, s'il y en a un, doit avoir été validé;</li>
|
|
<li>les décisions du semestre précédent ne seront pas modifiées;</li>
|
|
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
|
|
<li>les étudiants avec des notes en attente sont ignorés.</li>
|
|
</ul>
|
|
<p>Il est donc vivement conseillé de relire soigneusement les décisions à l'issue
|
|
de cette procédure !</p>
|
|
<form action="do_formsemestre_validation_auto">
|
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
|
|
<input type="submit" value="Calculer automatiquement ces décisions"/>
|
|
<p><em>Le calcul prend quelques minutes, soyez patients !</em></p>
|
|
</form>
|
|
""",
|
|
html_sco_header.sco_footer(),
|
|
]
|
|
return "\n".join(H)
|
|
|
|
|
|
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 = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
|
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)
|
|
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"""{html_sco_header.sco_header(page_title="Saisie automatique")}
|
|
<h2>Saisie automatique des décisions du semestre {formsemestre.titre_annee()}</h2>
|
|
<p>Opération effectuée.</p>
|
|
<p>{nb_valid} étudiants validés sur {len(etudids)}</p>
|
|
"""
|
|
]
|
|
if conflicts:
|
|
H.append(
|
|
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés
|
|
car décisions différentes déja saisies :
|
|
<ul>"""
|
|
)
|
|
for etud in conflicts:
|
|
H.append(
|
|
f"""<li><a href="{
|
|
url_for('notes.formsemestre_validation_etud_form',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
|
|
etudid=etud["etudid"], check=1)
|
|
}">{etud["nomprenom"]}</li>"""
|
|
)
|
|
H.append("</ul>")
|
|
H.append(
|
|
f"""<a href="{url_for('notes.formsemestre_recapcomplet',
|
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
|
|
}">continuer</a>"""
|
|
)
|
|
H.append(html_sco_header.sco_footer())
|
|
return "\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("%d/%m/%Y"),
|
|
},
|
|
),
|
|
(
|
|
"moy_ue",
|
|
{
|
|
"type": "float",
|
|
"allow_null": False,
|
|
"min_value": 0,
|
|
"max_value": 20,
|
|
"title": "Moyenne (/20) obtenue dans cette UE:",
|
|
},
|
|
),
|
|
(
|
|
"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 f"""
|
|
{html_sco_header.sco_header(
|
|
page_title="Validation UE antérieure",
|
|
javascripts=["js/validate_previous_ue.js"],
|
|
cssstyles=["css/jury_delete_manual.css"],
|
|
etudid=etud.id,
|
|
formsemestre_id=formsemestre.id,
|
|
)}
|
|
<h2 class="formsemestre">Gestion des validations d'UEs antérieures
|
|
de {etud.html_link_fiche()}
|
|
</h2>
|
|
|
|
<p class="help">Utiliser cette page pour enregistrer des UEs validées antérieurement,
|
|
<em>dans un semestre hors ScoDoc</em>.</p>
|
|
|
|
<div class="scobox explanation">
|
|
<p><b>Les UE validées dans ScoDoc sont
|
|
automatiquement prises en compte</b>.
|
|
</p>
|
|
<p>Cette page est surtout utile pour les étudiants ayant
|
|
suivi un début de cursus dans <b>un autre établissement</b>, ou qui
|
|
ont suivi une UE à l'étranger ou dans un semestre géré <b>sans ScoDoc</b>.
|
|
</>
|
|
<p>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).
|
|
</p>
|
|
<p>Pour les semestres précédents gérés avec ScoDoc, passer par la page jury normale.
|
|
</p>
|
|
<p>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).
|
|
</p>
|
|
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
|
|
</div>
|
|
|
|
{_get_etud_ue_cap_html(etud, formsemestre)}
|
|
|
|
<div class="scobox">
|
|
<div class="scobox-title">
|
|
Enregistrer une UE antérieure
|
|
</div>
|
|
{tf[1]}
|
|
</div>
|
|
<div id="ue_list_code" class="sco_box sco_green_bg">
|
|
<!-- filled by ue_sharing_code -->
|
|
</div>
|
|
{check_formation_ues(formation)[0]}
|
|
{html_sco_header.sco_footer()}
|
|
"""
|
|
|
|
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_cap_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: list[ScolarFormSemestreValidation] = (
|
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
|
.join(UniteEns)
|
|
.join(Formation)
|
|
.filter_by(formation_code=formsemestre.formation.formation_code)
|
|
.order_by(
|
|
sa.desc(UniteEns.semestre_idx),
|
|
UniteEns.acronyme,
|
|
sa.desc(ScolarFormSemestreValidation.event_date),
|
|
)
|
|
.all()
|
|
)
|
|
|
|
if not validations:
|
|
return ""
|
|
H = [
|
|
f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
|
|
<div class="sco_box_title">Validations d'UEs dans cette formation</div>
|
|
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
|
|
sur des semestres ou déclarées comme "antérieures" (externes).
|
|
</div>
|
|
<ul class="liste_validations">"""
|
|
]
|
|
for validation in validations:
|
|
if validation.formsemestre_id is None:
|
|
origine = " enregistrée d'un parcours antérieur (hors ScoDoc)"
|
|
else:
|
|
origine = f", du semestre {formsemestre.html_link_status()}"
|
|
if validation.semestre_id is not None:
|
|
origine += f" (<b>S{validation.semestre_id}</b>)"
|
|
H.append(f"""<li>{validation.html()}""")
|
|
if (validation.formsemestre and validation.formsemestre.can_edit_jury()) or (
|
|
current_user and current_user.has_permission(Permission.EtudInscrit)
|
|
):
|
|
H.append(
|
|
f"""
|
|
<form class="inline-form">
|
|
<button
|
|
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
|
|
>effacer</button>
|
|
</form>
|
|
""",
|
|
)
|
|
else:
|
|
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
|
|
H.append("</li>")
|
|
H.append("</ul></div>")
|
|
return "\n".join(H)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
logdb(
|
|
cnx,
|
|
method="formsemestre_validate_previous_ue",
|
|
etudid=etudid,
|
|
msg=f"Validation UE prec. {ue_id} {ue.acronyme}: {code}",
|
|
commit=False,
|
|
)
|
|
_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 = [
|
|
"""<div class="ue_warning"><span>Attention:</span> les UE suivantes de cette formation
|
|
sont utilisées dans des
|
|
semestres de rangs différents (eg S1 et S3). <br>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.
|
|
<ul>
|
|
"""
|
|
]
|
|
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']
|
|
} (<em>semestre <b class="fontred">{s['semestre_id']}</b></em>)"""
|
|
for s in sems
|
|
]
|
|
)
|
|
H.append(f"<li><b>{ue.acronyme}</b> : {slist}</li>")
|
|
H.append("</ul></div>")
|
|
|
|
return "\n".join(H), ue_multiples
|