forked from ScoDoc/ScoDoc
fix problème symlinks
This commit is contained in:
parent
031dc409be
commit
9474d420df
@ -205,8 +205,18 @@ def create_app(config_class=DevConfig):
|
|||||||
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
|
app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static")
|
||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
app.wsgi_app = ReverseProxied(app.wsgi_app)
|
||||||
app.logger.setLevel(logging.DEBUG)
|
app.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Evite de logguer toutes les requetes dans notre log
|
||||||
|
logging.getLogger("werkzeug").disabled = True
|
||||||
|
|
||||||
app.config.from_object(config_class)
|
app.config.from_object(config_class)
|
||||||
|
|
||||||
|
# Vérifie/crée lien sym pour les URL statiques
|
||||||
|
link_filename = f"{app.root_path}/static/links/{sco_version.SCOVERSION}"
|
||||||
|
if not os.path.exists(link_filename):
|
||||||
|
app.logger.info(f"creating symlink {link_filename}")
|
||||||
|
os.symlink("..", link_filename)
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
migrate.init_app(app, db)
|
migrate.init_app(app, db)
|
||||||
login.init_app(app)
|
login.init_app(app)
|
||||||
|
@ -212,7 +212,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
|||||||
|
|
||||||
formsemestres = query.order_by(FormSemestre.date_debut)
|
formsemestres = query.order_by(FormSemestre.date_debut)
|
||||||
|
|
||||||
return jsonify([formsemestre.to_dict() for formsemestre in formsemestres])
|
return jsonify(
|
||||||
|
[formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
@bp.route(
|
||||||
|
@ -62,7 +62,7 @@ def formsemestre(formsemestre_id: int):
|
|||||||
formsemestre: FormSemestre = models.FormSemestre.query.filter_by(
|
formsemestre: FormSemestre = models.FormSemestre.query.filter_by(
|
||||||
id=formsemestre_id
|
id=formsemestre_id
|
||||||
).first_or_404()
|
).first_or_404()
|
||||||
data = formsemestre.to_dict()
|
data = formsemestre.to_dict(convert_parcours=True)
|
||||||
# Pour le moment on a besoin de fixer le departement
|
# Pour le moment on a besoin de fixer le departement
|
||||||
# pour accéder aux préferences
|
# pour accéder aux préferences
|
||||||
dept = Departement.query.get(formsemestre.dept_id)
|
dept = Departement.query.get(formsemestre.dept_id)
|
||||||
@ -92,13 +92,9 @@ def formsemestre_apo(etape_apo: str):
|
|||||||
FormSemestreEtape.formsemestre_id == FormSemestre.id,
|
FormSemestreEtape.formsemestre_id == FormSemestre.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
res = [formsemestre.to_dict() for formsemestre in formsemestres]
|
return jsonify(
|
||||||
if len(res) == 0:
|
[formsemestre.to_dict(convert_parcours=True) for formsemestre in formsemestres]
|
||||||
return error_response(
|
)
|
||||||
404, message="Aucun formsemestre trouvé avec cette étape apogée"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return jsonify(res)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
|
||||||
|
106
app/but/apc_edit_ue.py
Normal file
106
app/but/apc_edit_ue.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
Edition associations UE <-> Ref. Compétence
|
||||||
|
"""
|
||||||
|
from flask import g, url_for
|
||||||
|
from app import db, log
|
||||||
|
from app.models import Formation, UniteEns
|
||||||
|
from app.models.but_refcomp import ApcNiveau
|
||||||
|
from app.scodoc import sco_codes_parcours
|
||||||
|
|
||||||
|
|
||||||
|
def form_ue_choix_niveau(formation: Formation, ue: UniteEns) -> str:
|
||||||
|
"""Form. HTML pour associer une UE à un niveau de compétence"""
|
||||||
|
if ue.type != sco_codes_parcours.UE_STANDARD:
|
||||||
|
return ""
|
||||||
|
ref_comp = ue.formation.referentiel_competence
|
||||||
|
if ref_comp is None:
|
||||||
|
return f"""<div class="ue_choix_niveau">
|
||||||
|
<div class="warning">Pas de référentiel de compétence associé à cette formation !</div>
|
||||||
|
<div><a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
|
||||||
|
scodoc_dept=g.scodoc_dept, formation_id=ue.formation.id)
|
||||||
|
}">associer un référentiel de compétence</a>
|
||||||
|
</div>
|
||||||
|
</div>"""
|
||||||
|
annee = (ue.semestre_idx + 1) // 2 # 1, 2, 3
|
||||||
|
niveaux_by_parcours = ref_comp.get_niveaux_by_parcours(annee)
|
||||||
|
|
||||||
|
# Les niveaux déjà associés à d'autres UE du même semestre
|
||||||
|
autres_ues = formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||||
|
niveaux_autres_ues = {
|
||||||
|
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||||
|
}
|
||||||
|
options = []
|
||||||
|
if niveaux_by_parcours["TC"]: # TC pour Tronc Commun
|
||||||
|
options.append("""<optgroup label="Tronc commun">""")
|
||||||
|
for n in niveaux_by_parcours["TC"]:
|
||||||
|
if n.id in niveaux_autres_ues:
|
||||||
|
disabled = "disabled"
|
||||||
|
else:
|
||||||
|
disabled = ""
|
||||||
|
options.append(
|
||||||
|
f"""<option value="{n.id}" {'selected'
|
||||||
|
if ue.niveau_competence == n else ''}
|
||||||
|
{disabled}>{n.annee} {n.competence.titre_long}
|
||||||
|
niveau {n.ordre}</option>"""
|
||||||
|
)
|
||||||
|
options.append("""</optgroup>""")
|
||||||
|
for parcour in ref_comp.parcours:
|
||||||
|
if len(niveaux_by_parcours[parcour.id]):
|
||||||
|
options.append(f"""<optgroup label="Parcours {parcour.libelle}">""")
|
||||||
|
for n in niveaux_by_parcours[parcour.id]:
|
||||||
|
if n.id in niveaux_autres_ues:
|
||||||
|
disabled = "disabled"
|
||||||
|
else:
|
||||||
|
disabled = ""
|
||||||
|
options.append(
|
||||||
|
f"""<option value="{n.id}" {'selected'
|
||||||
|
if ue.niveau_competence == n else ''}
|
||||||
|
{disabled}>{n.annee} {n.competence.titre_long}
|
||||||
|
niveau {n.ordre}</option>"""
|
||||||
|
)
|
||||||
|
options.append("""</optgroup>""")
|
||||||
|
options_str = "\n".join(options)
|
||||||
|
return f"""
|
||||||
|
<div class="ue_choix_niveau">
|
||||||
|
<form id="form_ue_choix_niveau">
|
||||||
|
<b>Niveau de compétence associé:</b>
|
||||||
|
<select onchange="set_ue_niveau_competence();" data-setter="{
|
||||||
|
url_for( "notes.set_ue_niveau_competence", scodoc_dept=g.scodoc_dept)
|
||||||
|
}">
|
||||||
|
<option value="" {'selected' if ue.niveau_competence is None else ''}>aucun</option>
|
||||||
|
{options_str}
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def set_ue_niveau_competence(ue_id: int, niveau_id: int):
|
||||||
|
"""Associe le niveau et l'UE"""
|
||||||
|
log(f"set_ue_niveau_competence( {ue_id}, {niveau_id} )")
|
||||||
|
ue = UniteEns.query.get_or_404(ue_id)
|
||||||
|
|
||||||
|
autres_ues = ue.formation.ues.filter_by(semestre_idx=ue.semestre_idx)
|
||||||
|
niveaux_autres_ues = {
|
||||||
|
oue.niveau_competence_id for oue in autres_ues if oue.id != ue.id
|
||||||
|
}
|
||||||
|
if niveau_id in niveaux_autres_ues:
|
||||||
|
log(
|
||||||
|
f"set_ue_niveau_competence: denying association of {ue} to already associated {niveau_id}"
|
||||||
|
)
|
||||||
|
return "", 409 # conflict
|
||||||
|
if niveau_id == "":
|
||||||
|
# suppression de l'association
|
||||||
|
ue.niveau_competence = None
|
||||||
|
else:
|
||||||
|
niveau = ApcNiveau.query.get_or_404(niveau_id)
|
||||||
|
ue.niveau_competence = niveau
|
||||||
|
db.session.add(ue)
|
||||||
|
db.session.commit()
|
||||||
|
return "", 204
|
@ -14,6 +14,7 @@ from flask import url_for, g
|
|||||||
|
|
||||||
from app.comp.res_but import ResultatsSemestreBUT
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
from app.models import FormSemestre, Identite
|
from app.models import FormSemestre, Identite
|
||||||
|
from app.models import but_validations
|
||||||
from app.models.groups import GroupDescr
|
from app.models.groups import GroupDescr
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_bulletins, sco_utils as scu
|
from app.scodoc import sco_bulletins, sco_utils as scu
|
||||||
@ -244,7 +245,7 @@ class BulletinBUT:
|
|||||||
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
|
f"{fmt_note(bonus_vect[ue.id])} sur {ue.acronyme}"
|
||||||
for ue in res.ues
|
for ue in res.ues
|
||||||
if ue.type != UE_SPORT
|
if ue.type != UE_SPORT
|
||||||
and res.modimpls_in_ue(ue.id, etudid)
|
and res.modimpls_in_ue(ue, etudid)
|
||||||
and ue.id in res.bonus_ues
|
and ue.id in res.bonus_ues
|
||||||
and bonus_vect[ue.id] > 0.0
|
and bonus_vect[ue.id] > 0.0
|
||||||
]
|
]
|
||||||
@ -274,6 +275,11 @@ class BulletinBUT:
|
|||||||
etat_inscription = etud.inscription_etat(formsemestre.id)
|
etat_inscription = etud.inscription_etat(formsemestre.id)
|
||||||
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
|
||||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||||
|
if formsemestre.formation.referentiel_competence is None:
|
||||||
|
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
|
||||||
|
else:
|
||||||
|
etud_ues_ids = res.etud_ues_ids(etud.id)
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
"version": "0",
|
"version": "0",
|
||||||
"type": "BUT",
|
"type": "BUT",
|
||||||
@ -318,9 +324,13 @@ class BulletinBUT:
|
|||||||
ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0
|
ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0
|
||||||
ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
|
ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
|
||||||
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
|
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
|
||||||
semestre_infos.update(
|
if sco_preferences.get_preference("bul_show_decision", formsemestre.id):
|
||||||
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
semestre_infos.update(
|
||||||
)
|
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
|
||||||
|
)
|
||||||
|
semestre_infos.update(
|
||||||
|
but_validations.dict_decision_jury(etud, formsemestre)
|
||||||
|
)
|
||||||
if etat_inscription == scu.INSCRIT:
|
if etat_inscription == scu.INSCRIT:
|
||||||
# moyenne des moyennes générales du semestre
|
# moyenne des moyennes générales du semestre
|
||||||
semestre_infos["notes"] = {
|
semestre_infos["notes"] = {
|
||||||
@ -365,10 +375,7 @@ class BulletinBUT:
|
|||||||
)
|
)
|
||||||
for ue in res.ues
|
for ue in res.ues
|
||||||
# si l'UE comporte des modules auxquels on est inscrit:
|
# si l'UE comporte des modules auxquels on est inscrit:
|
||||||
if (
|
if ((ue.type == UE_SPORT) or ue.id in etud_ues_ids)
|
||||||
(ue.type == UE_SPORT)
|
|
||||||
or self.res.modimpls_in_ue(ue.id, etud.id)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
"semestre": semestre_infos,
|
"semestre": semestre_infos,
|
||||||
},
|
},
|
||||||
|
18
app/but/forms/jury_but_forms.py
Normal file
18
app/but/forms/jury_but_forms.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""ScoDoc 9.3 : Formulaires / jurys BUT
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import SubmitField
|
||||||
|
|
||||||
|
|
||||||
|
class FormSemestreValidationAutoBUTForm(FlaskForm):
|
||||||
|
"simple form de confirmation"
|
||||||
|
submit = SubmitField("Lancer le calcul")
|
||||||
|
cancel = SubmitField("Annuler")
|
@ -13,7 +13,9 @@ from wtforms import SelectField, SubmitField
|
|||||||
|
|
||||||
|
|
||||||
class FormationRefCompForm(FlaskForm):
|
class FormationRefCompForm(FlaskForm):
|
||||||
referentiel_competence = SelectField("Référentiels déjà chargés")
|
referentiel_competence = SelectField(
|
||||||
|
"Choisir parmi les référentiels déjà chargés :"
|
||||||
|
)
|
||||||
submit = SubmitField("Valider")
|
submit = SubmitField("Valider")
|
||||||
cancel = SubmitField("Annuler")
|
cancel = SubmitField("Annuler")
|
||||||
|
|
||||||
@ -23,7 +25,7 @@ class RefCompLoadForm(FlaskForm):
|
|||||||
"Choisir un référentiel de compétences officiel BUT"
|
"Choisir un référentiel de compétences officiel BUT"
|
||||||
)
|
)
|
||||||
upload = FileField(
|
upload = FileField(
|
||||||
label="Ou bien sélectionner un fichier XML au format Orébut",
|
label="... ou bien sélectionner un fichier XML au format Orébut (réservé aux développeurs !)",
|
||||||
validators=[
|
validators=[
|
||||||
FileAllowed(
|
FileAllowed(
|
||||||
[
|
[
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
# See LICENSE
|
# See LICENSE
|
||||||
##############################################################################
|
##############################################################################
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
from typing import TextIO
|
|
||||||
|
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
@ -57,13 +56,13 @@ def orebut_import_refcomp(xml_data: str, dept_id: int, orig_filename=None):
|
|||||||
try:
|
try:
|
||||||
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
|
c = ApcCompetence(**ApcCompetence.attr_from_xml(competence.attrib))
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
except sqlalchemy.exc.IntegrityError:
|
except sqlalchemy.exc.IntegrityError as exc:
|
||||||
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
# ne devrait plus se produire car pas d'unicité de l'id: donc inutile
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
|
f"""Un référentiel a déjà été chargé avec les mêmes compétences ! ({competence.attrib["id"]})
|
||||||
"""
|
"""
|
||||||
)
|
) from exc
|
||||||
ref.competences.append(c)
|
ref.competences.append(c)
|
||||||
# --- SITUATIONS
|
# --- SITUATIONS
|
||||||
situations = competence.find("situations")
|
situations = competence.find("situations")
|
||||||
|
1054
app/but/jury_but.py
Normal file
1054
app/but/jury_but.py
Normal file
File diff suppressed because it is too large
Load Diff
137
app/but/jury_but_pv.py
Normal file
137
app/but/jury_but_pv.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Jury BUT: table synthèse résultats semestre / PV
|
||||||
|
"""
|
||||||
|
from flask import g, request, url_for
|
||||||
|
|
||||||
|
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||||
|
|
||||||
|
from app import log
|
||||||
|
from app.but import jury_but
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.scodoc.gen_tables import GenTable
|
||||||
|
from app.scodoc import sco_excel
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
from app.scodoc import sco_preferences
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
def _descr_cursus_but(etud: Identite) -> str:
|
||||||
|
"description de la liste des semestres BUT suivis"
|
||||||
|
# prend simplement tous les semestre de type APC, ce qui sera faux si
|
||||||
|
# l'étudiant change de spécialité au sein du même département
|
||||||
|
# (ce qui ne peut normalement pas se produire)
|
||||||
|
indices = sorted(
|
||||||
|
[
|
||||||
|
ins.formsemestre.semestre_id
|
||||||
|
if ins.formsemestre.semestre_id is not None
|
||||||
|
else -1
|
||||||
|
for ins in etud.formsemestre_inscriptions
|
||||||
|
if ins.formsemestre.formation.is_apc()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return ", ".join(f"S{indice}" for indice in indices)
|
||||||
|
|
||||||
|
|
||||||
|
def pvjury_table_but(formsemestre_id: int, format="html") -> list[dict]:
|
||||||
|
"""Page récapitulant les décisions de jury BUT
|
||||||
|
formsemestre peut être pair ou impair
|
||||||
|
"""
|
||||||
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
assert formsemestre.formation.is_apc()
|
||||||
|
title = "Procès-verbal de jury BUT annuel"
|
||||||
|
|
||||||
|
if format == "html":
|
||||||
|
line_sep = "<br/>"
|
||||||
|
else:
|
||||||
|
line_sep = "\n"
|
||||||
|
# remplace pour le BUT la fonction sco_pvjury.pvjury_table
|
||||||
|
annee_but = (formsemestre.semestre_id + 1) // 2
|
||||||
|
titles = {
|
||||||
|
"nom": "Nom",
|
||||||
|
"cursus": "Cursus",
|
||||||
|
"ues": "UE validées",
|
||||||
|
"niveaux": "Niveaux de compétences validés",
|
||||||
|
"decision_but": f"Décision BUT{annee_but}",
|
||||||
|
"diplome": "Résultat au diplôme",
|
||||||
|
"devenir": "Devenir",
|
||||||
|
"observations": "Observations",
|
||||||
|
}
|
||||||
|
rows = []
|
||||||
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
|
etud: Identite = Identite.query.get(etudid)
|
||||||
|
try:
|
||||||
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
if deca.annee_but != annee_but: # wtf ?
|
||||||
|
log(
|
||||||
|
f"pvjury_table_but: inconsistent annee_but {deca.annee_but} != {annee_but}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
except ScoValueError:
|
||||||
|
deca = None
|
||||||
|
row = {
|
||||||
|
"nom": etud.etat_civil_pv(line_sep=line_sep),
|
||||||
|
"_nom_order": etud.sort_key,
|
||||||
|
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||||
|
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||||
|
"_nom_target": url_for(
|
||||||
|
"scolar.ficheEtud",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
etudid=etud.id,
|
||||||
|
),
|
||||||
|
"cursus": _descr_cursus_but(etud),
|
||||||
|
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||||
|
"niveaux": deca.descr_niveaux_validation(line_sep=line_sep)
|
||||||
|
if deca
|
||||||
|
else "-",
|
||||||
|
"decision_but": deca.code_valide if deca else "",
|
||||||
|
"devenir": ", ".join([f"S{i}" for i in deca.get_autorisations_passage()]),
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
|
rows.sort(key=lambda x: x["_nom_order"])
|
||||||
|
|
||||||
|
# Style excel... passages à la ligne sur \n
|
||||||
|
xls_style_base = sco_excel.excel_make_style()
|
||||||
|
xls_style_base["alignment"] = Alignment(wrapText=True, vertical="top")
|
||||||
|
|
||||||
|
tab = GenTable(
|
||||||
|
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}",
|
||||||
|
caption=title,
|
||||||
|
columns_ids=titles.keys(),
|
||||||
|
html_caption=title,
|
||||||
|
html_class="pvjury_table_but table_leftalign",
|
||||||
|
html_title=f"""<div style="margin-bottom: 8px;"><span style="font-size: 120%; font-weight: bold;">{title}</span>
|
||||||
|
<span style="padding-left: 20px;">
|
||||||
|
<a href="{url_for("notes.pvjury_table_but",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, format="xlsx")}"
|
||||||
|
class="stdlink">version excel</a></span></div>
|
||||||
|
|
||||||
|
""",
|
||||||
|
html_with_td_classes=True,
|
||||||
|
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||||
|
page_title=title,
|
||||||
|
pdf_title=title,
|
||||||
|
preferences=sco_preferences.SemPreferences(),
|
||||||
|
rows=rows,
|
||||||
|
table_id="formation_table_recap",
|
||||||
|
titles=titles,
|
||||||
|
xls_columns_width={
|
||||||
|
"nom": 32,
|
||||||
|
"cursus": 12,
|
||||||
|
"ues": 32,
|
||||||
|
"niveaux": 32,
|
||||||
|
"decision_but": 14,
|
||||||
|
"diplome": 17,
|
||||||
|
"devenir": 8,
|
||||||
|
"observations": 12,
|
||||||
|
},
|
||||||
|
xls_style_base=xls_style_base,
|
||||||
|
)
|
||||||
|
return tab.make_page(format=format, javascripts=["js/etud_info.js"], init_qtip=True)
|
424
app/but/jury_but_recap.py
Normal file
424
app/but/jury_but_recap.py
Normal file
@ -0,0 +1,424 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Jury BUT: table recap annuelle et liens saisie
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import numpy as np
|
||||||
|
from flask import g, url_for
|
||||||
|
|
||||||
|
from app.but import jury_but
|
||||||
|
from app.but.jury_but import (
|
||||||
|
DecisionsProposeesAnnee,
|
||||||
|
DecisionsProposeesRCUE,
|
||||||
|
DecisionsProposeesUE,
|
||||||
|
)
|
||||||
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
|
||||||
|
from app.scodoc.sco_codes_parcours import (
|
||||||
|
BUT_BARRE_RCUE,
|
||||||
|
BUT_BARRE_UE,
|
||||||
|
BUT_BARRE_UE8,
|
||||||
|
BUT_RCUE_SUFFISANT,
|
||||||
|
)
|
||||||
|
from app.scodoc import sco_formsemestre_status
|
||||||
|
from app.scodoc import html_sco_header
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
|
def formsemestre_saisie_jury_but(
|
||||||
|
formsemestre2: FormSemestre,
|
||||||
|
read_only: bool = False,
|
||||||
|
selected_etudid: int = None,
|
||||||
|
mode="jury",
|
||||||
|
) -> str:
|
||||||
|
"""formsemestre est un semestre PAIR
|
||||||
|
Si readonly, ne montre pas le lien "saisir la décision"
|
||||||
|
|
||||||
|
=> page html complète
|
||||||
|
|
||||||
|
Si mode == "recap", table recap des codes, sans liens de saisie.
|
||||||
|
"""
|
||||||
|
# Quick & Dirty
|
||||||
|
# pour chaque etud de res2 trié
|
||||||
|
# S1: UE1, ..., UEn
|
||||||
|
# S2: UE1, ..., UEn
|
||||||
|
#
|
||||||
|
# UE1_s1, UE1_s2, moy_rcue, UE2... , Nbrcue_validables, Nbrcue<8, passage_de_droit, valide_moitie_rcue
|
||||||
|
#
|
||||||
|
# Pour chaque etud de res2 trié
|
||||||
|
# DecisionsProposeesAnnee(etud, formsemestre2)
|
||||||
|
# Pour le 1er etud, faire un check_ues_ready_jury(self) -> page d'erreur
|
||||||
|
# -> rcue .ue_1, .ue_2 -> stroe moy ues, rcue.moy_rcue, etc
|
||||||
|
if formsemestre2.semestre_id % 2 != 0:
|
||||||
|
raise ScoValueError("Cette page ne fonctionne que sur les semestres pairs")
|
||||||
|
|
||||||
|
if formsemestre2.formation.referentiel_competence is None:
|
||||||
|
raise ScoValueError(
|
||||||
|
"""
|
||||||
|
<p>Pas de référentiel de compétences associé à la formation !</p>
|
||||||
|
<p>Pour associer un référentiel, passer par le menu <b>Semestre /
|
||||||
|
Voir la formation... </b> et suivre le lien <em>"associer à un référentiel
|
||||||
|
de compétences"</em>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
rows, titles, column_ids = get_table_jury_but(
|
||||||
|
formsemestre2, read_only=read_only, mode=mode
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
return (
|
||||||
|
'<div class="table_recap"><div class="message">aucun étudiant !</div></div>'
|
||||||
|
)
|
||||||
|
filename = scu.sanitize_filename(
|
||||||
|
f"""jury-but-{formsemestre2.titre_num()}-{time.strftime("%Y-%m-%d")}"""
|
||||||
|
)
|
||||||
|
klass = "table_jury_but_bilan" if mode == "recap" else ""
|
||||||
|
table_html = build_table_jury_but_html(
|
||||||
|
filename, rows, titles, column_ids, selected_etudid=selected_etudid, klass=klass
|
||||||
|
)
|
||||||
|
H = [
|
||||||
|
html_sco_header.sco_header(
|
||||||
|
page_title=f"{formsemestre2.sem_modalite()}: jury BUT annuel",
|
||||||
|
no_side_bar=True,
|
||||||
|
init_qtip=True,
|
||||||
|
javascripts=["js/etud_info.js", "js/table_recap.js"],
|
||||||
|
),
|
||||||
|
sco_formsemestre_status.formsemestre_status_head(
|
||||||
|
formsemestre_id=formsemestre2.id
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if mode == "recap":
|
||||||
|
H.append(
|
||||||
|
f"""<h3>Décisions de jury enregistrées pour les étudiants de ce semestre</h3>
|
||||||
|
<div class="table_jury_but_links">
|
||||||
|
<div>
|
||||||
|
<a href="{url_for(
|
||||||
|
"notes.pvjury_table_but",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
||||||
|
}" class="stdlink">tableau PV de jury</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
|
||||||
|
{table_html}
|
||||||
|
|
||||||
|
<div class="table_jury_but_links">
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
if (mode == "recap") and not read_only:
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<p><a class="stdlink" href="{url_for(
|
||||||
|
"notes.formsemestre_saisie_jury",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
||||||
|
}">Saisie des décisions du jury</a>
|
||||||
|
</p>"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<p><a class="stdlink" href="{url_for(
|
||||||
|
"notes.formsemestre_validation_auto_but",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
||||||
|
}">Calcul automatique des décisions du jury</a>
|
||||||
|
</p>
|
||||||
|
<p><a class="stdlink" href="{url_for(
|
||||||
|
"notes.formsemestre_jury_but_recap",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre2.id)
|
||||||
|
}">Tableau récapitulatif des décisions du jury</a>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{html_sco_header.sco_footer()}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return "\n".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
def build_table_jury_but_html(
|
||||||
|
filename: str, rows, titles, column_ids, selected_etudid: int = None, klass=""
|
||||||
|
) -> str:
|
||||||
|
"""assemble la table html"""
|
||||||
|
footer_rows = [] # inutilisé pour l'instant
|
||||||
|
H = [
|
||||||
|
f"""<div class="table_recap"><table class="table_recap apc jury table_jury_but {klass}"
|
||||||
|
data-filename="{filename}">"""
|
||||||
|
]
|
||||||
|
# header
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<thead>
|
||||||
|
{scu.gen_row(column_ids, titles, "th")}
|
||||||
|
</thead>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
# body
|
||||||
|
H.append("<tbody>")
|
||||||
|
for row in rows:
|
||||||
|
H.append(f"{scu.gen_row(column_ids, row, selected_etudid=selected_etudid)}\n")
|
||||||
|
H.append("</tbody>\n")
|
||||||
|
# footer
|
||||||
|
H.append("<tfoot>")
|
||||||
|
idx_last = len(footer_rows) - 1
|
||||||
|
for i, row in enumerate(footer_rows):
|
||||||
|
H.append(f'{scu.gen_row(column_ids, row, "th" if i == idx_last else "td")}\n')
|
||||||
|
H.append(
|
||||||
|
"""
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return "".join(H)
|
||||||
|
|
||||||
|
|
||||||
|
class RowCollector:
|
||||||
|
"""Une ligne de la table"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
cells: dict = None,
|
||||||
|
titles: dict = None,
|
||||||
|
convert_values=True,
|
||||||
|
column_classes: dict = None,
|
||||||
|
):
|
||||||
|
self.titles = titles
|
||||||
|
self.row = cells or {} # col_id : str
|
||||||
|
self.column_classes = column_classes # col_id : str, css class
|
||||||
|
self.idx = 0
|
||||||
|
self.last_etud_cell_idx = 0
|
||||||
|
if convert_values:
|
||||||
|
self.fmt_note = scu.fmt_note
|
||||||
|
else:
|
||||||
|
self.fmt_note = lambda x: x
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.row[key] = value
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.row[key]
|
||||||
|
|
||||||
|
def get_row_dict(self):
|
||||||
|
"La ligne, comme un dict"
|
||||||
|
# create empty cells
|
||||||
|
for col_id in self.titles:
|
||||||
|
if col_id not in self.row:
|
||||||
|
self.row[col_id] = ""
|
||||||
|
klass = self.column_classes.get(col_id)
|
||||||
|
if klass:
|
||||||
|
self.row[f"_{col_id}_class"] = klass
|
||||||
|
return self.row
|
||||||
|
|
||||||
|
def add_cell(
|
||||||
|
self,
|
||||||
|
col_id: str,
|
||||||
|
title: str,
|
||||||
|
content: str,
|
||||||
|
classes: str = "",
|
||||||
|
idx: int = None,
|
||||||
|
column_class="",
|
||||||
|
):
|
||||||
|
"""Add a row to our table. classes is a list of css class names"""
|
||||||
|
self.idx = idx if idx is not None else self.idx
|
||||||
|
self.row[col_id] = content
|
||||||
|
if classes:
|
||||||
|
self.row[f"_{col_id}_class"] = classes + f" c{self.idx}"
|
||||||
|
if not col_id in self.titles:
|
||||||
|
self.titles[col_id] = title
|
||||||
|
self.titles[f"_{col_id}_col_order"] = self.idx
|
||||||
|
if classes:
|
||||||
|
self.titles[f"_{col_id}_class"] = classes
|
||||||
|
self.column_classes[col_id] = column_class
|
||||||
|
self.idx += 1
|
||||||
|
|
||||||
|
def add_etud_cells(self, etud: Identite, formsemestre: FormSemestre):
|
||||||
|
"Les cells code, nom, prénom etc."
|
||||||
|
# --- Codes (seront cachés, mais exportés en excel)
|
||||||
|
self.add_cell("etudid", "etudid", etud.id, "codes")
|
||||||
|
self.add_cell("code_nip", "code_nip", etud.code_nip or "", "codes")
|
||||||
|
# --- Identité étudiant (adapté de res_comon/get_table_recap, à factoriser XXX TODO)
|
||||||
|
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||||
|
self.add_cell("nom_disp", "Nom", etud.nom_disp(), "identite_detail")
|
||||||
|
self["_nom_disp_order"] = etud.sort_key
|
||||||
|
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||||
|
self.add_cell("nom_short", "Nom", etud.nom_short, "identite_court")
|
||||||
|
self["_nom_short_order"] = etud.sort_key
|
||||||
|
self["_nom_short_target"] = url_for(
|
||||||
|
"notes.formsemestre_bulletinetud",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre.id,
|
||||||
|
etudid=etud.id,
|
||||||
|
)
|
||||||
|
self["_nom_short_target_attrs"] = f'class="etudinfo" id="{etud.id}"'
|
||||||
|
self["_nom_disp_target"] = self["_nom_short_target"]
|
||||||
|
self["_nom_disp_target_attrs"] = self["_nom_short_target_attrs"]
|
||||||
|
self.last_etud_cell_idx = self.idx
|
||||||
|
|
||||||
|
def add_ue_cells(self, dec_ue: DecisionsProposeesUE):
|
||||||
|
"cell de moyenne d'UE"
|
||||||
|
col_id = f"moy_ue_{dec_ue.ue.id}"
|
||||||
|
note_class = ""
|
||||||
|
val = dec_ue.moy_ue
|
||||||
|
if isinstance(val, float):
|
||||||
|
if val < BUT_BARRE_UE:
|
||||||
|
note_class = " moy_inf"
|
||||||
|
elif val >= BUT_BARRE_UE:
|
||||||
|
note_class = " moy_ue_valid"
|
||||||
|
if val < BUT_BARRE_UE8:
|
||||||
|
note_class = " moy_ue_warning" # notes très basses
|
||||||
|
self.add_cell(
|
||||||
|
col_id,
|
||||||
|
dec_ue.ue.acronyme,
|
||||||
|
self.fmt_note(val),
|
||||||
|
"col_ue" + note_class,
|
||||||
|
column_class="col_ue",
|
||||||
|
)
|
||||||
|
self.add_cell(
|
||||||
|
col_id + "_code",
|
||||||
|
dec_ue.ue.acronyme,
|
||||||
|
dec_ue.code_valide or "",
|
||||||
|
"col_ue_code recorded_code",
|
||||||
|
column_class="col_ue",
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_rcue_cells(self, dec_rcue: DecisionsProposeesRCUE):
|
||||||
|
"2 cells: moyenne du RCUE, code enregistré"
|
||||||
|
rcue = dec_rcue.rcue
|
||||||
|
col_id = f"moy_rcue_{rcue.ue_1.niveau_competence_id}" # le niveau_id
|
||||||
|
note_class = ""
|
||||||
|
val = rcue.moy_rcue
|
||||||
|
if isinstance(val, float):
|
||||||
|
if val < BUT_BARRE_RCUE:
|
||||||
|
note_class = " moy_ue_inf"
|
||||||
|
elif val >= BUT_BARRE_RCUE:
|
||||||
|
note_class = " moy_ue_valid"
|
||||||
|
if val < BUT_RCUE_SUFFISANT:
|
||||||
|
note_class = " moy_ue_warning" # notes très basses
|
||||||
|
self.add_cell(
|
||||||
|
col_id,
|
||||||
|
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
|
||||||
|
self.fmt_note(val),
|
||||||
|
"col_rcue" + note_class,
|
||||||
|
column_class="col_rcue",
|
||||||
|
)
|
||||||
|
self.add_cell(
|
||||||
|
col_id + "_code",
|
||||||
|
f"<div>{rcue.ue_1.acronyme}</div><div>{rcue.ue_2.acronyme}</div>",
|
||||||
|
dec_rcue.code_valide or "",
|
||||||
|
"col_rcue_code recorded_code",
|
||||||
|
column_class="col_rcue",
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_nb_rcues_cell(self, deca: DecisionsProposeesAnnee):
|
||||||
|
"cell avec nb niveaux validables / total"
|
||||||
|
klass = " "
|
||||||
|
if deca.nb_rcues_under_8 > 0:
|
||||||
|
klass += "moy_ue_warning"
|
||||||
|
elif deca.nb_validables < deca.nb_competences:
|
||||||
|
klass += "moy_ue_inf"
|
||||||
|
else:
|
||||||
|
klass += "moy_ue_valid"
|
||||||
|
self.add_cell(
|
||||||
|
"rcues_validables",
|
||||||
|
"RCUEs",
|
||||||
|
f"""{deca.nb_validables}/{deca.nb_competences}"""
|
||||||
|
+ ((" " + scu.EMO_WARNING) if deca.nb_rcues_under_8 > 0 else ""),
|
||||||
|
"col_rcue col_rcues_validables" + klass,
|
||||||
|
)
|
||||||
|
self["_rcues_validables_data"] = {
|
||||||
|
"etudid": deca.etud.id,
|
||||||
|
"nomprenom": deca.etud.nomprenom,
|
||||||
|
}
|
||||||
|
if len(deca.rcues_annee) > 0:
|
||||||
|
# permet un tri par nb de niveaux validables + moyenne gen indicative S_pair
|
||||||
|
if deca.res_pair and deca.etud.id in deca.res_pair.etud_moy_gen:
|
||||||
|
moy = deca.res_pair.etud_moy_gen[deca.etud.id]
|
||||||
|
if np.isnan(moy):
|
||||||
|
moy_gen_d = "x"
|
||||||
|
else:
|
||||||
|
moy_gen_d = f"{int(moy*1000):05}"
|
||||||
|
else:
|
||||||
|
moy_gen_d = "x"
|
||||||
|
self["_rcues_validables_order"] = f"{deca.nb_validables:04d}-{moy_gen_d}"
|
||||||
|
else:
|
||||||
|
# etudiants sans RCUE: pas de semestre impair, ...
|
||||||
|
# les classe à la fin
|
||||||
|
self[
|
||||||
|
"_rcues_validables_order"
|
||||||
|
] = f"{deca.nb_validables:04d}-00000-{deca.etud.sort_key}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_table_jury_but(
|
||||||
|
formsemestre2: FormSemestre, read_only: bool = False, mode="jury"
|
||||||
|
) -> tuple[list[dict], list[str], list[str]]:
|
||||||
|
"""Construit la table des résultats annuels pour le jury BUT"""
|
||||||
|
res2: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre2)
|
||||||
|
titles = {} # column_id : title
|
||||||
|
column_classes = {}
|
||||||
|
rows = []
|
||||||
|
for etudid in formsemestre2.etuds_inscriptions:
|
||||||
|
etud: Identite = Identite.query.get(etudid)
|
||||||
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre2)
|
||||||
|
row = RowCollector(titles=titles, column_classes=column_classes)
|
||||||
|
row.add_etud_cells(etud, formsemestre2)
|
||||||
|
row.idx = 100 # laisse place pour les colonnes de groupes
|
||||||
|
# --- Nombre de niveaux
|
||||||
|
row.add_nb_rcues_cell(deca)
|
||||||
|
# --- Les RCUEs
|
||||||
|
for rcue in deca.rcues_annee:
|
||||||
|
dec_rcue = deca.dec_rcue_by_ue.get(rcue.ue_1.id)
|
||||||
|
if dec_rcue is not None: # None si l'UE n'est pas associée à un niveau
|
||||||
|
row.add_ue_cells(deca.decisions_ues[rcue.ue_1.id])
|
||||||
|
row.add_ue_cells(deca.decisions_ues[rcue.ue_2.id])
|
||||||
|
row.add_rcue_cells(dec_rcue)
|
||||||
|
# --- Le code annuel existant
|
||||||
|
row.add_cell(
|
||||||
|
"code_annee",
|
||||||
|
"Année",
|
||||||
|
f"""{deca.code_valide or ''}""",
|
||||||
|
"col_code_annee",
|
||||||
|
)
|
||||||
|
# --- Le lien de saisie
|
||||||
|
if mode != "recap":
|
||||||
|
row.add_cell(
|
||||||
|
"lien_saisie",
|
||||||
|
"",
|
||||||
|
f"""
|
||||||
|
<a href="{url_for(
|
||||||
|
'notes.formsemestre_validation_but',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
etudid=etud.id,
|
||||||
|
formsemestre_id=formsemestre2.id,
|
||||||
|
)}" class="stdlink">
|
||||||
|
{"voir" if read_only else ("modif." if deca.code_valide else "saisie")}
|
||||||
|
décision</a>
|
||||||
|
"""
|
||||||
|
if deca.inscription_etat == scu.INSCRIT
|
||||||
|
else deca.inscription_etat,
|
||||||
|
"col_lien_saisie_but",
|
||||||
|
)
|
||||||
|
rows.append(row)
|
||||||
|
rows_dict = [row.get_row_dict() for row in rows]
|
||||||
|
if len(rows_dict) > 0:
|
||||||
|
res2.recap_add_partitions(rows_dict, titles, col_idx=row.last_etud_cell_idx + 1)
|
||||||
|
column_ids = [title for title in titles if not title.startswith("_")]
|
||||||
|
column_ids.sort(key=lambda col_id: titles.get("_" + col_id + "_col_order", 1000))
|
||||||
|
rows_dict.sort(key=lambda row: row["_nom_disp_order"])
|
||||||
|
return rows_dict, titles, column_ids
|
34
app/but/jury_but_validation_auto.py
Normal file
34
app/but/jury_but_validation_auto.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Jury BUT: clacul des décisions de jury annuelles "automatiques"
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import g, url_for
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.but import jury_but
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
|
def formsemestre_validation_auto_but(formsemestre: FormSemestre) -> int:
|
||||||
|
"""Calcul automatique des décisions de jury sur une année BUT.
|
||||||
|
Returns: nombre d'étudiants "admis"
|
||||||
|
"""
|
||||||
|
if not formsemestre.formation.is_apc():
|
||||||
|
raise ScoValueError("fonction réservée aux formations BUT")
|
||||||
|
nb_admis = 0
|
||||||
|
for etudid in formsemestre.etuds_inscriptions:
|
||||||
|
etud: Identite = Identite.query.get(etudid)
|
||||||
|
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||||
|
if deca.admis: # année réussie
|
||||||
|
deca.record_all()
|
||||||
|
nb_admis += 1
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return nb_admis
|
173
app/but/jury_but_view.py
Normal file
173
app/but/jury_but_view.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Jury BUT: affichage/formulaire
|
||||||
|
"""
|
||||||
|
from flask import g, url_for
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.but.jury_but import DecisionsProposeesAnnee, DecisionsProposeesUE
|
||||||
|
from app.models import FormSemestre, FormSemestreInscription, UniteEns
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
|
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.code_valide and not read_only:
|
||||||
|
erase_span = f"""<a href="{
|
||||||
|
url_for("notes.formsemestre_jury_but_erase",
|
||||||
|
scodoc_dept=g.scodoc_dept, formsemestre_id=deca.formsemestre_id,
|
||||||
|
etudid=deca.etud.id)}" class="stdlink">effacer décisions</a>"""
|
||||||
|
else:
|
||||||
|
erase_span = ""
|
||||||
|
|
||||||
|
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>({'non ' if deca.code_valide is None else ''}enregistrée)</span>
|
||||||
|
<span>{erase_span}</span>
|
||||||
|
</div>
|
||||||
|
<div class="but_explanation">{deca.explanation}</div>
|
||||||
|
</div>"""
|
||||||
|
)
|
||||||
|
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div><b>Niveaux de compétences et unités d'enseignement :</b></div>
|
||||||
|
<div class="but_annee">
|
||||||
|
<div class="titre"></div>
|
||||||
|
<div class="titre">S{1}</div>
|
||||||
|
<div class="titre">S{2}</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)
|
||||||
|
if dec_rcue is None:
|
||||||
|
break
|
||||||
|
# Semestre impair
|
||||||
|
H.append(
|
||||||
|
_gen_but_niveau_ue(
|
||||||
|
dec_rcue.rcue.ue_1,
|
||||||
|
dec_rcue.rcue.moy_ue_1,
|
||||||
|
deca.decisions_ues[dec_rcue.rcue.ue_1.id],
|
||||||
|
disabled=read_only,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Semestre pair
|
||||||
|
H.append(
|
||||||
|
_gen_but_niveau_ue(
|
||||||
|
dec_rcue.rcue.ue_2,
|
||||||
|
dec_rcue.rcue.moy_ue_2,
|
||||||
|
deca.decisions_ues[dec_rcue.rcue.ue_2.id],
|
||||||
|
disabled=read_only,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# RCUE
|
||||||
|
H.append(
|
||||||
|
f"""<div class="but_niveau_rcue
|
||||||
|
{'recorded' if dec_rcue.code_valide is not None else ''}
|
||||||
|
">
|
||||||
|
<div class="but_note">{scu.fmt_note(dec_rcue.rcue.moy_rcue)}</div>
|
||||||
|
<div class="but_code">{
|
||||||
|
_gen_but_select("code_rcue_"+str(niveau.id),
|
||||||
|
dec_rcue.codes,
|
||||||
|
dec_rcue.code_valide,
|
||||||
|
disabled=True, klass="manual"
|
||||||
|
)
|
||||||
|
}</div>
|
||||||
|
</div>"""
|
||||||
|
)
|
||||||
|
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 = "",
|
||||||
|
) -> str:
|
||||||
|
"Le menu html select avec les codes"
|
||||||
|
h = "\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}"
|
||||||
|
onchange="change_menu_code(this);"
|
||||||
|
{"disabled" if disabled else ""}
|
||||||
|
>{h}</select>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _gen_but_niveau_ue(
|
||||||
|
ue: UniteEns, moy_ue: float, dec_ue: DecisionsProposeesUE, disabled=False
|
||||||
|
):
|
||||||
|
return f"""<div class="but_niveau_ue {
|
||||||
|
'recorded' if dec_ue.code_valide is not None else ''}
|
||||||
|
">
|
||||||
|
<div title="{ue.titre}">{ue.acronyme}</div>
|
||||||
|
<div class="but_note">{scu.fmt_note(moy_ue)}</div>
|
||||||
|
<div class="but_code">{
|
||||||
|
_gen_but_select("code_ue_"+str(ue.id),
|
||||||
|
dec_ue.codes,
|
||||||
|
dec_ue.code_valide, disabled=disabled
|
||||||
|
)
|
||||||
|
}</div>
|
||||||
|
</div>"""
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
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])
|
||||||
|
if len(deca.rcues_annee) > 0:
|
||||||
|
return f"""<div class="infos_but">
|
||||||
|
{show_etud(deca, read_only=True)}
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
except ScoValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ""
|
@ -767,6 +767,21 @@ class BonusStMalo(BonusIUTRennes1):
|
|||||||
__doc__ = BonusIUTRennes1.__doc__
|
__doc__ = BonusIUTRennes1.__doc__
|
||||||
|
|
||||||
|
|
||||||
|
class BonusLaRocheSurYon(BonusSportAdditif):
|
||||||
|
"""Bonus IUT de La Roche-sur-Yon
|
||||||
|
|
||||||
|
Si une note de bonus est saisie, l'étudiant est gratifié de 0,2 points
|
||||||
|
sur sa moyenne générale ou, en BUT, sur la moyenne de chaque UE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_larochesuryon"
|
||||||
|
displayed_name = "IUT de La Roche-sur-Yon"
|
||||||
|
seuil_moy_gen = 0.0
|
||||||
|
seuil_comptage = 0.0
|
||||||
|
proportion_point = 1e10 # le moindre point sature le bonus
|
||||||
|
bonus_max = 0.2 # à 0.2
|
||||||
|
|
||||||
|
|
||||||
class BonusLaRochelle(BonusSportAdditif):
|
class BonusLaRochelle(BonusSportAdditif):
|
||||||
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT de La Rochelle.
|
||||||
|
|
||||||
@ -1023,6 +1038,54 @@ class BonusNantes(BonusSportAdditif):
|
|||||||
bonus_max = 0.5 # plafonnement à 0.5 points
|
bonus_max = 0.5 # plafonnement à 0.5 points
|
||||||
|
|
||||||
|
|
||||||
|
class BonusOrleans(BonusSportAdditif):
|
||||||
|
"""Calcul bonus modules optionnels (sport, culture), règle IUT d'Orléans
|
||||||
|
<p><b>Cadre général :</b>
|
||||||
|
En reconnaissance de l'engagement des étudiants dans la vie associative,
|
||||||
|
sociale ou professionnelle, l’IUT d’Orléans accorde, sous conditions,
|
||||||
|
une bonification aux étudiants inscrits qui en font la demande en début
|
||||||
|
d’année universitaire.
|
||||||
|
</p>
|
||||||
|
<p>Cet engagement doit être régulier et correspondre à une activité réelle
|
||||||
|
et sérieuse qui bénéficie à toute la communauté étudiante de l’IUT,
|
||||||
|
de l’Université ou à l’ensemble de la collectivité.</p>
|
||||||
|
<p><b>Bonification :</b>
|
||||||
|
Pour les DUT et LP, cette bonification interviendra sur la moyenne générale
|
||||||
|
des semestres pairs :
|
||||||
|
<ul><li> du 2ème semestre pour les étudiants de 1ère année de DUT</li>
|
||||||
|
<li> du 4ème semestre pour les étudiants de 2nde année de DUT</li>
|
||||||
|
<li> du 6ème semestre pour les étudiants en LP</li>
|
||||||
|
</ul>
|
||||||
|
Pour le BUT, cette bonification interviendra sur la moyenne de chacune
|
||||||
|
des UE des semestre pairs :
|
||||||
|
<ul><li> du 2ème semestre pour les étudiants de 1ère année de BUT</li>
|
||||||
|
<li> du 4ème semestre pour les étudiants de 2ème année de BUT</li>
|
||||||
|
<li> du 6ème semestre pour les étudiants de 3ème année de BUT</li>
|
||||||
|
</ul>
|
||||||
|
<em>La bonification ne peut dépasser +0,5 points par année universitaire.</em>
|
||||||
|
</p>
|
||||||
|
<p><b> Avant février 2020 :</b>
|
||||||
|
Un bonus de 2,5% de la note de sport est accordé à la moyenne générale.
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "bonus_iutorleans"
|
||||||
|
displayed_name = "IUT d'Orléans"
|
||||||
|
bonus_max = 0.5
|
||||||
|
seuil_moy_gen = 0.0 # seuls les points au dessus du seuil sont comptés
|
||||||
|
proportion_point = 1
|
||||||
|
classic_use_bonus_ues = False
|
||||||
|
|
||||||
|
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
|
||||||
|
if self.formsemestre.date_debut > datetime.date(2020, 2, 1):
|
||||||
|
self.proportion_point = 1.0
|
||||||
|
else:
|
||||||
|
self.proportion_point = 2.5 / 100.0
|
||||||
|
return super().compute_bonus(
|
||||||
|
sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BonusPoitiers(BonusSportAdditif):
|
class BonusPoitiers(BonusSportAdditif):
|
||||||
"""Calcul bonus optionnels (sport, culture), règle IUT de Poitiers.
|
"""Calcul bonus optionnels (sport, culture), règle IUT de Poitiers.
|
||||||
|
|
||||||
|
@ -161,8 +161,11 @@ class ModuleImplResults:
|
|||||||
evals_notes = evals_notes.merge(
|
evals_notes = evals_notes.merge(
|
||||||
eval_df, how="left", left_index=True, right_index=True
|
eval_df, how="left", left_index=True, right_index=True
|
||||||
)
|
)
|
||||||
# Notes en attente: (on prend dans evals_notes pour ne pas avoir les dem.)
|
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||||
nb_att = sum(evals_notes[str(evaluation.id)] == scu.NOTES_ATTENTE)
|
nb_att = sum(
|
||||||
|
evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
|
== scu.NOTES_ATTENTE
|
||||||
|
)
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
|
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
|
||||||
)
|
)
|
||||||
|
@ -496,17 +496,26 @@ def compute_malus(
|
|||||||
"""
|
"""
|
||||||
ues_idx = [ue.id for ue in ues]
|
ues_idx = [ue.id for ue in ues]
|
||||||
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
|
malus = pd.DataFrame(index=modimpl_inscr_df.index, columns=ues_idx, dtype=float)
|
||||||
|
if len(sem_modimpl_moys.flat) == 0: # vide
|
||||||
|
return malus
|
||||||
|
if len(sem_modimpl_moys.shape) > 2:
|
||||||
|
# BUT: ne retient que la 1er composante du malus qui est scalaire
|
||||||
|
# au sens ou chaque note de malus n'affecte que la moyenne de l'UE
|
||||||
|
# de rattachement de son module.
|
||||||
|
sem_modimpl_moys_scalar = sem_modimpl_moys[:, :, 0]
|
||||||
|
else: # classic
|
||||||
|
sem_modimpl_moys_scalar = sem_modimpl_moys
|
||||||
for ue in ues:
|
for ue in ues:
|
||||||
if ue.type != UE_SPORT:
|
if ue.type != UE_SPORT:
|
||||||
modimpl_mask = np.array(
|
modimpl_mask = np.array(
|
||||||
[
|
[
|
||||||
(m.module.module_type == ModuleType.MALUS)
|
(m.module.module_type == ModuleType.MALUS)
|
||||||
and (m.module.ue.id == ue.id)
|
and (m.module.ue.id == ue.id) # UE de rattachement
|
||||||
for m in formsemestre.modimpls_sorted
|
for m in formsemestre.modimpls_sorted
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
if len(modimpl_mask):
|
if len(modimpl_mask):
|
||||||
malus_moys = sem_modimpl_moys[:, modimpl_mask].sum(axis=1)
|
malus_moys = sem_modimpl_moys_scalar[:, modimpl_mask].sum(axis=1)
|
||||||
malus[ue.id] = malus_moys
|
malus[ue.id] = malus_moys
|
||||||
|
|
||||||
malus.fillna(0.0, inplace=True)
|
malus.fillna(0.0, inplace=True)
|
||||||
|
@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
"""Résultats semestres BUT
|
"""Résultats semestres BUT
|
||||||
"""
|
"""
|
||||||
|
from collections.abc import Generator
|
||||||
|
from re import U
|
||||||
import time
|
import time
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -28,6 +30,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
"modimpl_coefs_df",
|
"modimpl_coefs_df",
|
||||||
"modimpls_evals_poids",
|
"modimpls_evals_poids",
|
||||||
"sem_cube",
|
"sem_cube",
|
||||||
|
"etuds_parcour_id", # parcours de chaque étudiant
|
||||||
|
"ues_inscr_parcours_df", # inscriptions aux UE / parcours
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, formsemestre):
|
def __init__(self, formsemestre):
|
||||||
@ -35,7 +39,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
|
|
||||||
self.sem_cube = None
|
self.sem_cube = None
|
||||||
"""ndarray (etuds x modimpl x ue)"""
|
"""ndarray (etuds x modimpl x ue)"""
|
||||||
|
self.etuds_parcour_id = None
|
||||||
|
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||||
if not self.load_cached():
|
if not self.load_cached():
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
self.compute()
|
self.compute()
|
||||||
@ -55,6 +60,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
self.modimpls_results,
|
self.modimpls_results,
|
||||||
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
) = moy_ue.notes_sem_load_cube(self.formsemestre)
|
||||||
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
self.modimpl_inscr_df = inscr_mod.df_load_modimpl_inscr(self.formsemestre)
|
||||||
|
self.ues_inscr_parcours_df = self.load_ues_inscr_parcours()
|
||||||
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
self.modimpl_coefs_df, _, _ = moy_ue.df_load_modimpl_coefs(
|
||||||
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
self.formsemestre, modimpls=self.formsemestre.modimpls_sorted
|
||||||
)
|
)
|
||||||
@ -108,6 +114,9 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
# Clippe toutes les moyennes d'UE dans [0,20]
|
# Clippe toutes les moyennes d'UE dans [0,20]
|
||||||
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
self.etud_moy_ue.clip(lower=0.0, upper=20.0, inplace=True)
|
||||||
|
|
||||||
|
# Nanifie les moyennes d'UE hors parcours pour chaque étudiant
|
||||||
|
self.etud_moy_ue *= self.ues_inscr_parcours_df
|
||||||
|
|
||||||
# Moyenne générale indicative:
|
# Moyenne générale indicative:
|
||||||
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
|
||||||
# donc la moyenne indicative)
|
# donc la moyenne indicative)
|
||||||
@ -149,16 +158,24 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
"""
|
"""
|
||||||
return self.modimpl_coefs_df.loc[ue.id].sum()
|
return self.modimpl_coefs_df.loc[ue.id].sum()
|
||||||
|
|
||||||
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]:
|
def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
|
||||||
"""Liste des modimpl ayant des coefs non nuls vers cette UE
|
"""Liste des modimpl ayant des coefs non nuls vers cette UE
|
||||||
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
|
et auxquels l'étudiant est inscrit. Inclus modules bonus le cas échéant.
|
||||||
"""
|
"""
|
||||||
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
|
# sert pour l'affichage ou non de l'UE sur le bulletin et la table recap
|
||||||
coefs = self.modimpl_coefs_df # row UE, cols modimpl
|
if ue.type == UE_SPORT:
|
||||||
|
return [
|
||||||
|
modimpl
|
||||||
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
|
if modimpl.module.ue.id == ue.id
|
||||||
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||||
|
]
|
||||||
|
coefs = self.modimpl_coefs_df # row UE (sans bonus), cols modimpl
|
||||||
modimpls = [
|
modimpls = [
|
||||||
modimpl
|
modimpl
|
||||||
for modimpl in self.formsemestre.modimpls_sorted
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
if (coefs[modimpl.id][ue_id] != 0)
|
if modimpl.module.ue.type != UE_SPORT
|
||||||
|
and (coefs[modimpl.id][ue.id] != 0)
|
||||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||||
]
|
]
|
||||||
if not with_bonus:
|
if not with_bonus:
|
||||||
@ -175,3 +192,50 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||||||
i = self.modimpl_coefs_df.columns.get_loc(modimpl_id)
|
i = self.modimpl_coefs_df.columns.get_loc(modimpl_id)
|
||||||
j = self.modimpl_coefs_df.index.get_loc(ue_id)
|
j = self.modimpl_coefs_df.index.get_loc(ue_id)
|
||||||
return self.sem_cube[:, i, j]
|
return self.sem_cube[:, i, j]
|
||||||
|
|
||||||
|
def load_ues_inscr_parcours(self) -> pd.DataFrame:
|
||||||
|
"""Chargement des inscriptions aux parcours et calcul de la
|
||||||
|
matrice d'inscriptions (etuds, ue).
|
||||||
|
S'il n'y pas de référentiel de compétence, donc pas de parcours,
|
||||||
|
on considère l'étudiant inscrit à toutes les ue.
|
||||||
|
La matrice avec ue ne comprend que les UE non bonus.
|
||||||
|
1.0 si étudiant inscrit à l'UE, NaN sinon.
|
||||||
|
"""
|
||||||
|
etuds_parcour_id = {
|
||||||
|
inscr.etudid: inscr.parcour_id for inscr in self.formsemestre.inscriptions
|
||||||
|
}
|
||||||
|
self.etuds_parcour_id = etuds_parcour_id
|
||||||
|
ue_ids = [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||||
|
# matrice de 1, inscrits par défaut à toutes les UE:
|
||||||
|
ues_inscr_parcours_df = pd.DataFrame(
|
||||||
|
1.0, index=etuds_parcour_id.keys(), columns=ue_ids, dtype=float
|
||||||
|
)
|
||||||
|
if self.formsemestre.formation.referentiel_competence is None:
|
||||||
|
return ues_inscr_parcours_df
|
||||||
|
|
||||||
|
ue_by_parcours = {} # parcours_id : {ue_id:0|1}
|
||||||
|
for parcour in self.formsemestre.formation.referentiel_competence.parcours:
|
||||||
|
ue_by_parcours[parcour.id] = {
|
||||||
|
ue.id: 1.0
|
||||||
|
for ue in self.formsemestre.formation.query_ues_parcour(
|
||||||
|
parcour
|
||||||
|
).filter_by(semestre_idx=self.formsemestre.semestre_id)
|
||||||
|
}
|
||||||
|
for etudid in etuds_parcour_id:
|
||||||
|
parcour = etuds_parcour_id[etudid]
|
||||||
|
if parcour is not None:
|
||||||
|
ues_inscr_parcours_df.loc[etudid] = ue_by_parcours[
|
||||||
|
etuds_parcour_id[etudid]
|
||||||
|
]
|
||||||
|
return ues_inscr_parcours_df
|
||||||
|
|
||||||
|
def etud_ues_ids(self, etudid: int) -> list[int]:
|
||||||
|
"""Liste des id d'UE auxquelles l'étudiant est inscrit (sans bonus).
|
||||||
|
(surchargée ici pour prendre en compte les parcours)
|
||||||
|
"""
|
||||||
|
s = self.ues_inscr_parcours_df.loc[etudid]
|
||||||
|
return s.index[s.notna()]
|
||||||
|
|
||||||
|
def etud_ues(self, etudid: int) -> Generator[UniteEns]:
|
||||||
|
"""Liste des UE auxquelles l'étudiant est inscrit."""
|
||||||
|
return (UniteEns.query.get(ue_id) for ue_id in self.etud_ues_ids(etudid))
|
||||||
|
@ -112,6 +112,14 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"dict { etudid : indice dans les inscrits }"
|
"dict { etudid : indice dans les inscrits }"
|
||||||
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
return {e.id: idx for idx, e in enumerate(self.etuds)}
|
||||||
|
|
||||||
|
def etud_ues_ids(self, etudid: int) -> list[int]:
|
||||||
|
"""Liste des UE auxquelles l'etudiant est inscrit, sans bonus
|
||||||
|
(surchargée en BUT pour prendre en compte les parcours)
|
||||||
|
"""
|
||||||
|
# Pour les formations classiques, etudid n'est pas utilisé
|
||||||
|
# car tous les étudiants sont inscrits à toutes les UE
|
||||||
|
return [ue.id for ue in self.ues if ue.type != UE_SPORT]
|
||||||
|
|
||||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||||
Utile pour stats bottom tableau recap.
|
Utile pour stats bottom tableau recap.
|
||||||
@ -179,7 +187,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
ues = sorted(list(ues), key=lambda x: x.numero or 0)
|
ues = sorted(list(ues), key=lambda x: x.numero or 0)
|
||||||
return ues
|
return ues
|
||||||
|
|
||||||
def modimpls_in_ue(self, ue_id, etudid, with_bonus=True) -> list[ModuleImpl]:
|
def modimpls_in_ue(self, ue: UniteEns, etudid, with_bonus=True) -> list[ModuleImpl]:
|
||||||
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit.
|
"""Liste des modimpl de cette UE auxquels l'étudiant est inscrit.
|
||||||
Utile en formations classiques, surchargée pour le BUT.
|
Utile en formations classiques, surchargée pour le BUT.
|
||||||
Inclus modules bonus le cas échéant.
|
Inclus modules bonus le cas échéant.
|
||||||
@ -189,7 +197,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
modimpls = [
|
modimpls = [
|
||||||
modimpl
|
modimpl
|
||||||
for modimpl in self.formsemestre.modimpls_sorted
|
for modimpl in self.formsemestre.modimpls_sorted
|
||||||
if modimpl.module.ue.id == ue_id
|
if modimpl.module.ue.id == ue.id
|
||||||
and self.modimpl_inscr_df[modimpl.id][etudid]
|
and self.modimpl_inscr_df[modimpl.id][etudid]
|
||||||
]
|
]
|
||||||
if not with_bonus:
|
if not with_bonus:
|
||||||
@ -391,7 +399,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
# --- TABLEAU RECAP
|
# --- TABLEAU RECAP
|
||||||
|
|
||||||
def get_table_recap(
|
def get_table_recap(
|
||||||
self, convert_values=False, include_evaluations=False, modejury=False
|
self, convert_values=False, include_evaluations=False, mode_jury=False
|
||||||
):
|
):
|
||||||
"""Result: tuple avec
|
"""Result: tuple avec
|
||||||
- rows: liste de dicts { column_id : value }
|
- rows: liste de dicts { column_id : value }
|
||||||
@ -542,7 +550,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
titles_bot[
|
titles_bot[
|
||||||
f"_{col_id}_target_attrs"
|
f"_{col_id}_target_attrs"
|
||||||
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
] = f"""title="{ue.titre} S{ue.semestre_idx or '?'}" """
|
||||||
if modejury:
|
if mode_jury:
|
||||||
# pas d'autre colonnes de résultats
|
# pas d'autre colonnes de résultats
|
||||||
continue
|
continue
|
||||||
# Bonus (sport) dans cette UE ?
|
# Bonus (sport) dans cette UE ?
|
||||||
@ -564,7 +572,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
# Les moyennes des modules (ou ressources et SAÉs) dans cette UE
|
||||||
idx_malus = idx # place pour colonne malus à gauche des modules
|
idx_malus = idx # place pour colonne malus à gauche des modules
|
||||||
idx += 1
|
idx += 1
|
||||||
for modimpl in self.modimpls_in_ue(ue.id, etudid, with_bonus=False):
|
for modimpl in self.modimpls_in_ue(ue, etudid, with_bonus=False):
|
||||||
if ue_status["is_capitalized"]:
|
if ue_status["is_capitalized"]:
|
||||||
val = "-c-"
|
val = "-c-"
|
||||||
else:
|
else:
|
||||||
@ -622,9 +630,10 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
f"_{col_id}_target_attrs"
|
f"_{col_id}_target_attrs"
|
||||||
] = f""" title="{modimpl.module.titre} ({nom_resp})" """
|
] = f""" title="{modimpl.module.titre} ({nom_resp})" """
|
||||||
modimpl_ids.add(modimpl.id)
|
modimpl_ids.add(modimpl.id)
|
||||||
|
nb_ues_etud_parcours = len(self.etud_ues_ids(etudid))
|
||||||
ue_valid_txt = (
|
ue_valid_txt = (
|
||||||
ue_valid_txt_html
|
ue_valid_txt_html
|
||||||
) = f"{nb_ues_validables}/{len(ues_sans_bonus)}"
|
) = f"{nb_ues_validables}/{nb_ues_etud_parcours}"
|
||||||
if nb_ues_warning:
|
if nb_ues_warning:
|
||||||
ue_valid_txt_html += " " + scu.EMO_WARNING
|
ue_valid_txt_html += " " + scu.EMO_WARNING
|
||||||
add_cell(
|
add_cell(
|
||||||
@ -641,7 +650,17 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
elif nb_ues_validables < len(ues_sans_bonus):
|
elif nb_ues_validables < len(ues_sans_bonus):
|
||||||
row["_ues_validables_class"] += " moy_inf"
|
row["_ues_validables_class"] += " moy_inf"
|
||||||
row["_ues_validables_order"] = nb_ues_validables # pour tri
|
row["_ues_validables_order"] = nb_ues_validables # pour tri
|
||||||
if modejury:
|
if mode_jury:
|
||||||
|
dec_sem = self.validations.decisions_jury.get(etudid)
|
||||||
|
jury_code_sem = dec_sem["code"] if dec_sem else ""
|
||||||
|
idx = add_cell(
|
||||||
|
row,
|
||||||
|
"jury_code_sem",
|
||||||
|
"Jury",
|
||||||
|
jury_code_sem,
|
||||||
|
"jury_code_sem",
|
||||||
|
1000,
|
||||||
|
)
|
||||||
idx = add_cell(
|
idx = add_cell(
|
||||||
row,
|
row,
|
||||||
"jury_link",
|
"jury_link",
|
||||||
@ -651,11 +670,11 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
)
|
)
|
||||||
}">saisir décision</a>""",
|
}">saisir décision</a>""",
|
||||||
"col_jury_link",
|
"col_jury_link",
|
||||||
1000,
|
idx,
|
||||||
)
|
)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
|
|
||||||
self._recap_add_partitions(rows, titles)
|
self.recap_add_partitions(rows, titles)
|
||||||
self._recap_add_admissions(rows, titles)
|
self._recap_add_admissions(rows, titles)
|
||||||
|
|
||||||
# tri par rang croissant
|
# tri par rang croissant
|
||||||
@ -762,7 +781,9 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
"apo": row_apo,
|
"apo": row_apo,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _recap_etud_groups_infos(self, etudid: int, row: dict, titles: dict):
|
def _recap_etud_groups_infos(
|
||||||
|
self, etudid: int, row: dict, titles: dict
|
||||||
|
): # XXX non utilisé
|
||||||
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
|
"""Table recap: ajoute à row les colonnes sur les groupes pour cet etud"""
|
||||||
# dec = self.get_etud_decision_sem(etudid)
|
# dec = self.get_etud_decision_sem(etudid)
|
||||||
# if dec:
|
# if dec:
|
||||||
@ -818,7 +839,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
else:
|
else:
|
||||||
row[f"_{cid}_class"] = "admission"
|
row[f"_{cid}_class"] = "admission"
|
||||||
|
|
||||||
def _recap_add_partitions(self, rows: list[dict], titles: dict):
|
def recap_add_partitions(self, rows: list[dict], titles: dict, col_idx: int = None):
|
||||||
"""Ajoute les colonnes indiquant les groupes
|
"""Ajoute les colonnes indiquant les groupes
|
||||||
rows est une liste de dict avec une clé "etudid"
|
rows est une liste de dict avec une clé "etudid"
|
||||||
Les colonnes ont la classe css "partition"
|
Les colonnes ont la classe css "partition"
|
||||||
@ -827,7 +848,7 @@ class ResultatsSemestre(ResultatsCache):
|
|||||||
self.formsemestre.id
|
self.formsemestre.id
|
||||||
)
|
)
|
||||||
first_partition = True
|
first_partition = True
|
||||||
col_order = 10
|
col_order = 10 if col_idx is None else col_idx
|
||||||
for partition in partitions:
|
for partition in partitions:
|
||||||
cid = f"part_{partition['partition_id']}"
|
cid = f"part_{partition['partition_id']}"
|
||||||
rg_cid = cid + "_rg" # rang dans la partition
|
rg_cid = cid + "_rg" # rang dans la partition
|
||||||
|
@ -54,6 +54,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
self.ue_rangs_by_group = {} # { ue_id : {group_id : (Series, Series)}}
|
||||||
self.expr_diagnostics = ""
|
self.expr_diagnostics = ""
|
||||||
self.parcours = self.formsemestre.formation.get_parcours()
|
self.parcours = self.formsemestre.formation.get_parcours()
|
||||||
|
self._modimpls_dict_by_ue = {} # local cache
|
||||||
|
|
||||||
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
def get_inscrits(self, include_demdef=True, order_by=False) -> list[Identite]:
|
||||||
"""Liste des étudiants inscrits
|
"""Liste des étudiants inscrits
|
||||||
@ -145,6 +146,10 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
"""Liste des modules pour une UE (ou toutes si ue_id==None),
|
||||||
triés par numéros (selon le type de formation)
|
triés par numéros (selon le type de formation)
|
||||||
"""
|
"""
|
||||||
|
# cached ?
|
||||||
|
modimpls_dict = self._modimpls_dict_by_ue.get(ue_id)
|
||||||
|
if modimpls_dict:
|
||||||
|
return modimpls_dict
|
||||||
modimpls_dict = []
|
modimpls_dict = []
|
||||||
for modimpl in self.formsemestre.modimpls_sorted:
|
for modimpl in self.formsemestre.modimpls_sorted:
|
||||||
if (ue_id is None) or (modimpl.module.ue.id == ue_id):
|
if (ue_id is None) or (modimpl.module.ue.id == ue_id):
|
||||||
@ -152,6 +157,7 @@ class NotesTableCompat(ResultatsSemestre):
|
|||||||
# compat ScoDoc < 9.2: ajoute matières
|
# compat ScoDoc < 9.2: ajoute matières
|
||||||
d["mat"] = modimpl.module.matiere.to_dict()
|
d["mat"] = modimpl.module.matiere.to_dict()
|
||||||
modimpls_dict.append(d)
|
modimpls_dict.append(d)
|
||||||
|
self._modimpls_dict_by_ue[ue_id] = modimpls_dict
|
||||||
return modimpls_dict
|
return modimpls_dict
|
||||||
|
|
||||||
def compute_rangs(self):
|
def compute_rangs(self):
|
||||||
|
@ -87,10 +87,10 @@ def permission_required(permission):
|
|||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||||
if not current_user.has_permission(permission, scodoc_dept):
|
if not current_user.has_permission(permission, scodoc_dept):
|
||||||
abort(403)
|
return current_app.login_manager.unauthorized()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return login_required(decorated_function)
|
return decorated_function
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ from app.scodoc import sco_codes_parcours
|
|||||||
def _build_code_field(code):
|
def _build_code_field(code):
|
||||||
return StringField(
|
return StringField(
|
||||||
label=code,
|
label=code,
|
||||||
|
default=code,
|
||||||
description=sco_codes_parcours.CODES_EXPL[code],
|
description=sco_codes_parcours.CODES_EXPL[code],
|
||||||
validators=[
|
validators=[
|
||||||
validators.regexp(
|
validators.regexp(
|
||||||
@ -58,6 +59,8 @@ def _build_code_field(code):
|
|||||||
|
|
||||||
class CodesDecisionsForm(FlaskForm):
|
class CodesDecisionsForm(FlaskForm):
|
||||||
"Formulaire code décisions Apogée"
|
"Formulaire code décisions Apogée"
|
||||||
|
ABAN = _build_code_field("ABAN")
|
||||||
|
ABL = _build_code_field("ABL")
|
||||||
ADC = _build_code_field("ADC")
|
ADC = _build_code_field("ADC")
|
||||||
ADJ = _build_code_field("ADJ")
|
ADJ = _build_code_field("ADJ")
|
||||||
ADM = _build_code_field("ADM")
|
ADM = _build_code_field("ADM")
|
||||||
@ -68,8 +71,13 @@ class CodesDecisionsForm(FlaskForm):
|
|||||||
CMP = _build_code_field("CMP")
|
CMP = _build_code_field("CMP")
|
||||||
DEF = _build_code_field("DEF")
|
DEF = _build_code_field("DEF")
|
||||||
DEM = _build_code_field("DEM")
|
DEM = _build_code_field("DEM")
|
||||||
|
EXCLU = _build_code_field("EXCLU")
|
||||||
NAR = _build_code_field("NAR")
|
NAR = _build_code_field("NAR")
|
||||||
|
PASD = _build_code_field("PASD")
|
||||||
|
PAS1NCI = _build_code_field("PAS1NCI")
|
||||||
RAT = _build_code_field("RAT")
|
RAT = _build_code_field("RAT")
|
||||||
|
RED = _build_code_field("RED")
|
||||||
|
|
||||||
NOTES_FMT = StringField(
|
NOTES_FMT = StringField(
|
||||||
label="Format notes exportées",
|
label="Format notes exportées",
|
||||||
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
description="""Format des notes. Par défaut <tt style="font-family: monotype;">%3.2f</tt> (deux chiffres après la virgule)""",
|
||||||
|
@ -43,7 +43,7 @@ from app.scodoc import sco_logos, html_sco_header
|
|||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
from app.scodoc.sco_config_actions import LogoInsert
|
from app.scodoc.sco_config_actions import LogoInsert
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
from app.scodoc.sco_logos import find_logo
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
|
||||||
|
|
||||||
@ -108,6 +108,8 @@ def dept_key_to_id(dept_key):
|
|||||||
def logo_name_validator(message=None):
|
def logo_name_validator(message=None):
|
||||||
def validate_logo_name(form, field):
|
def validate_logo_name(form, field):
|
||||||
name = field.data if field.data else ""
|
name = field.data if field.data else ""
|
||||||
|
if "." in name:
|
||||||
|
raise ValidationError(message)
|
||||||
if not scu.is_valid_filename(name):
|
if not scu.is_valid_filename(name):
|
||||||
raise ValidationError(message)
|
raise ValidationError(message)
|
||||||
|
|
||||||
@ -199,9 +201,12 @@ class LogoForm(FlaskForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs["meta"] = {"csrf": False}
|
kwargs["meta"] = {"csrf": False}
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.logo = find_logo(
|
logo = find_logo(
|
||||||
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
|
logoname=self.logo_id.data, dept_id=dept_key_to_id(self.dept_key.data)
|
||||||
).select()
|
)
|
||||||
|
if logo is None:
|
||||||
|
raise ScoValueError("logo introuvable")
|
||||||
|
self.logo = logo.select()
|
||||||
self.description = None
|
self.description = None
|
||||||
self.titre = None
|
self.titre = None
|
||||||
self.can_delete = True
|
self.can_delete = True
|
||||||
|
@ -1,14 +1,25 @@
|
|||||||
# -*- coding: UTF-8 -*
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
"""Modèles base de données ScoDoc
|
"""Modèles base de données ScoDoc
|
||||||
XXX version préliminaire ScoDoc8 #sco8 sans département
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
CODE_STR_LEN = 16 # chaine pour les codes
|
CODE_STR_LEN = 16 # chaine pour les codes
|
||||||
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
||||||
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
|
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
|
||||||
GROUPNAME_STR_LEN = 64
|
GROUPNAME_STR_LEN = 64
|
||||||
|
|
||||||
|
convention = {
|
||||||
|
"ix": "ix_%(column_0_label)s",
|
||||||
|
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||||
|
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
||||||
|
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||||
|
"pk": "pk_%(table_name)s",
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata_obj = sqlalchemy.MetaData(naming_convention=convention)
|
||||||
|
|
||||||
from app.models.raw_sql_init import create_database_functions
|
from app.models.raw_sql_init import create_database_functions
|
||||||
|
|
||||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||||
@ -65,5 +76,8 @@ from app.models.but_refcomp import (
|
|||||||
ApcCompetence,
|
ApcCompetence,
|
||||||
ApcSituationPro,
|
ApcSituationPro,
|
||||||
ApcAppCritique,
|
ApcAppCritique,
|
||||||
|
ApcParcours,
|
||||||
)
|
)
|
||||||
|
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||||
|
|
||||||
from app.models.config import ScoDocSiteConfig
|
from app.models.config import ScoDocSiteConfig
|
||||||
|
@ -11,7 +11,9 @@ class Absence(db.Model):
|
|||||||
|
|
||||||
__tablename__ = "absences"
|
__tablename__ = "absences"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
etudid = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
|
||||||
|
)
|
||||||
jour = db.Column(db.Date)
|
jour = db.Column(db.Date)
|
||||||
estabs = db.Column(db.Boolean())
|
estabs = db.Column(db.Boolean())
|
||||||
estjust = db.Column(db.Boolean())
|
estjust = db.Column(db.Boolean())
|
||||||
@ -59,7 +61,7 @@ class AbsenceNotification(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
notification_date = db.Column(
|
notification_date = db.Column(
|
||||||
db.DateTime(timezone=True), server_default=db.func.now()
|
db.DateTime(timezone=True), server_default=db.func.now()
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
"""ScoDoc 9 models : Formation BUT 2021
|
"""ScoDoc 9 models : Formation BUT 2021
|
||||||
|
XXX inutilisé
|
||||||
"""
|
"""
|
||||||
from enum import unique
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
|
||||||
|
|
||||||
|
|
||||||
class APCFormation(db.Model):
|
class APCFormation(db.Model):
|
||||||
"""Formation par compétence"""
|
"""Formation par compétence"""
|
||||||
|
@ -7,12 +7,14 @@
|
|||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import flask_sqlalchemy
|
||||||
from sqlalchemy.orm import class_mapper
|
from sqlalchemy.orm import class_mapper
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
|
|
||||||
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
# from https://stackoverflow.com/questions/2537471/method-of-iterating-over-sqlalchemy-models-defined-columns
|
||||||
@ -116,7 +118,7 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
# self.formations = formations
|
# self.formations = formations
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcReferentielCompetences {self.id} {self.specialite}>"
|
return f"<ApcReferentielCompetences {self.id} {self.specialite!r}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""Représentation complète du ref. de comp.
|
"""Représentation complète du ref. de comp.
|
||||||
@ -139,6 +141,52 @@ class ApcReferentielCompetences(db.Model, XMLModel):
|
|||||||
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
"parcours": {x.code: x.to_dict() for x in self.parcours},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_niveaux_by_parcours(self, annee) -> dict:
|
||||||
|
"""
|
||||||
|
Construit la liste des niveaux de compétences pour chaque parcours
|
||||||
|
de ce référentiel.
|
||||||
|
Les niveaux sont groupés par parcours, en isolant les niveaux de tronc commun.
|
||||||
|
Le tronc commun n'est pas identifié comme tel dans les référentiels Orébut:
|
||||||
|
on cherche les niveaux qui sont présents dans tous les parcours et les range sous
|
||||||
|
la clé "TC" (toujours présente mais éventuellement liste vide si pas de tronc commun).
|
||||||
|
|
||||||
|
résultat:
|
||||||
|
{
|
||||||
|
"TC" : [ ApcNiveau ],
|
||||||
|
parcour.id : [ ApcNiveau ]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
parcours = self.parcours.order_by(ApcParcours.numero).all()
|
||||||
|
niveaux_by_parcours = {
|
||||||
|
parcour.id: ApcNiveau.niveaux_annee_de_parcours(parcour, annee, self)
|
||||||
|
for parcour in parcours
|
||||||
|
}
|
||||||
|
# Cherche tronc commun
|
||||||
|
niveaux_ids_tc = set.intersection(
|
||||||
|
*[
|
||||||
|
{n.id for n in niveaux_by_parcours[parcour_id]}
|
||||||
|
for parcour_id in niveaux_by_parcours
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# Enleve les niveaux du tronc commun
|
||||||
|
niveaux_by_parcours_no_tc = {
|
||||||
|
parcour.id: [
|
||||||
|
niveau
|
||||||
|
for niveau in niveaux_by_parcours[parcour.id]
|
||||||
|
if niveau.id not in niveaux_ids_tc
|
||||||
|
]
|
||||||
|
for parcour in parcours
|
||||||
|
}
|
||||||
|
# Niveaux du TC
|
||||||
|
niveaux_tc = []
|
||||||
|
if len(parcours):
|
||||||
|
niveaux_parcours_1 = niveaux_by_parcours[parcours[0].id]
|
||||||
|
niveaux_tc = [
|
||||||
|
niveau for niveau in niveaux_parcours_1 if niveau.id in niveaux_ids_tc
|
||||||
|
]
|
||||||
|
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
|
||||||
|
return niveaux_by_parcours_no_tc
|
||||||
|
|
||||||
|
|
||||||
class ApcCompetence(db.Model, XMLModel):
|
class ApcCompetence(db.Model, XMLModel):
|
||||||
"Compétence"
|
"Compétence"
|
||||||
@ -204,9 +252,10 @@ class ApcCompetence(db.Model, XMLModel):
|
|||||||
self.niveaux = niveaux
|
self.niveaux = niveaux
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<ApcCompetence {self.id} {self.titre}>"
|
return f"<ApcCompetence {self.id} {self.titre!r}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
"repr dict recursive sur situations, composantes, niveaux"
|
||||||
return {
|
return {
|
||||||
"id_orebut": self.id_orebut,
|
"id_orebut": self.id_orebut,
|
||||||
"titre": self.titre,
|
"titre": self.titre,
|
||||||
@ -220,6 +269,16 @@ class ApcCompetence(db.Model, XMLModel):
|
|||||||
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_dict_bul(self) -> dict:
|
||||||
|
"dict court pour bulletins"
|
||||||
|
return {
|
||||||
|
"id_orebut": self.id_orebut,
|
||||||
|
"titre": self.titre,
|
||||||
|
"titre_long": self.titre_long,
|
||||||
|
"couleur": self.couleur,
|
||||||
|
"numero": self.numero,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class ApcSituationPro(db.Model, XMLModel):
|
class ApcSituationPro(db.Model, XMLModel):
|
||||||
"Situation professionnelle"
|
"Situation professionnelle"
|
||||||
@ -257,13 +316,20 @@ class ApcComposanteEssentielle(db.Model, XMLModel):
|
|||||||
|
|
||||||
|
|
||||||
class ApcNiveau(db.Model, XMLModel):
|
class ApcNiveau(db.Model, XMLModel):
|
||||||
|
"""Niveau de compétence
|
||||||
|
Chaque niveau peut être associé à deux UE,
|
||||||
|
des semestres impair et pair de la même année.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "apc_niveau"
|
||||||
|
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
competence_id = db.Column(
|
competence_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
db.Integer, db.ForeignKey("apc_competence.id"), nullable=False
|
||||||
)
|
)
|
||||||
libelle = db.Column(db.Text(), nullable=False)
|
libelle = db.Column(db.Text(), nullable=False)
|
||||||
annee = db.Column(db.Text(), nullable=False) # "BUT2"
|
annee = db.Column(db.Text(), nullable=False) # "BUT1", "BUT2", "BUT3"
|
||||||
# L'ordre est l'année d'apparition de ce niveau
|
# L'ordre est le niveau (1,2,3) ou (1,2) suivant la competence
|
||||||
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
|
ordre = db.Column(db.Integer, nullable=False) # 1, 2, 3
|
||||||
app_critiques = db.relationship(
|
app_critiques = db.relationship(
|
||||||
"ApcAppCritique",
|
"ApcAppCritique",
|
||||||
@ -271,6 +337,7 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
lazy="dynamic",
|
lazy="dynamic",
|
||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
)
|
)
|
||||||
|
ues = db.relationship("UniteEns", back_populates="niveau_competence")
|
||||||
|
|
||||||
def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques):
|
def __init__(self, id, competence_id, libelle, annee, ordre, app_critiques):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -281,9 +348,11 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
self.app_critiques = app_critiques
|
self.app_critiques = app_critiques
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} ordre={self.ordre}>"
|
return f"""<{self.__class__.__name__} ordre={self.ordre!r} annee={
|
||||||
|
self.annee!r} {self.competence!r}>"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
|
"as a dict, recursif sur les AC"
|
||||||
return {
|
return {
|
||||||
"libelle": self.libelle,
|
"libelle": self.libelle,
|
||||||
"annee": self.annee,
|
"annee": self.annee,
|
||||||
@ -291,6 +360,64 @@ class ApcNiveau(db.Model, XMLModel):
|
|||||||
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def to_dict_bul(self):
|
||||||
|
"dict pour bulletins: indique la compétence, pas les ACs (pour l'instant ?)"
|
||||||
|
return {
|
||||||
|
"libelle": self.libelle,
|
||||||
|
"annee": self.annee,
|
||||||
|
"ordre": self.ordre,
|
||||||
|
"competence": self.competence.to_dict_bul(),
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def niveaux_annee_de_parcours(
|
||||||
|
cls,
|
||||||
|
parcour: "ApcParcours",
|
||||||
|
annee: int,
|
||||||
|
referentiel_competence: ApcReferentielCompetences = None,
|
||||||
|
) -> flask_sqlalchemy.BaseQuery:
|
||||||
|
"""Les niveaux de l'année du parcours
|
||||||
|
Si le parcour est None, tous les niveaux de l'année
|
||||||
|
"""
|
||||||
|
if annee not in {1, 2, 3}:
|
||||||
|
raise ValueError("annee invalide pour un parcours BUT")
|
||||||
|
if referentiel_competence is None:
|
||||||
|
raise ScoValueError(
|
||||||
|
"Pas de référentiel de compétences associé à la formation !"
|
||||||
|
)
|
||||||
|
annee_formation = f"BUT{annee}"
|
||||||
|
if parcour is None:
|
||||||
|
return ApcNiveau.query.filter(
|
||||||
|
ApcNiveau.annee == annee_formation,
|
||||||
|
ApcCompetence.id == ApcNiveau.competence_id,
|
||||||
|
ApcCompetence.referentiel_id == referentiel_competence.id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return ApcNiveau.query.filter(
|
||||||
|
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||||
|
ApcParcours.id == ApcAnneeParcours.parcours_id,
|
||||||
|
ApcParcours.referentiel == parcour.referentiel,
|
||||||
|
ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id,
|
||||||
|
ApcCompetence.id == ApcNiveau.competence_id,
|
||||||
|
ApcAnneeParcours.parcours == parcour,
|
||||||
|
ApcNiveau.annee == annee_formation,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app_critiques_modules = db.Table(
|
||||||
|
"apc_modules_acs",
|
||||||
|
db.Column(
|
||||||
|
"module_id",
|
||||||
|
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
db.Column(
|
||||||
|
"app_crit_id",
|
||||||
|
db.ForeignKey("apc_app_critique.id"),
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ApcAppCritique(db.Model, XMLModel):
|
class ApcAppCritique(db.Model, XMLModel):
|
||||||
"Apprentissage Critique BUT"
|
"Apprentissage Critique BUT"
|
||||||
@ -299,12 +426,31 @@ class ApcAppCritique(db.Model, XMLModel):
|
|||||||
code = db.Column(db.Text(), nullable=False, index=True)
|
code = db.Column(db.Text(), nullable=False, index=True)
|
||||||
libelle = db.Column(db.Text())
|
libelle = db.Column(db.Text())
|
||||||
|
|
||||||
modules = db.relationship(
|
# modules = db.relationship(
|
||||||
"Module",
|
# "Module",
|
||||||
secondary="apc_modules_acs",
|
# secondary="apc_modules_acs",
|
||||||
lazy="dynamic",
|
# lazy="dynamic",
|
||||||
backref=db.backref("app_critiques", lazy="dynamic"),
|
# backref=db.backref("app_critiques", lazy="dynamic"),
|
||||||
)
|
# )
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def app_critiques_ref_comp(
|
||||||
|
cls,
|
||||||
|
ref_comp: ApcReferentielCompetences,
|
||||||
|
annee: str,
|
||||||
|
competence: ApcCompetence = None,
|
||||||
|
) -> flask_sqlalchemy.BaseQuery:
|
||||||
|
"Liste les AC de tous les parcours de ref_comp pour l'année indiquée"
|
||||||
|
assert annee in {"BUT1", "BUT2", "BUT3"}
|
||||||
|
query = cls.query.filter(
|
||||||
|
ApcAppCritique.niveau_id == ApcNiveau.id,
|
||||||
|
ApcNiveau.competence_id == ApcCompetence.id,
|
||||||
|
ApcNiveau.annee == annee,
|
||||||
|
ApcCompetence.referentiel_id == ref_comp.id,
|
||||||
|
)
|
||||||
|
if competence is not None:
|
||||||
|
query = query.filter(ApcNiveau.competence == competence)
|
||||||
|
return query
|
||||||
|
|
||||||
def __init__(self, id, niveau_id, code, libelle, modules):
|
def __init__(self, id, niveau_id, code, libelle, modules):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -320,18 +466,40 @@ class ApcAppCritique(db.Model, XMLModel):
|
|||||||
return self.code + " - " + self.titre
|
return self.code + " - " + self.titre
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.code}>"
|
return f"<{self.__class__.__name__} {self.code!r}>"
|
||||||
|
|
||||||
def get_saes(self):
|
def get_saes(self):
|
||||||
"""Liste des SAE associées"""
|
"""Liste des SAE associées"""
|
||||||
return [m for m in self.modules if m.module_type == ModuleType.SAE]
|
return [m for m in self.modules if m.module_type == ModuleType.SAE]
|
||||||
|
|
||||||
|
|
||||||
ApcAppCritiqueModules = db.Table(
|
parcours_modules = db.Table(
|
||||||
"apc_modules_acs",
|
"parcours_modules",
|
||||||
db.Column("module_id", db.ForeignKey("notes_modules.id")),
|
db.Column(
|
||||||
db.Column("app_crit_id", db.ForeignKey("apc_app_critique.id")),
|
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
|
||||||
|
),
|
||||||
|
db.Column(
|
||||||
|
"module_id",
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_modules.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
"""Association parcours <-> modules (many-to-many)"""
|
||||||
|
|
||||||
|
parcours_formsemestre = db.Table(
|
||||||
|
"parcours_formsemestre",
|
||||||
|
db.Column(
|
||||||
|
"parcours_id", db.Integer, db.ForeignKey("apc_parcours.id"), primary_key=True
|
||||||
|
),
|
||||||
|
db.Column(
|
||||||
|
"formsemestre_id",
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
"""Association parcours <-> formsemestre (many-to-many)"""
|
||||||
|
|
||||||
|
|
||||||
class ApcParcours(db.Model, XMLModel):
|
class ApcParcours(db.Model, XMLModel):
|
||||||
@ -358,7 +526,7 @@ class ApcParcours(db.Model, XMLModel):
|
|||||||
self.annes = annes
|
self.annes = annes
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.code}>"
|
return f"<{self.__class__.__name__} {self.code!r}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@ -375,6 +543,7 @@ class ApcAnneeParcours(db.Model, XMLModel):
|
|||||||
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
|
db.Integer, db.ForeignKey("apc_parcours.id"), nullable=False
|
||||||
)
|
)
|
||||||
ordre = db.Column(db.Integer)
|
ordre = db.Column(db.Integer)
|
||||||
|
"numéro de l'année: 1, 2, 3"
|
||||||
|
|
||||||
def __init__(self, id, parcours_id, ordre):
|
def __init__(self, id, parcours_id, ordre):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -382,7 +551,7 @@ class ApcAnneeParcours(db.Model, XMLModel):
|
|||||||
self.ordre = ordre
|
self.ordre = ordre
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} ordre={self.ordre}>"
|
return f"<{self.__class__.__name__} ordre={self.ordre!r} parcours={self.parcours.code!r}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
return {
|
return {
|
||||||
@ -420,6 +589,7 @@ class ApcParcoursNiveauCompetence(db.Model):
|
|||||||
"annee_parcours",
|
"annee_parcours",
|
||||||
passive_deletes=True,
|
passive_deletes=True,
|
||||||
cascade="save-update, merge, delete, delete-orphan",
|
cascade="save-update, merge, delete, delete-orphan",
|
||||||
|
lazy="dynamic",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
annee_parcours = db.relationship(
|
annee_parcours = db.relationship(
|
||||||
@ -432,4 +602,4 @@ class ApcParcoursNiveauCompetence(db.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.competence} {self.annee_parcours}>"
|
return f"<{self.__class__.__name__} {self.competence!r}<->{self.annee_parcours!r} niveau={self.niveau!r}>"
|
||||||
|
339
app/models/but_validations.py
Normal file
339
app/models/but_validations.py
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
# -*- coding: UTF-8 -*
|
||||||
|
|
||||||
|
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||||
|
"""
|
||||||
|
|
||||||
|
import flask_sqlalchemy
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
|
||||||
|
from app.models import CODE_STR_LEN
|
||||||
|
from app.models.but_refcomp import ApcNiveau
|
||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.ues import UniteEns
|
||||||
|
from app.models.formations import Formation
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.scodoc import sco_codes_parcours as sco_codes
|
||||||
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
class ApcValidationRCUE(db.Model):
|
||||||
|
"""Validation des niveaux de compétences
|
||||||
|
|
||||||
|
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||||
|
|
||||||
|
le formsemestre est celui du semestre PAIR du niveau de compétence
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__ = "apc_validation_rcue"
|
||||||
|
# Assure unicité de la décision:
|
||||||
|
__table_args__ = (
|
||||||
|
db.UniqueConstraint("etudid", "formsemestre_id", "ue1_id", "ue2_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
formsemestre_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
|
||||||
|
)
|
||||||
|
# Les deux UE associées à ce niveau:
|
||||||
|
ue1_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||||
|
ue2_id = db.Column(db.Integer, db.ForeignKey("notes_ue.id"), nullable=False)
|
||||||
|
# optionnel, le parcours dans lequel se trouve la compétence:
|
||||||
|
parcours_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), nullable=True)
|
||||||
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||||
|
|
||||||
|
etud = db.relationship("Identite", backref="apc_validations_rcues")
|
||||||
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_rcues")
|
||||||
|
ue1 = db.relationship("UniteEns", foreign_keys=ue1_id)
|
||||||
|
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
||||||
|
parcour = db.relationship("ApcParcours")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} {self.id} {self.etud} {self.ue1}/{self.ue2}:{self.code!r}>"
|
||||||
|
|
||||||
|
def niveau(self) -> ApcNiveau:
|
||||||
|
"""Le niveau de compétence associé à cet RCUE."""
|
||||||
|
# Par convention, il est donné par la seconde UE
|
||||||
|
return self.ue2.niveau_competence
|
||||||
|
|
||||||
|
def to_dict_bul(self) -> dict:
|
||||||
|
"Export dict pour bulletins"
|
||||||
|
return {"code": self.code, "niveau": self.niveau().to_dict_bul()}
|
||||||
|
|
||||||
|
|
||||||
|
# Attention: ce n'est pas un modèle mais une classe ordinaire:
|
||||||
|
class RegroupementCoherentUE:
|
||||||
|
"""Le regroupement cohérent d'UE, dans la terminologie du BUT, est le couple d'UEs
|
||||||
|
de la même année (BUT1,2,3) liées au *même niveau de compétence*.
|
||||||
|
|
||||||
|
La moyenne (10/20) au RCU déclenche la compensation des UE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
etud: Identite,
|
||||||
|
formsemestre_1: FormSemestre,
|
||||||
|
ue_1: UniteEns,
|
||||||
|
formsemestre_2: FormSemestre,
|
||||||
|
ue_2: UniteEns,
|
||||||
|
inscription_etat: str,
|
||||||
|
):
|
||||||
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_but import ResultatsSemestreBUT
|
||||||
|
|
||||||
|
# Ordonne les UE dans le sens croissant (S1,S2) ou (S3,S4)...
|
||||||
|
if formsemestre_1.semestre_id > formsemestre_2.semestre_id:
|
||||||
|
(ue_1, formsemestre_1), (ue_2, formsemestre_2) = (
|
||||||
|
(
|
||||||
|
ue_2,
|
||||||
|
formsemestre_2,
|
||||||
|
),
|
||||||
|
(ue_1, formsemestre_1),
|
||||||
|
)
|
||||||
|
assert formsemestre_1.semestre_id % 2 == 1
|
||||||
|
assert formsemestre_2.semestre_id % 2 == 0
|
||||||
|
assert abs(formsemestre_1.semestre_id - formsemestre_2.semestre_id) == 1
|
||||||
|
assert ue_1.niveau_competence_id == ue_2.niveau_competence_id
|
||||||
|
self.etud = etud
|
||||||
|
self.formsemestre_1 = formsemestre_1
|
||||||
|
"semestre impair"
|
||||||
|
self.ue_1 = ue_1
|
||||||
|
self.formsemestre_2 = formsemestre_2
|
||||||
|
"semestre pair"
|
||||||
|
self.ue_2 = ue_2
|
||||||
|
# Stocke les moyennes d'UE
|
||||||
|
if inscription_etat != scu.INSCRIT:
|
||||||
|
self.moy_rcue = None
|
||||||
|
self.moy_ue_1 = self.moy_ue_2 = "-"
|
||||||
|
self.moy_ue_1_val = self.moy_ue_2_val = 0.0
|
||||||
|
return
|
||||||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_1)
|
||||||
|
if ue_1.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_1.id]:
|
||||||
|
self.moy_ue_1 = res.etud_moy_ue[ue_1.id][etud.id]
|
||||||
|
self.moy_ue_1_val = self.moy_ue_1 # toujours float, peut être NaN
|
||||||
|
else:
|
||||||
|
self.moy_ue_1 = None
|
||||||
|
self.moy_ue_1_val = 0.0
|
||||||
|
res: ResultatsSemestreBUT = res_sem.load_formsemestre_results(formsemestre_2)
|
||||||
|
if ue_2.id in res.etud_moy_ue and etud.id in res.etud_moy_ue[ue_2.id]:
|
||||||
|
self.moy_ue_2 = res.etud_moy_ue[ue_2.id][etud.id]
|
||||||
|
self.moy_ue_2_val = self.moy_ue_2
|
||||||
|
else:
|
||||||
|
self.moy_ue_2 = None
|
||||||
|
self.moy_ue_2_val = 0.0
|
||||||
|
# Calcul de la moyenne au RCUE
|
||||||
|
if (self.moy_ue_1 is not None) and (self.moy_ue_2 is not None):
|
||||||
|
# Moyenne RCUE (les pondérations par défaut sont 1.)
|
||||||
|
self.moy_rcue = (
|
||||||
|
self.moy_ue_1 * ue_1.coef_rcue + self.moy_ue_2 * ue_2.coef_rcue
|
||||||
|
) / (ue_1.coef_rcue + ue_2.coef_rcue)
|
||||||
|
else:
|
||||||
|
self.moy_rcue = None
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<{self.__class__.__name__} {self.ue_1.acronyme}({self.moy_ue_1}) {self.ue_2.acronyme}({self.moy_ue_2})>"
|
||||||
|
|
||||||
|
def query_validations(
|
||||||
|
self,
|
||||||
|
) -> flask_sqlalchemy.BaseQuery: # list[ApcValidationRCUE]
|
||||||
|
"""Les validations de jury enregistrées pour ce RCUE"""
|
||||||
|
niveau = self.ue_2.niveau_competence
|
||||||
|
|
||||||
|
return (
|
||||||
|
ApcValidationRCUE.query.filter_by(
|
||||||
|
etudid=self.etud.id,
|
||||||
|
)
|
||||||
|
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||||
|
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||||
|
.filter(ApcNiveau.id == niveau.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
def other_ue(self, ue: UniteEns) -> UniteEns:
|
||||||
|
"""L'autre UE du regroupement. Si ue ne fait pas partie du regroupement, ValueError"""
|
||||||
|
if ue.id == self.ue_1.id:
|
||||||
|
return self.ue_2
|
||||||
|
elif ue.id == self.ue_2.id:
|
||||||
|
return self.ue_1
|
||||||
|
raise ValueError(f"ue {ue} hors RCUE {self}")
|
||||||
|
|
||||||
|
def est_enregistre(self) -> bool:
|
||||||
|
"""Vrai si ce RCUE, donc le niveau de compétences correspondant
|
||||||
|
a une décision jury enregistrée
|
||||||
|
"""
|
||||||
|
return self.query_validations().count() > 0
|
||||||
|
|
||||||
|
def est_compensable(self):
|
||||||
|
"""Vrai si ce RCUE est validable par compensation
|
||||||
|
c'est à dire que sa moyenne est > 10 avec une UE < 10
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
(self.moy_rcue is not None)
|
||||||
|
and (self.moy_rcue > sco_codes.BUT_BARRE_RCUE)
|
||||||
|
and (
|
||||||
|
(self.moy_ue_1_val < sco_codes.NOTES_BARRE_GEN)
|
||||||
|
or (self.moy_ue_2_val < sco_codes.NOTES_BARRE_GEN)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def est_suffisant(self) -> bool:
|
||||||
|
"""Vrai si ce RCUE est > 8"""
|
||||||
|
return (self.moy_rcue is not None) and (
|
||||||
|
self.moy_rcue > sco_codes.BUT_RCUE_SUFFISANT
|
||||||
|
)
|
||||||
|
|
||||||
|
def est_validable(self) -> bool:
|
||||||
|
"""Vrai si ce RCU satisfait les conditions pour être validé
|
||||||
|
Pour cela, il suffit que la moyenne des UE qui le constitue soit > 10
|
||||||
|
"""
|
||||||
|
return (self.moy_rcue is not None) and (
|
||||||
|
self.moy_rcue > sco_codes.BUT_BARRE_RCUE
|
||||||
|
)
|
||||||
|
|
||||||
|
def code_valide(self) -> Union[ApcValidationRCUE, None]:
|
||||||
|
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
|
||||||
|
validation = self.query_validations().first()
|
||||||
|
if (validation is not None) and (
|
||||||
|
validation.code in sco_codes.CODES_RCUE_VALIDES
|
||||||
|
):
|
||||||
|
return validation
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# unused
|
||||||
|
def find_rcues(
|
||||||
|
formsemestre: FormSemestre, ue: UniteEns, etud: Identite, inscription_etat: str
|
||||||
|
) -> list[RegroupementCoherentUE]:
|
||||||
|
"""Les RCUE (niveau de compétence) à considérer pour cet étudiant dans
|
||||||
|
ce semestre pour cette UE.
|
||||||
|
|
||||||
|
Cherche les UEs du même niveau de compétence auxquelles l'étudiant est inscrit.
|
||||||
|
En cas de redoublement, il peut y en avoir plusieurs, donc plusieurs RCUEs.
|
||||||
|
|
||||||
|
Résultat: la liste peut être vide.
|
||||||
|
"""
|
||||||
|
if (ue.niveau_competence is None) or (ue.semestre_idx is None):
|
||||||
|
return []
|
||||||
|
|
||||||
|
if ue.semestre_idx % 2: # S1, S3, S5
|
||||||
|
other_semestre_idx = ue.semestre_idx + 1
|
||||||
|
else:
|
||||||
|
other_semestre_idx = ue.semestre_idx - 1
|
||||||
|
|
||||||
|
cursor = db.session.execute(
|
||||||
|
text(
|
||||||
|
"""SELECT
|
||||||
|
ue.id, formsemestre.id
|
||||||
|
FROM
|
||||||
|
notes_ue ue,
|
||||||
|
notes_formsemestre_inscription inscr,
|
||||||
|
notes_formsemestre formsemestre
|
||||||
|
|
||||||
|
WHERE
|
||||||
|
inscr.etudid = :etudid
|
||||||
|
AND inscr.formsemestre_id = formsemestre.id
|
||||||
|
|
||||||
|
AND formsemestre.semestre_id = :other_semestre_idx
|
||||||
|
AND ue.formation_id = formsemestre.formation_id
|
||||||
|
AND ue.niveau_competence_id = :ue_niveau_competence_id
|
||||||
|
AND ue.semestre_idx = :other_semestre_idx
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"etudid": etud.id,
|
||||||
|
"other_semestre_idx": other_semestre_idx,
|
||||||
|
"ue_niveau_competence_id": ue.niveau_competence_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rcues = []
|
||||||
|
for ue_id, formsemestre_id in cursor:
|
||||||
|
other_ue = UniteEns.query.get(ue_id)
|
||||||
|
other_formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
|
rcues.append(
|
||||||
|
RegroupementCoherentUE(
|
||||||
|
etud, formsemestre, ue, other_formsemestre, other_ue, inscription_etat
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# safety check: 1 seul niveau de comp. concerné:
|
||||||
|
assert len({rcue.ue_1.niveau_competence_id for rcue in rcues}) == 1
|
||||||
|
return rcues
|
||||||
|
|
||||||
|
|
||||||
|
class ApcValidationAnnee(db.Model):
|
||||||
|
"""Validation des années du BUT"""
|
||||||
|
|
||||||
|
__tablename__ = "apc_validation_annee"
|
||||||
|
# Assure unicité de la décision:
|
||||||
|
__table_args__ = (db.UniqueConstraint("etudid", "annee_scolaire"),)
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
etudid = db.Column(
|
||||||
|
db.Integer,
|
||||||
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
ordre = db.Column(db.Integer, nullable=False)
|
||||||
|
"numéro de l'année: 1, 2, 3"
|
||||||
|
formsemestre_id = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("notes_formsemestre.id"), nullable=True
|
||||||
|
)
|
||||||
|
"le semestre IMPAIR (le 1er) de l'année"
|
||||||
|
annee_scolaire = db.Column(db.Integer, nullable=False) # 2021
|
||||||
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
|
code = db.Column(db.String(CODE_STR_LEN), nullable=False, index=True)
|
||||||
|
|
||||||
|
etud = db.relationship("Identite", backref="apc_validations_annees")
|
||||||
|
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.__class__.__name__} {self.id} {self.etud} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"
|
||||||
|
|
||||||
|
def to_dict_bul(self) -> dict:
|
||||||
|
"dict pour bulletins"
|
||||||
|
return {
|
||||||
|
"annee_scolaire": self.annee_scolaire,
|
||||||
|
"date": self.date.isoformat(),
|
||||||
|
"code": self.code,
|
||||||
|
"ordre": self.ordre,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||||
|
"""
|
||||||
|
Un dict avec les décisions de jury BUT enregistrées.
|
||||||
|
Ne reprend pas les décisions d'UE, non spécifiques au BUT.
|
||||||
|
"""
|
||||||
|
decisions = {}
|
||||||
|
# --- RCUEs: seulement sur semestres pairs XXX à améliorer
|
||||||
|
if formsemestre.semestre_id % 2 == 0:
|
||||||
|
# validations émises depuis ce formsemestre:
|
||||||
|
validations_rcues = ApcValidationRCUE.query.filter_by(
|
||||||
|
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||||
|
)
|
||||||
|
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||||
|
else:
|
||||||
|
decisions["decision_rcue"] = []
|
||||||
|
# --- Année: prend la validation pour l'année scolaire de ce semestre
|
||||||
|
validation = (
|
||||||
|
ApcValidationAnnee.query.filter_by(
|
||||||
|
etudid=etud.id,
|
||||||
|
annee_scolaire=formsemestre.annee_scolaire(),
|
||||||
|
)
|
||||||
|
.join(ApcValidationAnnee.formsemestre)
|
||||||
|
.join(FormSemestre.formation)
|
||||||
|
.filter(Formation.formation_code == formsemestre.formation.formation_code)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if validation:
|
||||||
|
decisions["decision_annee"] = validation.to_dict_bul()
|
||||||
|
else:
|
||||||
|
decisions["decision_annee"] = None
|
||||||
|
return decisions
|
@ -9,6 +9,8 @@ from app.comp import bonus_spo
|
|||||||
from app.scodoc.sco_exceptions import ScoValueError
|
from app.scodoc.sco_exceptions import ScoValueError
|
||||||
|
|
||||||
from app.scodoc.sco_codes_parcours import (
|
from app.scodoc.sco_codes_parcours import (
|
||||||
|
ABAN,
|
||||||
|
ABL,
|
||||||
ADC,
|
ADC,
|
||||||
ADJ,
|
ADJ,
|
||||||
ADM,
|
ADM,
|
||||||
@ -19,11 +21,17 @@ from app.scodoc.sco_codes_parcours import (
|
|||||||
CMP,
|
CMP,
|
||||||
DEF,
|
DEF,
|
||||||
DEM,
|
DEM,
|
||||||
|
EXCLU,
|
||||||
NAR,
|
NAR,
|
||||||
|
PASD,
|
||||||
|
PAS1NCI,
|
||||||
RAT,
|
RAT,
|
||||||
|
RED,
|
||||||
)
|
)
|
||||||
|
|
||||||
CODES_SCODOC_TO_APO = {
|
CODES_SCODOC_TO_APO = {
|
||||||
|
ABAN: "ABAN",
|
||||||
|
ABL: "ABL",
|
||||||
ADC: "ADMC",
|
ADC: "ADMC",
|
||||||
ADJ: "ADM",
|
ADJ: "ADM",
|
||||||
ADM: "ADM",
|
ADM: "ADM",
|
||||||
@ -34,8 +42,12 @@ CODES_SCODOC_TO_APO = {
|
|||||||
CMP: "COMP",
|
CMP: "COMP",
|
||||||
DEF: "NAR",
|
DEF: "NAR",
|
||||||
DEM: "NAR",
|
DEM: "NAR",
|
||||||
|
EXCLU: "EXC",
|
||||||
NAR: "NAR",
|
NAR: "NAR",
|
||||||
|
PASD: "PASD",
|
||||||
|
PAS1NCI: "PAS1NCI",
|
||||||
RAT: "ATT",
|
RAT: "ATT",
|
||||||
|
RED: "RED",
|
||||||
"NOTES_FMT": "%3.2f",
|
"NOTES_FMT": "%3.2f",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,9 +173,8 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_code_apo(cls, code: str) -> str:
|
def get_code_apo(cls, code: str) -> str:
|
||||||
"""La représentation d'un code pour les exports Apogée.
|
"""La représentation d'un code pour les exports Apogée.
|
||||||
Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL
|
Par exemple, à l'IUT du H., le code ADM est réprésenté par VAL
|
||||||
Les codes par défaut sont donnés dans sco_apogee_csv.
|
Les codes par défaut sont donnés dans sco_apogee_csv.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
|
cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
|
||||||
if not cfg:
|
if not cfg:
|
||||||
@ -172,6 +183,11 @@ class ScoDocSiteConfig(db.Model):
|
|||||||
code_apo = cfg.value
|
code_apo = cfg.value
|
||||||
return code_apo
|
return code_apo
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_codes_apo_dict(cls) -> dict[str:str]:
|
||||||
|
"Un dict avec code jury : code exporté"
|
||||||
|
return {code: cls.get_code_apo(code) for code in CODES_SCODOC_TO_APO}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def set_code_apo(cls, code: str, code_apo: str):
|
def set_code_apo(cls, code: str, code_apo: str):
|
||||||
"""Enregistre nouvelle représentation du code"""
|
"""Enregistre nouvelle représentation du code"""
|
||||||
|
@ -60,7 +60,9 @@ class Identite(db.Model):
|
|||||||
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
|
admission = db.relationship("Admission", backref="identite", lazy="dynamic")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Etud {self.id}/{self.departement.acronym} {self.nom} {self.prenom}>"
|
return (
|
||||||
|
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_request(cls, etudid=None, code_nip=None):
|
def from_request(cls, etudid=None, code_nip=None):
|
||||||
@ -133,8 +135,10 @@ class Identite(db.Model):
|
|||||||
def sort_key(self) -> tuple:
|
def sort_key(self) -> tuple:
|
||||||
"clé pour tris par ordre alphabétique"
|
"clé pour tris par ordre alphabétique"
|
||||||
return (
|
return (
|
||||||
scu.suppress_accents(self.nom_usuel or self.nom or "").lower(),
|
scu.sanitize_string(
|
||||||
scu.suppress_accents(self.prenom or "").lower(),
|
self.nom_usuel or self.nom or "", remove_spaces=False
|
||||||
|
).lower(),
|
||||||
|
scu.sanitize_string(self.prenom or "", remove_spaces=False).lower(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_first_email(self, field="email") -> str:
|
def get_first_email(self, field="email") -> str:
|
||||||
@ -201,6 +205,19 @@ class Identite(db.Model):
|
|||||||
d.update(adresse.to_dict(convert_nulls_to_str=True))
|
d.update(adresse.to_dict(convert_nulls_to_str=True))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||||
|
"Liste des inscriptions à des formsemestres, triée, la plus récente en tête"
|
||||||
|
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||||
|
|
||||||
|
return (
|
||||||
|
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
|
||||||
|
.filter(
|
||||||
|
FormSemestreInscription.etudid == self.id,
|
||||||
|
)
|
||||||
|
.order_by(desc(FormSemestre.date_debut))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
def inscription_courante(self):
|
def inscription_courante(self):
|
||||||
"""La première inscription à un formsemestre _actuellement_ en cours.
|
"""La première inscription à un formsemestre _actuellement_ en cours.
|
||||||
None s'il n'y en a pas (ou plus, ou pas encore).
|
None s'il n'y en a pas (ou plus, ou pas encore).
|
||||||
@ -212,7 +229,7 @@ class Identite(db.Model):
|
|||||||
]
|
]
|
||||||
return r[0] if r else None
|
return r[0] if r else None
|
||||||
|
|
||||||
def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]:
|
def inscriptions_courantes(self) -> list["FormSemestreInscription"]:
|
||||||
"""Liste des inscriptions à des semestres _courants_
|
"""Liste des inscriptions à des semestres _courants_
|
||||||
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
(il est rare qu'il y en ai plus d'une, mais c'est possible).
|
||||||
Triées par date de début de semestre décroissante (le plus récent en premier).
|
Triées par date de début de semestre décroissante (le plus récent en premier).
|
||||||
@ -240,18 +257,6 @@ class Identite(db.Model):
|
|||||||
]
|
]
|
||||||
return r[0] if r else None
|
return r[0] if r else None
|
||||||
|
|
||||||
def inscription_etat(self, formsemestre_id):
|
|
||||||
"""État de l'inscription de cet étudiant au semestre:
|
|
||||||
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
|
||||||
"""
|
|
||||||
# voir si ce n'est pas trop lent:
|
|
||||||
ins = models.FormSemestreInscription.query.filter_by(
|
|
||||||
etudid=self.id, formsemestre_id=formsemestre_id
|
|
||||||
).first()
|
|
||||||
if ins:
|
|
||||||
return ins.etat
|
|
||||||
return False
|
|
||||||
|
|
||||||
def inscription_descr(self) -> dict:
|
def inscription_descr(self) -> dict:
|
||||||
"""Description de l'état d'inscription"""
|
"""Description de l'état d'inscription"""
|
||||||
inscription_courante = self.inscription_courante()
|
inscription_courante = self.inscription_courante()
|
||||||
@ -290,6 +295,18 @@ class Identite(db.Model):
|
|||||||
"situation": situation,
|
"situation": situation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def inscription_etat(self, formsemestre_id):
|
||||||
|
"""État de l'inscription de cet étudiant au semestre:
|
||||||
|
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
|
||||||
|
"""
|
||||||
|
# voir si ce n'est pas trop lent:
|
||||||
|
ins = models.FormSemestreInscription.query.filter_by(
|
||||||
|
etudid=self.id, formsemestre_id=formsemestre_id
|
||||||
|
).first()
|
||||||
|
if ins:
|
||||||
|
return ins.etat
|
||||||
|
return False
|
||||||
|
|
||||||
def descr_situation_etud(self) -> str:
|
def descr_situation_etud(self) -> str:
|
||||||
"""Chaîne décrivant la situation _actuelle_ de l'étudiant.
|
"""Chaîne décrivant la situation _actuelle_ de l'étudiant.
|
||||||
Exemple:
|
Exemple:
|
||||||
@ -361,6 +378,15 @@ class Identite(db.Model):
|
|||||||
|
|
||||||
return situation
|
return situation
|
||||||
|
|
||||||
|
def etat_civil_pv(self, line_sep="\n") -> str:
|
||||||
|
"""Présentation, pour PV jury
|
||||||
|
M. Pierre Dupont
|
||||||
|
n° 12345678
|
||||||
|
né(e) le 7/06/1974
|
||||||
|
à Paris
|
||||||
|
"""
|
||||||
|
return f"""{self.nomprenom}{line_sep}n°{self.code_nip or ""}{line_sep}né{self.e} le {self.date_naissance.strftime("%d/%m/%Y") if self.date_naissance else ""}{line_sep}à {self.lieu_naissance or ""}"""
|
||||||
|
|
||||||
def photo_html(self, title=None, size="small") -> str:
|
def photo_html(self, title=None, size="small") -> str:
|
||||||
"""HTML img tag for the photo, either in small size (h90)
|
"""HTML img tag for the photo, either in small size (h90)
|
||||||
or original size (size=="orig")
|
or original size (size=="orig")
|
||||||
@ -434,7 +460,7 @@ class Adresse(db.Model):
|
|||||||
adresse_id = db.synonym("id")
|
adresse_id = db.synonym("id")
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
email = db.Column(db.Text()) # mail institutionnel
|
email = db.Column(db.Text()) # mail institutionnel
|
||||||
emailperso = db.Column(db.Text) # email personnel (exterieur)
|
emailperso = db.Column(db.Text) # email personnel (exterieur)
|
||||||
@ -468,7 +494,7 @@ class Admission(db.Model):
|
|||||||
adm_id = db.synonym("id")
|
adm_id = db.synonym("id")
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
# Anciens champs de ScoDoc7, à revoir pour être plus générique et souple
|
# Anciens champs de ScoDoc7, à revoir pour être plus générique et souple
|
||||||
# notamment dans le cadre du bac 2021
|
# notamment dans le cadre du bac 2021
|
||||||
@ -513,21 +539,21 @@ class Admission(db.Model):
|
|||||||
|
|
||||||
def to_dict(self, no_nulls=False):
|
def to_dict(self, no_nulls=False):
|
||||||
"""Représentation dictionnaire,"""
|
"""Représentation dictionnaire,"""
|
||||||
e = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
e.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
if no_nulls:
|
if no_nulls:
|
||||||
for k in e:
|
for k in d.keys():
|
||||||
if e[k] is None:
|
if d[k] is None:
|
||||||
col_type = getattr(
|
col_type = getattr(
|
||||||
sqlalchemy.inspect(models.Admission).columns, "apb_groupe"
|
sqlalchemy.inspect(models.Admission).columns, "apb_groupe"
|
||||||
).expression.type
|
).expression.type
|
||||||
if isinstance(col_type, sqlalchemy.Text):
|
if isinstance(col_type, sqlalchemy.Text):
|
||||||
e[k] = ""
|
d[k] = ""
|
||||||
elif isinstance(col_type, sqlalchemy.Integer):
|
elif isinstance(col_type, sqlalchemy.Integer):
|
||||||
e[k] = 0
|
d[k] = 0
|
||||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||||
e[k] = False
|
d[k] = False
|
||||||
return e
|
return d
|
||||||
|
|
||||||
|
|
||||||
# Suivi scolarité / débouchés
|
# Suivi scolarité / débouchés
|
||||||
@ -538,7 +564,7 @@ class ItemSuivi(db.Model):
|
|||||||
itemsuivi_id = db.synonym("id")
|
itemsuivi_id = db.synonym("id")
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
situation = db.Column(db.Text)
|
situation = db.Column(db.Text)
|
||||||
|
@ -32,6 +32,21 @@ class Scolog(db.Model):
|
|||||||
authenticated_user = db.Column(db.Text) # login, sans contrainte
|
authenticated_user = db.Column(db.Text) # login, sans contrainte
|
||||||
# zope_remote_addr suppressed
|
# zope_remote_addr suppressed
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def logdb(
|
||||||
|
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
|
||||||
|
):
|
||||||
|
"""Add entry in student's log (replacement for old scolog.logdb)"""
|
||||||
|
entry = Scolog(
|
||||||
|
method=method,
|
||||||
|
msg=msg,
|
||||||
|
etudid=etudid,
|
||||||
|
authenticated_user=current_user.user_name,
|
||||||
|
)
|
||||||
|
db.session.add(entry)
|
||||||
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
class ScolarNews(db.Model):
|
class ScolarNews(db.Model):
|
||||||
"""Nouvelles pour page d'accueil"""
|
"""Nouvelles pour page d'accueil"""
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
"""ScoDoc 9 models : Formations
|
"""ScoDoc 9 models : Formations
|
||||||
"""
|
"""
|
||||||
|
import flask_sqlalchemy
|
||||||
|
|
||||||
import app
|
import app
|
||||||
from app import db
|
from app import db
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
|
from app.models.but_refcomp import (
|
||||||
|
ApcAnneeParcours,
|
||||||
|
ApcCompetence,
|
||||||
|
ApcNiveau,
|
||||||
|
ApcParcours,
|
||||||
|
ApcParcoursNiveauCompetence,
|
||||||
|
)
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.ues import UniteEns
|
from app.models.ues import UniteEns
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
from app.scodoc.sco_codes_parcours import UE_STANDARD
|
||||||
|
|
||||||
|
|
||||||
class Formation(db.Model):
|
class Formation(db.Model):
|
||||||
@ -45,7 +55,11 @@ class Formation(db.Model):
|
|||||||
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
modules = db.relationship("Module", lazy="dynamic", backref="formation")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme}')>"
|
return f"<{self.__class__.__name__}(id={self.id}, dept_id={self.dept_id}, acronyme='{self.acronyme!r}')>"
|
||||||
|
|
||||||
|
def to_html(self) -> str:
|
||||||
|
"titre complet pour affichage"
|
||||||
|
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
@ -55,7 +69,10 @@ class Formation(db.Model):
|
|||||||
return e
|
return e
|
||||||
|
|
||||||
def get_parcours(self):
|
def get_parcours(self):
|
||||||
"""get l'instance de TypeParcours de cette formation"""
|
"""get l'instance de TypeParcours de cette formation
|
||||||
|
(le TypeParcours définit le genre de formation, à ne pas confondre
|
||||||
|
avec les parcours du BUT).
|
||||||
|
"""
|
||||||
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
return sco_codes_parcours.get_parcours_from_code(self.type_parcours)
|
||||||
|
|
||||||
def get_titre_version(self) -> str:
|
def get_titre_version(self) -> str:
|
||||||
@ -97,6 +114,13 @@ class Formation(db.Model):
|
|||||||
else:
|
else:
|
||||||
keys = f"{self.id}.{semestre_idx}"
|
keys = f"{self.id}.{semestre_idx}"
|
||||||
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
|
df_cache.ModuleCoefsCache.delete_many(keys | {f"{self.id}"})
|
||||||
|
# Invalidate aussi les poids de toutes les évals de la formation
|
||||||
|
for modimpl in ModuleImpl.query.filter(
|
||||||
|
ModuleImpl.module_id == Module.id,
|
||||||
|
Module.formation_id == self.id,
|
||||||
|
):
|
||||||
|
modimpl.invalidate_evaluations_poids()
|
||||||
|
|
||||||
sco_cache.invalidate_formsemestre()
|
sco_cache.invalidate_formsemestre()
|
||||||
|
|
||||||
def invalidate_cached_sems(self):
|
def invalidate_cached_sems(self):
|
||||||
@ -148,6 +172,40 @@ class Formation(db.Model):
|
|||||||
if change:
|
if change:
|
||||||
app.clear_scodoc_cache()
|
app.clear_scodoc_cache()
|
||||||
|
|
||||||
|
def query_ues_parcour(self, parcour: ApcParcours) -> flask_sqlalchemy.BaseQuery:
|
||||||
|
"""Les UEs d'un parcours de la formation.
|
||||||
|
Exemple: pour avoir les UE du semestre 3, faire
|
||||||
|
`formation.query_ues_parcour(parcour).filter_by(semestre_idx=3)`
|
||||||
|
"""
|
||||||
|
return UniteEns.query.filter_by(formation=self).filter(
|
||||||
|
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||||
|
UniteEns.type == UE_STANDARD,
|
||||||
|
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||||
|
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||||
|
ApcAnneeParcours.parcours_id == parcour.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def query_competences_parcour(
|
||||||
|
self, parcour: ApcParcours
|
||||||
|
) -> flask_sqlalchemy.BaseQuery:
|
||||||
|
"""Les ApcCompetences d'un parcours de la formation.
|
||||||
|
None si pas de référentiel de compétences.
|
||||||
|
"""
|
||||||
|
if self.referentiel_competence_id is None:
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
ApcCompetence.query.filter_by(referentiel_id=self.referentiel_competence_id)
|
||||||
|
.join(
|
||||||
|
ApcParcoursNiveauCompetence,
|
||||||
|
ApcParcoursNiveauCompetence.competence_id == ApcCompetence.id,
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
ApcAnneeParcours,
|
||||||
|
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||||
|
)
|
||||||
|
.filter(ApcAnneeParcours.parcours_id == parcour.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Matiere(db.Model):
|
class Matiere(db.Model):
|
||||||
"""Matières: regroupe les modules d'une UE
|
"""Matières: regroupe les modules d'une UE
|
||||||
@ -168,7 +226,7 @@ class Matiere(db.Model):
|
|||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
||||||
self.ue_id}, titre='{self.titre}')>"""
|
self.ue_id}, titre='{self.titre!r}')>"""
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
"""as a dict, with the same conversions as in ScoDoc7"""
|
||||||
|
@ -5,19 +5,31 @@
|
|||||||
import datetime
|
import datetime
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
|
|
||||||
|
from flask import flash
|
||||||
import flask_sqlalchemy
|
import flask_sqlalchemy
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import log
|
from app import log
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
|
from app.models.but_refcomp import (
|
||||||
|
ApcAnneeParcours,
|
||||||
|
ApcNiveau,
|
||||||
|
ApcParcours,
|
||||||
|
ApcParcoursNiveauCompetence,
|
||||||
|
)
|
||||||
|
from app.models.groups import GroupDescr, Partition
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.models.ues import UniteEns
|
from app.models.but_refcomp import ApcParcours
|
||||||
|
from app.models.but_refcomp import parcours_formsemestre
|
||||||
|
from app.models.etudiants import Identite
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
from app.models.moduleimpls import ModuleImpl
|
from app.models.moduleimpls import ModuleImpl
|
||||||
from app.models.etudiants import Identite
|
from app.models.ues import UniteEns
|
||||||
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
@ -113,6 +125,14 @@ class FormSemestre(db.Model):
|
|||||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||||
|
|
||||||
|
# BUT
|
||||||
|
parcours = db.relationship(
|
||||||
|
"ApcParcours",
|
||||||
|
secondary=parcours_formsemestre,
|
||||||
|
lazy="subquery",
|
||||||
|
backref=db.backref("formsemestres", lazy=True),
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super(FormSemestre, self).__init__(**kwargs)
|
super(FormSemestre, self).__init__(**kwargs)
|
||||||
if self.modalite is None:
|
if self.modalite is None:
|
||||||
@ -121,7 +141,7 @@ class FormSemestre(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self, convert_parcours=False):
|
||||||
"dict (compatible ScoDoc7)"
|
"dict (compatible ScoDoc7)"
|
||||||
d = dict(self.__dict__)
|
d = dict(self.__dict__)
|
||||||
d.pop("_sa_instance_state", None)
|
d.pop("_sa_instance_state", None)
|
||||||
@ -140,6 +160,8 @@ class FormSemestre(db.Model):
|
|||||||
d["date_fin"] = d["date_fin_iso"] = ""
|
d["date_fin"] = d["date_fin_iso"] = ""
|
||||||
d["responsables"] = [u.id for u in self.responsables]
|
d["responsables"] = [u.id for u in self.responsables]
|
||||||
d["titre_formation"] = self.titre_formation()
|
d["titre_formation"] = self.titre_formation()
|
||||||
|
if convert_parcours:
|
||||||
|
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def to_dict_api(self):
|
def to_dict_api(self):
|
||||||
@ -219,6 +241,22 @@ class FormSemestre(db.Model):
|
|||||||
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
sem_ues = sem_ues.filter(UniteEns.type != sco_codes_parcours.UE_SPORT)
|
||||||
return sem_ues.order_by(UniteEns.numero)
|
return sem_ues.order_by(UniteEns.numero)
|
||||||
|
|
||||||
|
def query_ues_parcours_etud(self, etudid: int) -> flask_sqlalchemy.BaseQuery:
|
||||||
|
"""UE que suit l'étudiant dans ce semestre BUT
|
||||||
|
en fonction du parcours dans lequel il est inscrit.
|
||||||
|
|
||||||
|
Si voulez les UE d'un parcours, il est plus efficace de passer par
|
||||||
|
`formation.query_ues_parcour(parcour)`.
|
||||||
|
"""
|
||||||
|
return self.query_ues().filter(
|
||||||
|
FormSemestreInscription.etudid == etudid,
|
||||||
|
FormSemestreInscription.formsemestre == self,
|
||||||
|
UniteEns.niveau_competence_id == ApcNiveau.id,
|
||||||
|
ApcParcoursNiveauCompetence.competence_id == ApcNiveau.competence_id,
|
||||||
|
ApcParcoursNiveauCompetence.annee_parcours_id == ApcAnneeParcours.id,
|
||||||
|
ApcAnneeParcours.parcours_id == FormSemestreInscription.parcour_id,
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def modimpls_sorted(self) -> list[ModuleImpl]:
|
def modimpls_sorted(self) -> list[ModuleImpl]:
|
||||||
"""Liste des modimpls du semestre (y compris bonus)
|
"""Liste des modimpls du semestre (y compris bonus)
|
||||||
@ -245,6 +283,28 @@ class FormSemestre(db.Model):
|
|||||||
)
|
)
|
||||||
return modimpls
|
return modimpls
|
||||||
|
|
||||||
|
def modimpls_parcours(self, parcours: ApcParcours) -> list[ModuleImpl]:
|
||||||
|
"""Liste des modimpls du semestre (sans les bonus (?)) dans le parcours donné.
|
||||||
|
- triée par type/numéro/code ??
|
||||||
|
"""
|
||||||
|
cursor = db.session.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT modimpl.id
|
||||||
|
FROM notes_moduleimpl modimpl, notes_modules mod,
|
||||||
|
parcours_modules pm, parcours_formsemestre pf
|
||||||
|
WHERE modimpl.formsemestre_id = :formsemestre_id
|
||||||
|
AND modimpl.module_id = mod.id
|
||||||
|
AND pm.module_id = mod.id
|
||||||
|
AND pm.parcours_id = pf.parcours_id
|
||||||
|
AND pf.parcours_id = :parcours_id
|
||||||
|
AND pf.formsemestre_id = :formsemestre_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"formsemestre_id": self.id, "parcours_id": parcours.id},
|
||||||
|
)
|
||||||
|
return [ModuleImpl.query.get(modimpl_id) for modimpl_id in cursor]
|
||||||
|
|
||||||
def can_be_edited_by(self, user):
|
def can_be_edited_by(self, user):
|
||||||
"""Vrai si user peut modifier ce semestre"""
|
"""Vrai si user peut modifier ce semestre"""
|
||||||
if not user.has_permission(Permission.ScoImplement): # pas chef
|
if not user.has_permission(Permission.ScoImplement): # pas chef
|
||||||
@ -311,6 +371,25 @@ class FormSemestre(db.Model):
|
|||||||
return ""
|
return ""
|
||||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||||
|
|
||||||
|
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||||
|
"""Calcule la liste des regroupements cohérents d'UE impliquant ce
|
||||||
|
formsemestre.
|
||||||
|
Pour une année donnée: l'étudiant est inscrit dans ScoDoc soit dans le semestre
|
||||||
|
impair, soit pair, soit les deux (il est rare mais pas impossible d'avoir une
|
||||||
|
inscription seulement en semestre pair, par exemple suite à un transfert ou un
|
||||||
|
arrêt temporaire du cursus).
|
||||||
|
|
||||||
|
1. Déterminer l'*autre* formsemestre: semestre précédent ou suivant de la même
|
||||||
|
année, formation compatible (même référentiel de compétence) dans lequel
|
||||||
|
l'étudiant est inscrit.
|
||||||
|
|
||||||
|
2. Construire les couples d'UE (regroupements cohérents): apparier les UE qui
|
||||||
|
ont le même `ApcParcoursNiveauCompetence`.
|
||||||
|
"""
|
||||||
|
if not self.formation.is_apc():
|
||||||
|
return []
|
||||||
|
raise NotImplementedError() # XXX
|
||||||
|
|
||||||
def responsables_str(self, abbrev_prenom=True) -> str:
|
def responsables_str(self, abbrev_prenom=True) -> str:
|
||||||
"""chaîne "J. Dupond, X. Martin"
|
"""chaîne "J. Dupond, X. Martin"
|
||||||
ou "Jacques Dupond, Xavier Martin"
|
ou "Jacques Dupond, Xavier Martin"
|
||||||
@ -327,6 +406,11 @@ class FormSemestre(db.Model):
|
|||||||
"True si l'user est l'un des responsables du semestre"
|
"True si l'user est l'un des responsables du semestre"
|
||||||
return user.id in [u.id for u in self.responsables]
|
return user.id in [u.id for u in self.responsables]
|
||||||
|
|
||||||
|
def annee_scolaire(self) -> int:
|
||||||
|
"""L'année de début de l'année scolaire.
|
||||||
|
Par exemple, 2022 si le semestre va de septebre 2022 à février 2023."""
|
||||||
|
return scu.annee_scolaire_debut(self.date_debut.year, self.date_debut.month)
|
||||||
|
|
||||||
def annee_scolaire_str(self):
|
def annee_scolaire_str(self):
|
||||||
"2021 - 2022"
|
"2021 - 2022"
|
||||||
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
|
||||||
@ -425,6 +509,19 @@ class FormSemestre(db.Model):
|
|||||||
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
etudid, self.date_debut.isoformat(), self.date_fin.isoformat()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_codes_apogee(self, category=None) -> set[str]:
|
||||||
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
|
||||||
|
category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel
|
||||||
|
"""
|
||||||
|
codes = set()
|
||||||
|
if category is None or category == "etapes":
|
||||||
|
codes |= {e.etape_apo for e in self.etapes if e}
|
||||||
|
if (category is None or category == "sem") and self.elt_sem_apo:
|
||||||
|
codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x}
|
||||||
|
if (category is None or category == "annee") and self.elt_annee_apo:
|
||||||
|
codes |= {x.strip() for x in self.elt_annee_apo.split(",") if x}
|
||||||
|
return codes
|
||||||
|
|
||||||
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||||
"""Liste des étudiants inscrits à ce semestre
|
"""Liste des étudiants inscrits à ce semestre
|
||||||
Si include_demdef, tous les étudiants, avec les démissionnaires
|
Si include_demdef, tous les étudiants, avec les démissionnaires
|
||||||
@ -449,6 +546,85 @@ class FormSemestre(db.Model):
|
|||||||
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
"""Map { etudid : inscription } (incluant DEM et DEF)"""
|
||||||
return {ins.etud.id: ins for ins in self.inscriptions}
|
return {ins.etud.id: ins for ins in self.inscriptions}
|
||||||
|
|
||||||
|
def setup_parcours_groups(self) -> None:
|
||||||
|
"""Vérifie et créee si besoin la partition et les groupes de parcours BUT."""
|
||||||
|
if not self.formation.is_apc():
|
||||||
|
return
|
||||||
|
partition = Partition.query.filter_by(
|
||||||
|
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
|
||||||
|
).first()
|
||||||
|
if partition is None:
|
||||||
|
# Création de la partition de parcours
|
||||||
|
partition = Partition(
|
||||||
|
formsemestre_id=self.id,
|
||||||
|
partition_name=scu.PARTITION_PARCOURS,
|
||||||
|
numero=-1,
|
||||||
|
)
|
||||||
|
db.session.add(partition)
|
||||||
|
db.session.flush() # pour avoir un id
|
||||||
|
flash(f"Partition Parcours créée.")
|
||||||
|
|
||||||
|
for parcour in self.parcours:
|
||||||
|
if parcour.code:
|
||||||
|
group = GroupDescr.query.filter_by(
|
||||||
|
partition_id=partition.id, group_name=parcour.code
|
||||||
|
).first()
|
||||||
|
if not group:
|
||||||
|
partition.groups.append(GroupDescr(group_name=parcour.code))
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
def update_inscriptions_parcours_from_groups(self) -> None:
|
||||||
|
"""Met à jour les inscriptions dans les parcours du semestres en
|
||||||
|
fonction des groupes de parcours.
|
||||||
|
Les groupes de parcours sont ceux de la partition scu.PARTITION_PARCOURS
|
||||||
|
et leur nom est le code du parcours (eg "Cyber").
|
||||||
|
"""
|
||||||
|
partition = Partition.query.filter_by(
|
||||||
|
formsemestre_id=self.id, partition_name=scu.PARTITION_PARCOURS
|
||||||
|
).first()
|
||||||
|
if partition is None: # pas de partition de parcours
|
||||||
|
return
|
||||||
|
|
||||||
|
# Efface les inscriptions aux parcours:
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"""UPDATE notes_formsemestre_inscription
|
||||||
|
SET parcour_id=NULL
|
||||||
|
WHERE formsemestre_id=:formsemestre_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"formsemestre_id": self.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Inscrit les étudiants des groupes de parcours:
|
||||||
|
for group in partition.groups:
|
||||||
|
query = ApcParcours.query.filter_by(code=group.group_name)
|
||||||
|
if query.count() != 1:
|
||||||
|
log(
|
||||||
|
f"""update_inscriptions_parcours_from_groups: {
|
||||||
|
query.count()} parcours with code {group.group_name}"""
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
parcour = query.first()
|
||||||
|
db.session.execute(
|
||||||
|
text(
|
||||||
|
"""UPDATE notes_formsemestre_inscription ins
|
||||||
|
SET parcour_id=:parcour_id
|
||||||
|
FROM group_membership gm
|
||||||
|
WHERE formsemestre_id=:formsemestre_id
|
||||||
|
AND gm.etudid = ins.etudid
|
||||||
|
AND gm.group_id = :group_id
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"formsemestre_id": self.id,
|
||||||
|
"parcour_id": parcour.id,
|
||||||
|
"group_id": group.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
# Association id des utilisateurs responsables (aka directeurs des etudes) du semestre
|
||||||
notes_formsemestre_responsables = db.Table(
|
notes_formsemestre_responsables = db.Table(
|
||||||
@ -607,7 +783,9 @@ class FormSemestreInscription(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
formsemestre_inscription_id = db.synonym("id")
|
formsemestre_inscription_id = db.synonym("id")
|
||||||
|
|
||||||
etudid = db.Column(db.Integer, db.ForeignKey("identite.id"), index=True)
|
etudid = db.Column(
|
||||||
|
db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"), index=True
|
||||||
|
)
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
@ -627,11 +805,16 @@ class FormSemestreInscription(db.Model):
|
|||||||
)
|
)
|
||||||
# I inscrit, D demission en cours de semestre, DEF si "defaillant"
|
# I inscrit, D demission en cours de semestre, DEF si "defaillant"
|
||||||
etat = db.Column(db.String(CODE_STR_LEN), index=True)
|
etat = db.Column(db.String(CODE_STR_LEN), index=True)
|
||||||
# etape apogee d'inscription (experimental 2020)
|
# Etape Apogée d'inscription (ajout 2020)
|
||||||
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
etape = db.Column(db.String(APO_CODE_STR_LEN))
|
||||||
|
# Parcours (pour les BUT)
|
||||||
|
parcour_id = db.Column(db.Integer, db.ForeignKey("apc_parcours.id"), index=True)
|
||||||
|
parcour = db.relationship(ApcParcours)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={self.formsemestre_id} etat={self.etat}>"
|
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
|
||||||
|
self.formsemestre_id} etat={self.etat} {
|
||||||
|
('parcours='+str(self.parcour)) if self.parcour else ''}>"""
|
||||||
|
|
||||||
|
|
||||||
class NotesSemSet(db.Model):
|
class NotesSemSet(db.Model):
|
||||||
|
@ -23,7 +23,7 @@ class Partition(db.Model):
|
|||||||
)
|
)
|
||||||
# "TD", "TP", ... (NULL for 'all')
|
# "TD", "TP", ... (NULL for 'all')
|
||||||
partition_name = db.Column(db.String(SHORT_STR_LEN))
|
partition_name = db.Column(db.String(SHORT_STR_LEN))
|
||||||
# numero = ordre de presentation)
|
# Numero = ordre de presentation)
|
||||||
numero = db.Column(db.Integer)
|
numero = db.Column(db.Integer)
|
||||||
# Calculer le rang ?
|
# Calculer le rang ?
|
||||||
bul_show_rank = db.Column(
|
bul_show_rank = db.Column(
|
||||||
@ -33,6 +33,10 @@ class Partition(db.Model):
|
|||||||
show_in_lists = db.Column(
|
show_in_lists = db.Column(
|
||||||
db.Boolean(), nullable=False, default=True, server_default="true"
|
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||||
)
|
)
|
||||||
|
# Editable ? (faux pour les groupes de parcours)
|
||||||
|
groups_editable = db.Column(
|
||||||
|
db.Boolean(), nullable=False, default=True, server_default="true"
|
||||||
|
)
|
||||||
groups = db.relationship(
|
groups = db.relationship(
|
||||||
"GroupDescr",
|
"GroupDescr",
|
||||||
backref=db.backref("partition", lazy=True),
|
backref=db.backref("partition", lazy=True),
|
||||||
@ -106,7 +110,7 @@ class GroupDescr(db.Model):
|
|||||||
|
|
||||||
group_membership = db.Table(
|
group_membership = db.Table(
|
||||||
"group_membership",
|
"group_membership",
|
||||||
db.Column("etudid", db.Integer, db.ForeignKey("identite.id")),
|
db.Column("etudid", db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")),
|
||||||
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
|
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
|
||||||
db.UniqueConstraint("etudid", "group_id"),
|
db.UniqueConstraint("etudid", "group_id"),
|
||||||
)
|
)
|
||||||
@ -116,5 +120,5 @@ group_membership = db.Table(
|
|||||||
# __tablename__ = "group_membership"
|
# __tablename__ = "group_membership"
|
||||||
# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),)
|
# __table_args__ = (db.UniqueConstraint("etudid", "group_id"),)
|
||||||
# id = db.Column(db.Integer, primary_key=True)
|
# id = db.Column(db.Integer, primary_key=True)
|
||||||
# etudid = db.Column(db.Integer, db.ForeignKey("identite.id"))
|
# etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"))
|
||||||
# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id"))
|
# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id"))
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import APO_CODE_STR_LEN
|
from app.models import APO_CODE_STR_LEN
|
||||||
|
from app.models.but_refcomp import app_critiques_modules, parcours_modules
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
from app.scodoc.sco_codes_parcours import UE_SPORT
|
from app.scodoc.sco_codes_parcours import UE_SPORT
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
@ -44,13 +45,27 @@ class Module(db.Model):
|
|||||||
lazy=True,
|
lazy=True,
|
||||||
backref=db.backref("modules", lazy=True),
|
backref=db.backref("modules", lazy=True),
|
||||||
)
|
)
|
||||||
|
# BUT
|
||||||
|
parcours = db.relationship(
|
||||||
|
"ApcParcours",
|
||||||
|
secondary=parcours_modules,
|
||||||
|
lazy="subquery",
|
||||||
|
backref=db.backref("modules", lazy=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
app_critiques = db.relationship(
|
||||||
|
"ApcAppCritique",
|
||||||
|
secondary=app_critiques_modules,
|
||||||
|
lazy="subquery",
|
||||||
|
backref=db.backref("modules", lazy=True),
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.ue_coefs = []
|
self.ue_coefs = []
|
||||||
super(Module, self).__init__(**kwargs)
|
super(Module, self).__init__(**kwargs)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>"
|
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code!r}>"
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
e = dict(self.__dict__)
|
e = dict(self.__dict__)
|
||||||
@ -160,6 +175,12 @@ class Module(db.Model):
|
|||||||
# Liste seulement les coefs définis:
|
# Liste seulement les coefs définis:
|
||||||
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
return [(c.ue, c.coef) for c in self.get_ue_coefs_sorted()]
|
||||||
|
|
||||||
|
def get_codes_apogee(self) -> set[str]:
|
||||||
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||||
|
if self.code_apogee:
|
||||||
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
class ModuleUECoef(db.Model):
|
class ModuleUECoef(db.Model):
|
||||||
"""Coefficients des modules vers les UE (APC, BUT)
|
"""Coefficients des modules vers les UE (APC, BUT)
|
||||||
|
@ -17,7 +17,7 @@ class BulAppreciations(db.Model):
|
|||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
@ -36,7 +36,7 @@ class NotesNotes(db.Model):
|
|||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
evaluation_id = db.Column(
|
evaluation_id = db.Column(
|
||||||
db.Integer, db.ForeignKey("notes_evaluation.id"), index=True
|
db.Integer, db.ForeignKey("notes_evaluation.id"), index=True
|
||||||
@ -75,7 +75,7 @@ class NotesNotesLog(db.Model):
|
|||||||
|
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
evaluation_id = db.Column(
|
evaluation_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
|
@ -40,8 +40,15 @@ class UniteEns(db.Model):
|
|||||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||||
coefficient = db.Column(db.Float)
|
coefficient = db.Column(db.Float)
|
||||||
|
|
||||||
|
# coef. pour le calcul de moyennes de RCUE. Par défaut, 1.
|
||||||
|
coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0")
|
||||||
|
|
||||||
color = db.Column(db.Text())
|
color = db.Column(db.Text())
|
||||||
|
|
||||||
|
# BUT
|
||||||
|
niveau_competence_id = db.Column(db.Integer, db.ForeignKey("apc_niveau.id"))
|
||||||
|
niveau_competence = db.relationship("ApcNiveau", back_populates="ues")
|
||||||
|
|
||||||
# relations
|
# relations
|
||||||
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
matieres = db.relationship("Matiere", lazy="dynamic", backref="ue")
|
||||||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||||
@ -113,3 +120,9 @@ class UniteEns(db.Model):
|
|||||||
(Module.module_type != scu.ModuleType.SAE),
|
(Module.module_type != scu.ModuleType.SAE),
|
||||||
(Module.module_type != scu.ModuleType.RESSOURCE),
|
(Module.module_type != scu.ModuleType.RESSOURCE),
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
def get_codes_apogee(self) -> set[str]:
|
||||||
|
"""Les codes Apogée (codés en base comme "VRT1,VRT2")"""
|
||||||
|
if self.code_apogee:
|
||||||
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
|
return set()
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
from app import db
|
from app import db
|
||||||
from app.models import SHORT_STR_LEN
|
from app.models import SHORT_STR_LEN
|
||||||
from app.models import CODE_STR_LEN
|
from app.models import CODE_STR_LEN
|
||||||
|
from app.models.events import Scolog
|
||||||
|
|
||||||
|
|
||||||
class ScolarFormSemestreValidation(db.Model):
|
class ScolarFormSemestreValidation(db.Model):
|
||||||
@ -19,7 +20,7 @@ class ScolarFormSemestreValidation(db.Model):
|
|||||||
formsemestre_validation_id = db.synonym("id")
|
formsemestre_validation_id = db.synonym("id")
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
@ -36,7 +37,7 @@ class ScolarFormSemestreValidation(db.Model):
|
|||||||
# NULL pour les UE, True|False pour les semestres:
|
# NULL pour les UE, True|False pour les semestres:
|
||||||
assidu = db.Column(db.Boolean)
|
assidu = db.Column(db.Boolean)
|
||||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
# NULL sauf si compense un semestre:
|
# NULL sauf si compense un semestre: (pas utilisé pour BUT)
|
||||||
compense_formsemestre_id = db.Column(
|
compense_formsemestre_id = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
@ -54,7 +55,7 @@ class ScolarFormSemestreValidation(db.Model):
|
|||||||
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
ue = db.relationship("UniteEns", lazy="select", uselist=False)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue_id}, moy_ue={self.moy_ue})"
|
return f"{self.__class__.__name__}({self.formsemestre_id}, {self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||||
|
|
||||||
|
|
||||||
class ScolarAutorisationInscription(db.Model):
|
class ScolarAutorisationInscription(db.Model):
|
||||||
@ -66,10 +67,10 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
|
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
|
formation_code = db.Column(db.String(SHORT_STR_LEN), nullable=False)
|
||||||
# semestre ou on peut s'inscrire:
|
# Indice du semestre où on peut s'inscrire:
|
||||||
semestre_id = db.Column(db.Integer)
|
semestre_id = db.Column(db.Integer)
|
||||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
origin_formsemestre_id = db.Column(
|
origin_formsemestre_id = db.Column(
|
||||||
@ -77,6 +78,44 @@ class ScolarAutorisationInscription(db.Model):
|
|||||||
db.ForeignKey("notes_formsemestre.id"),
|
db.ForeignKey("notes_formsemestre.id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def autorise_etud(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
formation_code: str,
|
||||||
|
origin_formsemestre_id: int,
|
||||||
|
semestre_id: int,
|
||||||
|
):
|
||||||
|
"""Enregistre une autorisation, remplace celle émanant du même semestre si elle existe."""
|
||||||
|
cls.delete_autorisation_etud(etudid, origin_formsemestre_id)
|
||||||
|
autorisation = cls(
|
||||||
|
etudid=etudid,
|
||||||
|
formation_code=formation_code,
|
||||||
|
origin_formsemestre_id=origin_formsemestre_id,
|
||||||
|
semestre_id=semestre_id,
|
||||||
|
)
|
||||||
|
db.session.add(autorisation)
|
||||||
|
Scolog.logdb("autorise_etud", etudid=etudid, msg=f"passage vers S{semestre_id}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_autorisation_etud(
|
||||||
|
cls,
|
||||||
|
etudid: int,
|
||||||
|
origin_formsemestre_id: int,
|
||||||
|
):
|
||||||
|
"""Efface les autorisations de cette étudiant venant du sem. origine"""
|
||||||
|
autorisations = cls.query.filter_by(
|
||||||
|
etudid=etudid, origin_formsemestre_id=origin_formsemestre_id
|
||||||
|
)
|
||||||
|
for autorisation in autorisations:
|
||||||
|
db.session.delete(autorisation)
|
||||||
|
Scolog.logdb(
|
||||||
|
"autorise_etud",
|
||||||
|
etudid=etudid,
|
||||||
|
msg=f"annule passage vers S{autorisation.semestre_id}",
|
||||||
|
)
|
||||||
|
db.session.flush()
|
||||||
|
|
||||||
|
|
||||||
class ScolarEvent(db.Model):
|
class ScolarEvent(db.Model):
|
||||||
"""Evenement dans le parcours scolaire d'un étudiant"""
|
"""Evenement dans le parcours scolaire d'un étudiant"""
|
||||||
@ -86,7 +125,7 @@ class ScolarEvent(db.Model):
|
|||||||
event_id = db.synonym("id")
|
event_id = db.synonym("id")
|
||||||
etudid = db.Column(
|
etudid = db.Column(
|
||||||
db.Integer,
|
db.Integer,
|
||||||
db.ForeignKey("identite.id"),
|
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||||
)
|
)
|
||||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||||
formsemestre_id = db.Column(
|
formsemestre_id = db.Column(
|
||||||
|
@ -207,12 +207,16 @@ class TF(object):
|
|||||||
else:
|
else:
|
||||||
self.values[field] = 1
|
self.values[field] = 1
|
||||||
if field not in self.values:
|
if field not in self.values:
|
||||||
if "default" in descr: # first: default in form description
|
if (descr.get("input_type", None) == "checkbox") and self.submitted():
|
||||||
self.values[field] = descr["default"]
|
# aucune case cochée
|
||||||
else: # then: use initvalues dict
|
self.values[field] = []
|
||||||
self.values[field] = self.initvalues.get(field, "")
|
else:
|
||||||
if self.values[field] == None:
|
if "default" in descr: # first: default in form description
|
||||||
self.values[field] = ""
|
self.values[field] = descr["default"]
|
||||||
|
else: # then: use initvalues dict
|
||||||
|
self.values[field] = self.initvalues.get(field, "")
|
||||||
|
if self.values[field] is None:
|
||||||
|
self.values[field] = ""
|
||||||
|
|
||||||
# convert numbers, except ids
|
# convert numbers, except ids
|
||||||
if field.endswith("id") and self.values[field]:
|
if field.endswith("id") and self.values[field]:
|
||||||
@ -392,9 +396,7 @@ class TF(object):
|
|||||||
if self.top_buttons:
|
if self.top_buttons:
|
||||||
R.append(buttons_markup + "<p></p>")
|
R.append(buttons_markup + "<p></p>")
|
||||||
R.append('<table class="tf">')
|
R.append('<table class="tf">')
|
||||||
idx = 0
|
for field, descr in self.formdescription:
|
||||||
for idx in range(len(self.formdescription)):
|
|
||||||
(field, descr) = self.formdescription[idx]
|
|
||||||
if descr.get("readonly", False):
|
if descr.get("readonly", False):
|
||||||
R.append(self._ReadOnlyElement(field, descr))
|
R.append(self._ReadOnlyElement(field, descr))
|
||||||
continue
|
continue
|
||||||
@ -408,7 +410,7 @@ class TF(object):
|
|||||||
input_type = descr.get("input_type", "text")
|
input_type = descr.get("input_type", "text")
|
||||||
item_dom_id = descr.get("dom_id", "")
|
item_dom_id = descr.get("dom_id", "")
|
||||||
if item_dom_id:
|
if item_dom_id:
|
||||||
item_dom_attr = ' id="%s"' % item_dom_id
|
item_dom_attr = f' id="{item_dom_id}"'
|
||||||
else:
|
else:
|
||||||
item_dom_attr = ""
|
item_dom_attr = ""
|
||||||
# choix du template
|
# choix du template
|
||||||
@ -523,7 +525,6 @@ class TF(object):
|
|||||||
else:
|
else:
|
||||||
checked = ""
|
checked = ""
|
||||||
else: # boolcheckbox
|
else: # boolcheckbox
|
||||||
# open('/tmp/toto','a').write('GenForm: values[%s] = %s (%s)\n' % (field, values[field], type(values[field])))
|
|
||||||
if values[field] == "True":
|
if values[field] == "True":
|
||||||
v = True
|
v = True
|
||||||
elif values[field] == "False":
|
elif values[field] == "False":
|
||||||
|
@ -45,7 +45,7 @@ import random
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
import json
|
import json
|
||||||
|
from openpyxl.utils import get_column_letter
|
||||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Frame, PageBreak
|
||||||
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
|
||||||
from reportlab.lib.colors import Color
|
from reportlab.lib.colors import Color
|
||||||
@ -127,6 +127,8 @@ class GenTable(object):
|
|||||||
filename="table", # filename, without extension
|
filename="table", # filename, without extension
|
||||||
xls_sheet_name="feuille",
|
xls_sheet_name="feuille",
|
||||||
xls_before_table=[], # liste de cellules a placer avant la table
|
xls_before_table=[], # liste de cellules a placer avant la table
|
||||||
|
xls_style_base=None, # style excel pour les cellules
|
||||||
|
xls_columns_width=None, # { col_id : largeur en "pixels excel" }
|
||||||
pdf_title="", # au dessus du tableau en pdf
|
pdf_title="", # au dessus du tableau en pdf
|
||||||
pdf_table_style=None,
|
pdf_table_style=None,
|
||||||
pdf_col_widths=None,
|
pdf_col_widths=None,
|
||||||
@ -151,6 +153,8 @@ class GenTable(object):
|
|||||||
self.page_title = page_title
|
self.page_title = page_title
|
||||||
self.pdf_link = pdf_link
|
self.pdf_link = pdf_link
|
||||||
self.xls_link = xls_link
|
self.xls_link = xls_link
|
||||||
|
self.xls_style_base = xls_style_base
|
||||||
|
self.xls_columns_width = xls_columns_width or {}
|
||||||
self.xml_link = xml_link
|
self.xml_link = xml_link
|
||||||
# HTML parameters:
|
# HTML parameters:
|
||||||
if not table_id: # random id
|
if not table_id: # random id
|
||||||
@ -495,7 +499,8 @@ class GenTable(object):
|
|||||||
sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
sheet = wb.create_sheet(sheet_name=self.xls_sheet_name)
|
||||||
sheet.rows += self.xls_before_table
|
sheet.rows += self.xls_before_table
|
||||||
style_bold = sco_excel.excel_make_style(bold=True)
|
style_bold = sco_excel.excel_make_style(bold=True)
|
||||||
style_base = sco_excel.excel_make_style()
|
style_base = self.xls_style_base or sco_excel.excel_make_style()
|
||||||
|
|
||||||
sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
|
sheet.append_row(sheet.make_row(self.get_titles_list(), style_bold))
|
||||||
for line in self.get_data_list(xls_mode=True):
|
for line in self.get_data_list(xls_mode=True):
|
||||||
sheet.append_row(sheet.make_row(line, style_base))
|
sheet.append_row(sheet.make_row(line, style_base))
|
||||||
@ -505,6 +510,16 @@ class GenTable(object):
|
|||||||
if self.origin:
|
if self.origin:
|
||||||
sheet.append_blank_row() # empty line
|
sheet.append_blank_row() # empty line
|
||||||
sheet.append_single_cell_row(self.origin, style_base)
|
sheet.append_single_cell_row(self.origin, style_base)
|
||||||
|
# Largeurs des colonnes
|
||||||
|
columns_ids = list(self.columns_ids)
|
||||||
|
for col_id, width in self.xls_columns_width.items():
|
||||||
|
try:
|
||||||
|
idx = columns_ids.index(col_id)
|
||||||
|
col = get_column_letter(idx + 1)
|
||||||
|
sheet.set_column_dimension_width(col, width)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
if wb is None:
|
if wb is None:
|
||||||
return sheet.generate()
|
return sheet.generate()
|
||||||
|
|
||||||
|
@ -59,35 +59,29 @@ BOOTSTRAP_MULTISELECT_CSS = [
|
|||||||
def standard_html_header():
|
def standard_html_header():
|
||||||
"""Standard HTML header for pages outside depts"""
|
"""Standard HTML header for pages outside depts"""
|
||||||
# not used in ZScolar, see sco_header
|
# not used in ZScolar, see sco_header
|
||||||
return """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
return f"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
||||||
<html><head>
|
<html><head>
|
||||||
<title>ScoDoc: accueil</title>
|
<title>ScoDoc: accueil</title>
|
||||||
<META http-equiv="Content-Type" content="text/html; charset=%s">
|
<META http-equiv="Content-Type" content="text/html; charset={scu.SCO_ENCODING}">
|
||||||
<META http-equiv="Content-Style-Type" content="text/css">
|
<META http-equiv="Content-Style-Type" content="text/css">
|
||||||
<META name="LANG" content="fr">
|
<META name="LANG" content="fr">
|
||||||
<META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
|
<META name="DESCRIPTION" content="ScoDoc: gestion scolarite">
|
||||||
|
|
||||||
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css"/>
|
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css"/>
|
||||||
|
|
||||||
</head><body>%s""" % (
|
</head><body>{scu.CUSTOM_HTML_HEADER_CNX}"""
|
||||||
scu.SCO_ENCODING,
|
|
||||||
scu.CUSTOM_HTML_HEADER_CNX,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def standard_html_footer():
|
def standard_html_footer():
|
||||||
"""Le pied de page HTML de la page d'accueil."""
|
"""Le pied de page HTML de la page d'accueil."""
|
||||||
return """<p class="footer">
|
return f"""<p class="footer">
|
||||||
Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
|
Problème de connexion (identifiant, mot de passe): <em>contacter votre responsable ou chef de département</em>.</p>
|
||||||
<p>Problèmes et suggestions sur le logiciel: <a href="mailto:%s">%s</a></p>
|
<p>Problèmes et suggestions sur le logiciel: <a href="mailto:{scu.SCO_USERS_LIST}">{scu.SCO_USERS_LIST}</a></p>
|
||||||
<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
|
<p><em>ScoDoc est un logiciel libre développé par Emmanuel Viennet.</em></p>
|
||||||
</body></html>""" % (
|
</body></html>"""
|
||||||
scu.SCO_USERS_LIST,
|
|
||||||
scu.SCO_USERS_LIST,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
_HTML_BEGIN = """<!DOCTYPE html>
|
_HTML_BEGIN = f"""<!DOCTYPE html>
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
@ -100,27 +94,27 @@ _HTML_BEGIN = """<!DOCTYPE html>
|
|||||||
<meta name="DESCRIPTION" content="ScoDoc" />
|
<meta name="DESCRIPTION" content="ScoDoc" />
|
||||||
<title>%(page_title)s</title>
|
<title>%(page_title)s</title>
|
||||||
|
|
||||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
||||||
|
|
||||||
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
|
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
|
||||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload=function(){enableTooltips("gtrcontent")};
|
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||||
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
<script src="{scu.STATIC_DIR}/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
|
||||||
|
|
||||||
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||||
|
|
||||||
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||||
|
|
||||||
<script src="/ScoDoc/static/js/scodoc.js"></script>
|
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
|
||||||
<script src="/ScoDoc/static/js/etud_info.js"></script>
|
<script src="{scu.STATIC_DIR}/js/etud_info.js"></script>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -138,9 +132,9 @@ def sco_header(
|
|||||||
# optional args
|
# optional args
|
||||||
page_title="", # page title
|
page_title="", # page title
|
||||||
no_side_bar=False, # hide sidebar
|
no_side_bar=False, # hide sidebar
|
||||||
cssstyles=[], # additionals CSS sheets
|
cssstyles=(), # additionals CSS sheets
|
||||||
javascripts=[], # additionals JS filenames to load
|
javascripts=(), # additionals JS filenames to load
|
||||||
scripts=[], # script to put in page header
|
scripts=(), # script to put in page header
|
||||||
bodyOnLoad="", # JS
|
bodyOnLoad="", # JS
|
||||||
init_qtip=False, # include qTip
|
init_qtip=False, # include qTip
|
||||||
init_google_maps=False, # Google maps
|
init_google_maps=False, # Google maps
|
||||||
@ -148,6 +142,8 @@ def sco_header(
|
|||||||
titrebandeau="", # titre dans bandeau superieur
|
titrebandeau="", # titre dans bandeau superieur
|
||||||
head_message="", # message action (petit cadre jaune en haut)
|
head_message="", # message action (petit cadre jaune en haut)
|
||||||
user_check=True, # verifie passwords temporaires
|
user_check=True, # verifie passwords temporaires
|
||||||
|
etudid=None,
|
||||||
|
formsemestre_id=None,
|
||||||
):
|
):
|
||||||
"Main HTML page header for ScoDoc"
|
"Main HTML page header for ScoDoc"
|
||||||
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
||||||
@ -191,7 +187,7 @@ def sco_header(
|
|||||||
# jQuery UI
|
# jQuery UI
|
||||||
# can modify loaded theme here
|
# can modify loaded theme here
|
||||||
H.append(
|
H.append(
|
||||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||||
)
|
)
|
||||||
if init_google_maps:
|
if init_google_maps:
|
||||||
# It may be necessary to add an API key:
|
# It may be necessary to add an API key:
|
||||||
@ -200,72 +196,65 @@ def sco_header(
|
|||||||
# Feuilles de style additionnelles:
|
# Feuilles de style additionnelles:
|
||||||
for cssstyle in cssstyles:
|
for cssstyle in cssstyles:
|
||||||
H.append(
|
H.append(
|
||||||
"""<link type="text/css" rel="stylesheet" href="/ScoDoc/static/%s" />\n"""
|
f"""<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/{cssstyle}" />\n"""
|
||||||
% cssstyle
|
|
||||||
)
|
)
|
||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
"""
|
f"""
|
||||||
<link href="/ScoDoc/static/css/scodoc.css" rel="stylesheet" type="text/css" />
|
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" />
|
<link href="{scu.STATIC_DIR}/css/gt_table.css" rel="stylesheet" type="text/css" />
|
||||||
|
|
||||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload=function(){enableTooltips("gtrcontent")};
|
window.onload=function(){{enableTooltips("gtrcontent")}};
|
||||||
|
|
||||||
var SCO_URL="%(ScoURL)s";
|
var SCO_URL="{scu.ScoURL()}";
|
||||||
</script>"""
|
</script>"""
|
||||||
% params
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# jQuery
|
# jQuery
|
||||||
H.append(
|
H.append(
|
||||||
"""<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||||
"""
|
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
|
||||||
)
|
)
|
||||||
H.append('<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>')
|
|
||||||
# qTip
|
# qTip
|
||||||
if init_qtip:
|
if init_qtip:
|
||||||
H.append(
|
H.append(
|
||||||
'<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>'
|
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||||
)
|
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />"""
|
||||||
H.append(
|
|
||||||
'<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
'<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>'
|
f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||||
|
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>"""
|
||||||
)
|
)
|
||||||
|
|
||||||
H.append('<script src="/ScoDoc/static/js/scodoc.js"></script>')
|
|
||||||
if init_google_maps:
|
if init_google_maps:
|
||||||
H.append(
|
H.append(
|
||||||
'<script src="/ScoDoc/static/libjs/jquery.ui.map.full.min.js"></script>'
|
f'<script src="{scu.STATIC_DIR}/libjs/jquery.ui.map.full.min.js"></script>'
|
||||||
)
|
)
|
||||||
if init_datatables:
|
if init_datatables:
|
||||||
H.append(
|
H.append(
|
||||||
'<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css"/>'
|
f"""<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css"/>
|
||||||
|
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>"""
|
||||||
)
|
)
|
||||||
H.append('<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>')
|
|
||||||
# H.append(
|
# H.append(
|
||||||
# '<link href="/ScoDoc/static/css/tooltip.css" rel="stylesheet" type="text/css" />'
|
# f'<link href="{scu.STATIC_DIR}/css/tooltip.css" rel="stylesheet" type="text/css" />'
|
||||||
# )
|
# )
|
||||||
# JS additionels
|
# JS additionels
|
||||||
for js in javascripts:
|
for js in javascripts:
|
||||||
H.append("""<script src="/ScoDoc/static/%s"></script>\n""" % js)
|
H.append(f"""<script src="{scu.STATIC_DIR}/{js}"></script>\n""")
|
||||||
|
|
||||||
H.append(
|
H.append(
|
||||||
"""<style>
|
f"""<style>
|
||||||
#gtrcontent {
|
#gtrcontent {{
|
||||||
margin-left: %(margin_left)s;
|
margin-left: {params["margin_left"]};
|
||||||
height: 100%%;
|
height: 100%%;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}}
|
||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
% params
|
|
||||||
)
|
)
|
||||||
# Scripts de la page:
|
# Scripts de la page:
|
||||||
if scripts:
|
if scripts:
|
||||||
@ -281,25 +270,24 @@ def sco_header(
|
|||||||
H.append(scu.CUSTOM_HTML_HEADER)
|
H.append(scu.CUSTOM_HTML_HEADER)
|
||||||
#
|
#
|
||||||
if not no_side_bar:
|
if not no_side_bar:
|
||||||
H.append(html_sidebar.sidebar())
|
H.append(html_sidebar.sidebar(etudid))
|
||||||
H.append("""<div id="gtrcontent">""")
|
H.append("""<div id="gtrcontent">""")
|
||||||
# En attendant le replacement complet de cette fonction,
|
# En attendant le replacement complet de cette fonction,
|
||||||
# inclusion ici des messages flask
|
# inclusion ici des messages flask
|
||||||
H.append(render_template("flashed_messages.html"))
|
H.append(render_template("flashed_messages.html"))
|
||||||
#
|
#
|
||||||
# Barre menu semestre:
|
# Barre menu semestre:
|
||||||
H.append(formsemestre_page_title())
|
H.append(formsemestre_page_title(formsemestre_id))
|
||||||
|
|
||||||
# Avertissement si mot de passe à changer
|
# Avertissement si mot de passe à changer
|
||||||
if user_check:
|
if user_check:
|
||||||
if current_user.passwd_temp:
|
if current_user.passwd_temp:
|
||||||
H.append(
|
H.append(
|
||||||
"""<div class="passwd_warn">
|
f"""<div class="passwd_warn">
|
||||||
Attention !<br/>
|
Attention !<br/>
|
||||||
Vous avez reçu un mot de passe temporaire.<br/>
|
Vous avez reçu un mot de passe temporaire.<br/>
|
||||||
Vous devez le changer: <a href="%s/form_change_password?user_name=%s">cliquez ici</a>
|
Vous devez le changer: <a href="{scu.UsersURL}/form_change_password?user_name={current_user.user_name}">cliquez ici</a>
|
||||||
</div>"""
|
</div>"""
|
||||||
% (scu.UsersURL, current_user.user_name)
|
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
if head_message:
|
if head_message:
|
||||||
@ -328,6 +316,6 @@ def html_sem_header(
|
|||||||
else:
|
else:
|
||||||
h = ""
|
h = ""
|
||||||
if with_h2:
|
if with_h2:
|
||||||
return h + """<h2 class="formsemestre">%s</h2>""" % (title)
|
return h + f"""<h2 class="formsemestre">{title}</h2>"""
|
||||||
else:
|
else:
|
||||||
return h
|
return h
|
||||||
|
@ -73,7 +73,7 @@ def sidebar_common():
|
|||||||
return "".join(H)
|
return "".join(H)
|
||||||
|
|
||||||
|
|
||||||
def sidebar():
|
def sidebar(etudid: int = None):
|
||||||
"Main HTML page sidebar"
|
"Main HTML page sidebar"
|
||||||
# rewritten from legacy DTML code
|
# rewritten from legacy DTML code
|
||||||
from app.scodoc import sco_abs
|
from app.scodoc import sco_abs
|
||||||
@ -93,14 +93,14 @@ def sidebar():
|
|||||||
"""
|
"""
|
||||||
]
|
]
|
||||||
# ---- Il y-a-t-il un etudiant selectionné ?
|
# ---- Il y-a-t-il un etudiant selectionné ?
|
||||||
etudid = g.get("etudid", None)
|
etudid = etudid if etudid is not None else g.get("etudid", None)
|
||||||
if not etudid:
|
if etudid is None:
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
etudid = request.args.get("etudid", None)
|
etudid = request.args.get("etudid", None)
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
etudid = request.form.get("etudid", None)
|
etudid = request.form.get("etudid", None)
|
||||||
|
|
||||||
if etudid:
|
if etudid is not None:
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||||
params.update(etud)
|
params.update(etud)
|
||||||
params["fiche_url"] = url_for(
|
params["fiche_url"] = url_for(
|
||||||
|
@ -258,11 +258,16 @@ class ApoEtud(dict):
|
|||||||
self["nom"] = nom
|
self["nom"] = nom
|
||||||
self["prenom"] = prenom
|
self["prenom"] = prenom
|
||||||
self["naissance"] = naissance
|
self["naissance"] = naissance
|
||||||
self.cols = cols # { col_id : value } colid = 'apoL_c0001'
|
self.cols = cols
|
||||||
|
"{ col_id : value } colid = 'apoL_c0001'"
|
||||||
|
self.col_elts = {}
|
||||||
|
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
|
||||||
self.new_cols = {} # { col_id : value to record in csv }
|
self.new_cols = {} # { col_id : value to record in csv }
|
||||||
self.etud = None # etud ScoDoc
|
self.etud: Identite = None
|
||||||
|
"etudiant ScoDoc associé"
|
||||||
self.etat = None # ETUD_OK, ...
|
self.etat = None # ETUD_OK, ...
|
||||||
self.is_NAR = False # set to True si NARé dans un semestre
|
self.is_NAR = False
|
||||||
|
"True si NARé dans un semestre"
|
||||||
self.log = []
|
self.log = []
|
||||||
self.has_logged_no_decision = False
|
self.has_logged_no_decision = False
|
||||||
self.export_res_etape = export_res_etape # VET, ...
|
self.export_res_etape = export_res_etape # VET, ...
|
||||||
@ -276,7 +281,7 @@ class ApoEtud(dict):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "ApoEtud( nom='%s', nip='%s' )" % (self["nom"], self["nip"])
|
return f"""ApoEtud( nom='{self["nom"]}', nip='{self["nip"]}' )"""
|
||||||
|
|
||||||
def lookup_scodoc(self, etape_formsemestre_ids):
|
def lookup_scodoc(self, etape_formsemestre_ids):
|
||||||
"""Cherche l'étudiant ScoDoc associé à cet étudiant Apogée.
|
"""Cherche l'étudiant ScoDoc associé à cet étudiant Apogée.
|
||||||
@ -284,6 +289,10 @@ class ApoEtud(dict):
|
|||||||
met .etud à None.
|
met .etud à None.
|
||||||
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
|
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# futur: #WIP
|
||||||
|
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"]).first()
|
||||||
|
# self.etud = etud
|
||||||
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
|
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
|
||||||
if not etuds:
|
if not etuds:
|
||||||
# pas dans ScoDoc
|
# pas dans ScoDoc
|
||||||
@ -291,13 +300,16 @@ class ApoEtud(dict):
|
|||||||
self.log.append("non inscrit dans ScoDoc")
|
self.log.append("non inscrit dans ScoDoc")
|
||||||
self.etat = ETUD_ORPHELIN
|
self.etat = ETUD_ORPHELIN
|
||||||
else:
|
else:
|
||||||
|
# futur: #WIP
|
||||||
|
# formsemestre_ids = {
|
||||||
|
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
|
||||||
|
# }
|
||||||
|
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||||
self.etud = etuds[0]
|
self.etud = etuds[0]
|
||||||
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
|
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
|
||||||
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
|
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
|
||||||
self.in_formsemestre_ids = formsemestre_ids.intersection(
|
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||||
etape_formsemestre_ids
|
if not in_formsemestre_ids:
|
||||||
)
|
|
||||||
if not self.in_formsemestre_ids:
|
|
||||||
self.log.append(
|
self.log.append(
|
||||||
"connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape"
|
"connu dans ScoDoc, mais pas inscrit dans un semestre de cette étape"
|
||||||
)
|
)
|
||||||
@ -305,7 +317,7 @@ class ApoEtud(dict):
|
|||||||
else:
|
else:
|
||||||
self.etat = ETUD_OK
|
self.etat = ETUD_OK
|
||||||
|
|
||||||
def associate_sco(self, apo_data):
|
def associate_sco(self, apo_data: "ApoData"):
|
||||||
"""Recherche les valeurs des éléments Apogée pour cet étudiant
|
"""Recherche les valeurs des éléments Apogée pour cet étudiant
|
||||||
Set .new_cols
|
Set .new_cols
|
||||||
"""
|
"""
|
||||||
@ -327,7 +339,7 @@ class ApoEtud(dict):
|
|||||||
cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data)
|
cur_sem, autre_sem = self.etud_semestres_de_etape(apo_data)
|
||||||
for sem in apo_data.sems_etape:
|
for sem in apo_data.sems_etape:
|
||||||
el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem)
|
el = self.search_elt_in_sem(code, sem, cur_sem, autre_sem)
|
||||||
if el != None:
|
if el is not None:
|
||||||
sco_elts[code] = el
|
sco_elts[code] = el
|
||||||
break
|
break
|
||||||
self.col_elts[code] = el
|
self.col_elts[code] = el
|
||||||
@ -338,15 +350,15 @@ class ApoEtud(dict):
|
|||||||
self.new_cols[col_id] = sco_elts[code][
|
self.new_cols[col_id] = sco_elts[code][
|
||||||
apo_data.cols[col_id]["Type Rés."]
|
apo_data.cols[col_id]["Type Rés."]
|
||||||
]
|
]
|
||||||
except KeyError:
|
except KeyError as exc:
|
||||||
log(
|
log(
|
||||||
"associate_sco: missing key, etud=%s\ncode='%s'\netape='%s'"
|
f"associate_sco: missing key, etud={self}\ncode='{code}'\netape='{apo_data.etape_apogee}'"
|
||||||
% (self, code, apo_data.etape_apogee)
|
|
||||||
)
|
)
|
||||||
raise ScoValueError(
|
raise ScoValueError(
|
||||||
"""L'élément %s n'a pas de résultat: peut-être une erreur dans les codes sur le programme pédagogique (vérifier qu'il est bien associé à une UE ou semestre)?"""
|
f"""L'élément {code} n'a pas de résultat: peut-être une erreur
|
||||||
% code
|
dans les codes sur le programme pédagogique
|
||||||
)
|
(vérifier qu'il est bien associé à une UE ou semestre)?"""
|
||||||
|
) from exc
|
||||||
# recopie les 4 premieres colonnes (nom, ..., naissance):
|
# recopie les 4 premieres colonnes (nom, ..., naissance):
|
||||||
for col_id in apo_data.col_ids[:4]:
|
for col_id in apo_data.col_ids[:4]:
|
||||||
self.new_cols[col_id] = self.cols[col_id]
|
self.new_cols[col_id] = self.cols[col_id]
|
||||||
@ -356,7 +368,7 @@ class ApoEtud(dict):
|
|||||||
# codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids])
|
# codes = set([apo_data.cols[col_id].code for col_id in apo_data.col_ids])
|
||||||
# return codes - set(sco_elts)
|
# return codes - set(sco_elts)
|
||||||
|
|
||||||
def search_elt_in_sem(self, code, sem, cur_sem, autre_sem):
|
def search_elt_in_sem(self, code, sem, cur_sem, autre_sem) -> dict:
|
||||||
"""
|
"""
|
||||||
VET code jury etape
|
VET code jury etape
|
||||||
ELP élément pédagogique: UE, module
|
ELP élément pédagogique: UE, module
|
||||||
@ -820,10 +832,8 @@ class ApoData(object):
|
|||||||
elts[col["Code"]] = ApoElt([col])
|
elts[col["Code"]] = ApoElt([col])
|
||||||
return elts # { code apo : ApoElt }
|
return elts # { code apo : ApoElt }
|
||||||
|
|
||||||
def apo_read_etuds(self, f):
|
def apo_read_etuds(self, f) -> list[ApoEtud]:
|
||||||
"""Lecture des etudiants (et resultats) du fichier CSV Apogée
|
"""Lecture des etudiants (et resultats) du fichier CSV Apogée"""
|
||||||
-> liste de dicts
|
|
||||||
"""
|
|
||||||
L = []
|
L = []
|
||||||
while True:
|
while True:
|
||||||
line = f.readline()
|
line = f.readline()
|
||||||
@ -958,36 +968,38 @@ class ApoData(object):
|
|||||||
"""
|
"""
|
||||||
codes_by_sem = {}
|
codes_by_sem = {}
|
||||||
for sem in self.sems_etape:
|
for sem in self.sems_etape:
|
||||||
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(
|
||||||
|
sem["formsemestre_id"]
|
||||||
|
)
|
||||||
|
# L'ensemble des codes apo associés aux éléments:
|
||||||
|
codes_semestre = formsemestre.get_codes_apogee()
|
||||||
|
codes_modules = set().union(
|
||||||
|
*[
|
||||||
|
modimpl.module.get_codes_apogee()
|
||||||
|
for modimpl in formsemestre.modimpls
|
||||||
|
]
|
||||||
|
)
|
||||||
|
codes_ues = set().union(
|
||||||
|
*[
|
||||||
|
ue.get_codes_apogee()
|
||||||
|
for ue in formsemestre.query_ues(with_sport=True)
|
||||||
|
]
|
||||||
|
)
|
||||||
s = set()
|
s = set()
|
||||||
codes_by_sem[sem["formsemestre_id"]] = s
|
codes_by_sem[sem["formsemestre_id"]] = s
|
||||||
for col_id in self.col_ids[4:]:
|
for col_id in self.col_ids[4:]:
|
||||||
code = self.cols[col_id]["Code"] # 'V1RT'
|
code = self.cols[col_id]["Code"] # 'V1RT'
|
||||||
# associé à l'étape, l'année ou les semestre:
|
# associé à l'étape, l'année ou le semestre:
|
||||||
if (
|
if code in codes_semestre:
|
||||||
sco_formsemestre.sem_has_etape(sem, code)
|
|
||||||
or (code in {x.strip() for x in sem["elt_sem_apo"].split(",")})
|
|
||||||
or (code in {x.strip() for x in sem["elt_annee_apo"].split(",")})
|
|
||||||
):
|
|
||||||
s.add(code)
|
s.add(code)
|
||||||
continue
|
continue
|
||||||
# associé à une UE:
|
# associé à une UE:
|
||||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
if code in codes_ues:
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
s.add(code)
|
||||||
for ue in nt.get_ues_stat_dict():
|
continue
|
||||||
if ue["code_apogee"]:
|
|
||||||
codes = {x.strip() for x in ue["code_apogee"].split(",")}
|
|
||||||
if code in codes:
|
|
||||||
s.add(code)
|
|
||||||
continue
|
|
||||||
# associé à un module:
|
# associé à un module:
|
||||||
modimpls = nt.get_modimpls_dict()
|
if code in codes_modules:
|
||||||
for modimpl in modimpls:
|
s.add(code)
|
||||||
module = modimpl["module"]
|
|
||||||
if module["code_apogee"]:
|
|
||||||
codes = {x.strip() for x in module["code_apogee"].split(",")}
|
|
||||||
if code in codes:
|
|
||||||
s.add(code)
|
|
||||||
continue
|
|
||||||
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
|
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
|
||||||
return codes_by_sem
|
return codes_by_sem
|
||||||
|
|
||||||
|
@ -47,6 +47,7 @@
|
|||||||
qui est une description (humaine, format libre) de l'archive.
|
qui est une description (humaine, format libre) de l'archive.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import chardet
|
||||||
import datetime
|
import datetime
|
||||||
import glob
|
import glob
|
||||||
import json
|
import json
|
||||||
@ -55,7 +56,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import time
|
import time
|
||||||
import chardet
|
from typing import Union
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import g, request
|
from flask import g, request
|
||||||
@ -232,14 +233,17 @@ class BaseArchiver(object):
|
|||||||
os.mkdir(archive_id) # if exists, raises an OSError
|
os.mkdir(archive_id) # if exists, raises an OSError
|
||||||
finally:
|
finally:
|
||||||
scu.GSL.release()
|
scu.GSL.release()
|
||||||
self.store(archive_id, "_description.txt", description.encode("utf-8"))
|
self.store(archive_id, "_description.txt", description)
|
||||||
return archive_id
|
return archive_id
|
||||||
|
|
||||||
def store(self, archive_id: str, filename: str, data: bytes):
|
def store(self, archive_id: str, filename: str, data: Union[str, bytes]):
|
||||||
"""Store data in archive, under given filename.
|
"""Store data in archive, under given filename.
|
||||||
Filename may be modified (sanitized): return used filename
|
Filename may be modified (sanitized): return used filename
|
||||||
The file is created or replaced.
|
The file is created or replaced.
|
||||||
|
data may be str or bytes
|
||||||
"""
|
"""
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode(scu.SCO_ENCODING)
|
||||||
self.initialize()
|
self.initialize()
|
||||||
filename = scu.sanitize_filename(filename)
|
filename = scu.sanitize_filename(filename)
|
||||||
log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
|
log("storing %s (%d bytes) in %s" % (filename, len(data), archive_id))
|
||||||
@ -350,19 +354,21 @@ def do_formsemestre_archive(
|
|||||||
html_sco_header.sco_footer(),
|
html_sco_header.sco_footer(),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
data = data.encode(scu.SCO_ENCODING)
|
|
||||||
PVArchive.store(archive_id, "Tableau_moyennes.html", data)
|
PVArchive.store(archive_id, "Tableau_moyennes.html", data)
|
||||||
|
|
||||||
# Bulletins en JSON
|
# Bulletins en JSON
|
||||||
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
|
||||||
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
|
data_js = json.dumps(data, indent=1, cls=scu.ScoDocJSONEncoder)
|
||||||
data_js = data_js.encode(scu.SCO_ENCODING)
|
|
||||||
if data:
|
if data:
|
||||||
PVArchive.store(archive_id, "Bulletins.json", data_js)
|
PVArchive.store(archive_id, "Bulletins.json", data_js)
|
||||||
# Decisions de jury, en XLS
|
# Decisions de jury, en XLS
|
||||||
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
|
data = sco_pvjury.formsemestre_pvjury(formsemestre_id, format="xls", publish=False)
|
||||||
if data:
|
if data:
|
||||||
PVArchive.store(archive_id, "Decisions_Jury" + scu.XLSX_SUFFIX, data)
|
PVArchive.store(
|
||||||
|
archive_id,
|
||||||
|
"Decisions_Jury" + scu.XLSX_SUFFIX,
|
||||||
|
data,
|
||||||
|
)
|
||||||
# Classeur bulletins (PDF)
|
# Classeur bulletins (PDF)
|
||||||
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
|
||||||
formsemestre_id, version=bulVersion
|
formsemestre_id, version=bulVersion
|
||||||
|
@ -158,9 +158,24 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
|||||||
I["server_name"] = request.url_root
|
I["server_name"] = request.url_root
|
||||||
|
|
||||||
# Formation et parcours
|
# Formation et parcours
|
||||||
I["formation"] = sco_formations.formation_list(
|
if I["sem"]["formation_id"]:
|
||||||
args={"formation_id": I["sem"]["formation_id"]}
|
I["formation"] = sco_formations.formation_list(
|
||||||
)[0]
|
args={"formation_id": I["sem"]["formation_id"]}
|
||||||
|
)[0]
|
||||||
|
else: # what's the fuck ?
|
||||||
|
I["formation"] = {
|
||||||
|
"acronyme": "?",
|
||||||
|
"code_specialite": "",
|
||||||
|
"dept_id": 1,
|
||||||
|
"formation_code": "?",
|
||||||
|
"formation_id": -1,
|
||||||
|
"id": -1,
|
||||||
|
"referentiel_competence_id": None,
|
||||||
|
"titre": "?",
|
||||||
|
"titre_officiel": "?",
|
||||||
|
"type_parcours": 0,
|
||||||
|
"version": 0,
|
||||||
|
}
|
||||||
I["parcours"] = sco_codes_parcours.get_parcours_from_code(
|
I["parcours"] = sco_codes_parcours.get_parcours_from_code(
|
||||||
I["formation"]["type_parcours"]
|
I["formation"]["type_parcours"]
|
||||||
)
|
)
|
||||||
|
@ -439,15 +439,15 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
|
|||||||
ects_txt = "-"
|
ects_txt = "-"
|
||||||
|
|
||||||
t = {
|
t = {
|
||||||
"titre": ue["acronyme"] + " " + ue["titre"],
|
"titre": ue["acronyme"] + " " + (ue["titre"] or ""),
|
||||||
"_titre_html": plusminus
|
"_titre_html": plusminus
|
||||||
+ ue["acronyme"]
|
+ (ue["acronyme"] or "")
|
||||||
+ " "
|
+ " "
|
||||||
+ ue["titre"]
|
+ (ue["titre"] or "")
|
||||||
+ ' <span class="bul_ue_descr">'
|
+ ' <span class="bul_ue_descr">'
|
||||||
+ ue["ue_descr_txt"]
|
+ (ue["ue_descr_txt"] or "")
|
||||||
+ "</span>",
|
+ "</span>",
|
||||||
"_titre_help": ue["ue_descr_txt"],
|
"_titre_help": ue["ue_descr_txt"] or "",
|
||||||
"_titre_colspan": 2,
|
"_titre_colspan": 2,
|
||||||
"module": ue_descr,
|
"module": ue_descr,
|
||||||
"note": ue["moy_ue_txt"],
|
"note": ue["moy_ue_txt"],
|
||||||
|
@ -67,6 +67,7 @@ class ScoDocCache:
|
|||||||
|
|
||||||
timeout = None # ttl, infinite by default
|
timeout = None # ttl, infinite by default
|
||||||
prefix = ""
|
prefix = ""
|
||||||
|
verbose = False # if true, verbose logging (debug)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_key(cls, oid):
|
def _get_key(cls, oid):
|
||||||
@ -87,7 +88,10 @@ class ScoDocCache:
|
|||||||
def set(cls, oid, value):
|
def set(cls, oid, value):
|
||||||
"""Store value"""
|
"""Store value"""
|
||||||
key = cls._get_key(oid)
|
key = cls._get_key(oid)
|
||||||
# log(f"CACHE key={key}, type={type(value)}, timeout={cls.timeout}")
|
if cls.verbose:
|
||||||
|
log(
|
||||||
|
f"{cls.__name__}.set key={key}, type={type(value).__name__}, timeout={cls.timeout}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
status = CACHE.set(key, value, timeout=cls.timeout)
|
status = CACHE.set(key, value, timeout=cls.timeout)
|
||||||
if not status:
|
if not status:
|
||||||
@ -101,11 +105,15 @@ class ScoDocCache:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def delete(cls, oid):
|
def delete(cls, oid):
|
||||||
"""Remove from cache"""
|
"""Remove from cache"""
|
||||||
|
# if cls.verbose:
|
||||||
|
# log(f"{cls.__name__}.delete({oid})")
|
||||||
CACHE.delete(cls._get_key(oid))
|
CACHE.delete(cls._get_key(oid))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_many(cls, oids):
|
def delete_many(cls, oids):
|
||||||
"""Remove multiple keys at once"""
|
"""Remove multiple keys at once"""
|
||||||
|
if cls.verbose:
|
||||||
|
log(f"{cls.__name__}.delete_many({oids})")
|
||||||
# delete_many seems bugged:
|
# delete_many seems bugged:
|
||||||
# CACHE.delete_many([cls._get_key(oid) for oid in oids])
|
# CACHE.delete_many([cls._get_key(oid) for oid in oids])
|
||||||
for oid in oids:
|
for oid in oids:
|
||||||
|
@ -35,7 +35,7 @@ from app import log
|
|||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
class CodesParcours(enum.IntEnum):
|
class CodesParcours(enum.IntEnum):
|
||||||
"""Codes numériques de sparcours, enregistrés en base
|
"""Codes numériques des parcours, enregistrés en base
|
||||||
dans notes_formations.type_parcours
|
dans notes_formations.type_parcours
|
||||||
Ne pas modifier.
|
Ne pas modifier.
|
||||||
"""
|
"""
|
||||||
@ -68,7 +68,8 @@ NOTES_TOLERANCE = 0.00499999999999 # si note >= (BARRE-TOLERANCE), considere ok
|
|||||||
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
|
# (permet d'eviter d'afficher 10.00 sous barre alors que la moyenne vaut 9.999)
|
||||||
|
|
||||||
# Barre sur moyenne générale utilisée pour compensations semestres:
|
# Barre sur moyenne générale utilisée pour compensations semestres:
|
||||||
NOTES_BARRE_GEN_COMPENSATION = 10.0 - NOTES_TOLERANCE
|
NOTES_BARRE_GEN = 10.0
|
||||||
|
NOTES_BARRE_GEN_COMPENSATION = NOTES_BARRE_GEN - NOTES_TOLERANCE
|
||||||
|
|
||||||
# ----------------------------------------------------------------
|
# ----------------------------------------------------------------
|
||||||
# Types d'UE:
|
# Types d'UE:
|
||||||
@ -114,6 +115,8 @@ UE_SEM_DEFAULT = 1000000 # indice semestre des UE sans modules
|
|||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Codes proposés par ADIUT / Apogee
|
# Codes proposés par ADIUT / Apogee
|
||||||
|
ABAN = "ABAN"
|
||||||
|
ABL = "ABL"
|
||||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||||
ADJ = "ADJ" # admis par le jury
|
ADJ = "ADJ" # admis par le jury
|
||||||
@ -122,10 +125,16 @@ ATJ = "ATJ" # pb assiduité: décision repoussée au semestre suivant
|
|||||||
ATB = "ATB"
|
ATB = "ATB"
|
||||||
AJ = "AJ"
|
AJ = "AJ"
|
||||||
CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
|
CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
|
||||||
NAR = "NAR"
|
|
||||||
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
|
|
||||||
DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
|
DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
|
||||||
DEM = "DEM"
|
DEM = "DEM"
|
||||||
|
EXCLU = "EXCLU"
|
||||||
|
JSD = "JSD" # jury tenu mais pas de code (Jury Sans Décision)
|
||||||
|
NAR = "NAR"
|
||||||
|
PASD = "PASD"
|
||||||
|
PAS1NCI = "PAS1NCI"
|
||||||
|
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
|
||||||
|
RED = "RED"
|
||||||
|
UEBSL = "UEBSL" # UE blanchie
|
||||||
|
|
||||||
# codes actions
|
# codes actions
|
||||||
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
|
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
|
||||||
@ -143,22 +152,34 @@ ALL = "ALL"
|
|||||||
|
|
||||||
# Explication des codes (de semestre ou d'UE)
|
# Explication des codes (de semestre ou d'UE)
|
||||||
CODES_EXPL = {
|
CODES_EXPL = {
|
||||||
|
ABAN: "Non évalué pour manque d’assiduité: non présentation des notes de l'étudiant au jury",
|
||||||
|
ABL: "Année blanche",
|
||||||
ADC: "Validé par compensation",
|
ADC: "Validé par compensation",
|
||||||
ADJ: "Validé par le Jury",
|
ADJ: "Validé par le Jury",
|
||||||
ADM: "Validé",
|
ADM: "Validé",
|
||||||
AJ: "Ajourné",
|
AJ: "Ajourné (ou UE/BC de BUT en attente pour problème de moyenne)",
|
||||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||||
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
||||||
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
|
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
|
||||||
CMP: "Code UE acquise car semestre acquis",
|
CMP: """Code UE acquise car semestre acquis, ou, en BUT, acquise par
|
||||||
DEF: "Défaillant",
|
compensation UE avec l’UE de même compétence et de même année (ECTS acquis).
|
||||||
NAR: "Échec, non autorisé à redoubler",
|
Utilisé aussi pour les blocs de compétences BUT (RCUE).
|
||||||
RAT: "En attente d'un rattrapage",
|
""",
|
||||||
|
DEF: "Défaillant, pas ou peu de notes par arrêt de la formation. Non évalué par manque assiduité.",
|
||||||
DEM: "Démission",
|
DEM: "Démission",
|
||||||
|
EXCLU: "Exclusion: décision réservée à des décisions disciplinaires",
|
||||||
|
NAR: "Non admis, réorientation, non autorisé à redoubler",
|
||||||
|
PASD: """Année BUT: non admis, mais passage de droit:
|
||||||
|
Passage en Année Supérieure de Droit (+ de 50% des UE VAL et RCUE Ajourné(s) >=8)
|
||||||
|
""",
|
||||||
|
PAS1NCI: """Année BUT: Non admis, mais passage par décision de jury:
|
||||||
|
Passage en Année Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8)
|
||||||
|
""",
|
||||||
|
RAT: "En attente d'un rattrapage",
|
||||||
|
RED: "Année: Ajourné, mais autorisé à redoubler",
|
||||||
|
UEBSL: "UE blanchie",
|
||||||
}
|
}
|
||||||
# Nota: ces explications sont personnalisables via le fichier
|
|
||||||
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
|
|
||||||
# variable: CONFIG.CODES_EXP
|
|
||||||
|
|
||||||
# Les codes de semestres:
|
# Les codes de semestres:
|
||||||
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
||||||
@ -167,7 +188,21 @@ CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
|||||||
|
|
||||||
CODES_SEM_REO = {NAR: 1} # reorientation
|
CODES_SEM_REO = {NAR: 1} # reorientation
|
||||||
|
|
||||||
CODES_UE_VALIDES = {ADM: True, CMP: True} # UE validée
|
CODES_UE_VALIDES = {ADM: True, CMP: True, ADJ: True} # UE validée
|
||||||
|
CODES_RCUE_VALIDES = CODES_UE_VALIDES # Niveau RCUE validé
|
||||||
|
# Pour le BUT:
|
||||||
|
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||||
|
CODES_RCUE = {ADM, AJ, CMP}
|
||||||
|
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||||
|
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||||
|
BUT_RCUE_SUFFISANT = 8.0 - NOTES_TOLERANCE
|
||||||
|
BUT_CODES_PASSAGE = {
|
||||||
|
ADM,
|
||||||
|
ADJ,
|
||||||
|
PASD,
|
||||||
|
PAS1NCI,
|
||||||
|
ATJ,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def code_semestre_validant(code: str) -> bool:
|
def code_semestre_validant(code: str) -> bool:
|
||||||
|
@ -76,7 +76,7 @@ def html_edit_formation_apc(
|
|||||||
ues_by_sem[semestre_idx] = formation.ues.filter_by(
|
ues_by_sem[semestre_idx] = formation.ues.filter_by(
|
||||||
semestre_idx=semestre_idx
|
semestre_idx=semestre_idx
|
||||||
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
).order_by(UniteEns.semestre_idx, UniteEns.numero, UniteEns.acronyme)
|
||||||
ects = [ue.ects for ue in ues_by_sem[semestre_idx]]
|
ects = [ue.ects for ue in ues_by_sem[semestre_idx] if ue.type != UE_SPORT]
|
||||||
if None in ects:
|
if None in ects:
|
||||||
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
ects_by_sem[semestre_idx] = '<span class="missing_ue_ects">manquant</span>'
|
||||||
else:
|
else:
|
||||||
@ -127,27 +127,33 @@ def html_edit_formation_apc(
|
|||||||
formation=formation,
|
formation=formation,
|
||||||
titre=f"Ressources du S{semestre_idx}",
|
titre=f"Ressources du S{semestre_idx}",
|
||||||
create_element_msg="créer une nouvelle ressource",
|
create_element_msg="créer une nouvelle ressource",
|
||||||
matiere_parent=matiere_parent,
|
# matiere_parent=matiere_parent,
|
||||||
modules=ressources_in_sem,
|
modules=ressources_in_sem,
|
||||||
module_type=ModuleType.RESSOURCE,
|
module_type=ModuleType.RESSOURCE,
|
||||||
editable=editable,
|
editable=editable,
|
||||||
tag_editable=tag_editable,
|
tag_editable=tag_editable,
|
||||||
icons=icons,
|
icons=icons,
|
||||||
scu=scu,
|
scu=scu,
|
||||||
),
|
semestre_id=semestre_idx,
|
||||||
|
)
|
||||||
|
if ues_by_sem[semestre_idx].count() > 0
|
||||||
|
else "",
|
||||||
render_template(
|
render_template(
|
||||||
"pn/form_mods.html",
|
"pn/form_mods.html",
|
||||||
formation=formation,
|
formation=formation,
|
||||||
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
titre=f"Situations d'Apprentissage et d'Évaluation (SAÉs) S{semestre_idx}",
|
||||||
create_element_msg="créer une nouvelle SAÉ",
|
create_element_msg="créer une nouvelle SAÉ",
|
||||||
matiere_parent=matiere_parent,
|
# matiere_parent=matiere_parent,
|
||||||
modules=saes_in_sem,
|
modules=saes_in_sem,
|
||||||
module_type=ModuleType.SAE,
|
module_type=ModuleType.SAE,
|
||||||
editable=editable,
|
editable=editable,
|
||||||
tag_editable=tag_editable,
|
tag_editable=tag_editable,
|
||||||
icons=icons,
|
icons=icons,
|
||||||
scu=scu,
|
scu=scu,
|
||||||
),
|
semestre_id=semestre_idx,
|
||||||
|
)
|
||||||
|
if ues_by_sem[semestre_idx].count() > 0
|
||||||
|
else "",
|
||||||
render_template(
|
render_template(
|
||||||
"pn/form_mods.html",
|
"pn/form_mods.html",
|
||||||
formation=formation,
|
formation=formation,
|
||||||
@ -159,7 +165,10 @@ def html_edit_formation_apc(
|
|||||||
tag_editable=tag_editable,
|
tag_editable=tag_editable,
|
||||||
icons=icons,
|
icons=icons,
|
||||||
scu=scu,
|
scu=scu,
|
||||||
),
|
semestre_id=semestre_idx,
|
||||||
|
)
|
||||||
|
if ues_by_sem[semestre_idx].count() > 0
|
||||||
|
else """<span class="fontred">créer une UE pour pouvoir ajouter des modules</span>""",
|
||||||
]
|
]
|
||||||
|
|
||||||
return "\n".join(H)
|
return "\n".join(H)
|
||||||
|
@ -245,7 +245,11 @@ def formation_edit(formation_id=None, create=False):
|
|||||||
return (
|
return (
|
||||||
"\n".join(H)
|
"\n".join(H)
|
||||||
+ tf_error_message(
|
+ tf_error_message(
|
||||||
"Valeurs incorrectes: il existe déjà une formation avec même titre, acronyme et version."
|
f"""Valeurs incorrectes: il existe déjà <a href="{
|
||||||
|
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=others[0]["id"])
|
||||||
|
}">une formation</a> avec même titre,
|
||||||
|
acronyme et version.
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
+ tf[1]
|
+ tf[1]
|
||||||
+ html_sco_header.sco_footer()
|
+ html_sco_header.sco_footer()
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,7 @@ from flask_login import current_user
|
|||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.but import apc_edit_ue
|
||||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||||
from app.models import Formation, UniteEns, ModuleImpl, Module
|
from app.models import Formation, UniteEns, ModuleImpl, Module
|
||||||
from app.models import ScolarNews
|
from app.models import ScolarNews
|
||||||
@ -51,7 +52,6 @@ from app.scodoc.sco_exceptions import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_cache
|
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_edit_apc
|
from app.scodoc import sco_edit_apc
|
||||||
from app.scodoc import sco_edit_matiere
|
from app.scodoc import sco_edit_matiere
|
||||||
@ -77,6 +77,7 @@ _ueEditor = ndb.EditableTable(
|
|||||||
"is_external",
|
"is_external",
|
||||||
"code_apogee",
|
"code_apogee",
|
||||||
"coefficient",
|
"coefficient",
|
||||||
|
"coef_rcue",
|
||||||
"color",
|
"color",
|
||||||
),
|
),
|
||||||
sortkey="numero",
|
sortkey="numero",
|
||||||
@ -121,12 +122,7 @@ def do_ue_create(args):
|
|||||||
# create
|
# create
|
||||||
ue_id = _ueEditor.create(cnx, args)
|
ue_id = _ueEditor.create(cnx, args)
|
||||||
|
|
||||||
# Invalidate cache: vire les poids de toutes les évals de la formation
|
formation: Formation = Formation.query.get(args["formation_id"])
|
||||||
for modimpl in ModuleImpl.query.filter(
|
|
||||||
ModuleImpl.module_id == Module.id, Module.formation_id == args["formation_id"]
|
|
||||||
):
|
|
||||||
modimpl.invalidate_evaluations_poids()
|
|
||||||
formation = Formation.query.get(args["formation_id"])
|
|
||||||
formation.invalidate_module_coefs()
|
formation.invalidate_module_coefs()
|
||||||
# news
|
# news
|
||||||
ue = UniteEns.query.get(ue_id)
|
ue = UniteEns.query.get(ue_id)
|
||||||
@ -144,11 +140,10 @@ def do_ue_create(args):
|
|||||||
|
|
||||||
def do_ue_delete(ue_id, delete_validations=False, force=False):
|
def do_ue_delete(ue_id, delete_validations=False, force=False):
|
||||||
"delete UE and attached matieres (but not modules)"
|
"delete UE and attached matieres (but not modules)"
|
||||||
from app.scodoc import sco_formations
|
|
||||||
from app.scodoc import sco_parcours_dut
|
from app.scodoc import sco_parcours_dut
|
||||||
|
|
||||||
ue = UniteEns.query.get_or_404(ue_id)
|
ue = UniteEns.query.get_or_404(ue_id)
|
||||||
formation_id = ue.formation_id
|
formation = ue.formation
|
||||||
semestre_idx = ue.semestre_idx
|
semestre_idx = ue.semestre_idx
|
||||||
if not ue.can_be_deleted():
|
if not ue.can_be_deleted():
|
||||||
raise ScoNonEmptyFormationObject(
|
raise ScoNonEmptyFormationObject(
|
||||||
@ -157,7 +152,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
|||||||
dest_url=url_for(
|
dest_url=url_for(
|
||||||
"notes.ue_table",
|
"notes.ue_table",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formation_id=formation_id,
|
formation_id=formation.id,
|
||||||
semestre_idx=semestre_idx,
|
semestre_idx=semestre_idx,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -181,7 +176,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
|||||||
cancel_url=url_for(
|
cancel_url=url_for(
|
||||||
"notes.ue_table",
|
"notes.ue_table",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formation_id=formation_id,
|
formation_id=formation.id,
|
||||||
semestre_idx=semestre_idx,
|
semestre_idx=semestre_idx,
|
||||||
),
|
),
|
||||||
parameters={"ue_id": ue.id, "dialog_confirmed": 1},
|
parameters={"ue_id": ue.id, "dialog_confirmed": 1},
|
||||||
@ -192,7 +187,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
|||||||
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
|
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
|
||||||
{"ue_id": ue.id},
|
{"ue_id": ue.id},
|
||||||
)
|
)
|
||||||
|
# delete old formulas
|
||||||
|
ndb.SimpleQuery(
|
||||||
|
"DELETE FROM notes_formsemestre_ue_computation_expr WHERE ue_id=%(ue_id)s",
|
||||||
|
{"ue_id": ue.id},
|
||||||
|
)
|
||||||
# delete all matiere in this UE
|
# delete all matiere in this UE
|
||||||
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
||||||
for mat in mats:
|
for mat in mats:
|
||||||
@ -207,13 +206,13 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
|||||||
_ueEditor.delete(cnx, ue.id)
|
_ueEditor.delete(cnx, ue.id)
|
||||||
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
|
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
|
||||||
# utilisé: acceptable de tout invalider):
|
# utilisé: acceptable de tout invalider):
|
||||||
sco_cache.invalidate_formsemestre()
|
formation.invalidate_module_coefs()
|
||||||
|
# -> invalide aussi .invalidate_formsemestre()
|
||||||
# news
|
# news
|
||||||
F = sco_formations.formation_list(args={"formation_id": formation_id})[0]
|
|
||||||
ScolarNews.add(
|
ScolarNews.add(
|
||||||
typ=ScolarNews.NEWS_FORM,
|
typ=ScolarNews.NEWS_FORM,
|
||||||
obj=formation_id,
|
obj=formation.id,
|
||||||
text=f"Modification de la formation {F['acronyme']}",
|
text=f"Modification de la formation {formation.acronyme}",
|
||||||
max_frequency=10 * 60,
|
max_frequency=10 * 60,
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
@ -222,7 +221,7 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
|
|||||||
url_for(
|
url_for(
|
||||||
"notes.ue_table",
|
"notes.ue_table",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formation_id=formation_id,
|
formation_id=formation.id,
|
||||||
semestre_idx=semestre_idx,
|
semestre_idx=semestre_idx,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -248,13 +247,16 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
|
title = f"Modification de l'UE {ue.acronyme} {ue.titre}"
|
||||||
initvalues = ue_dict
|
initvalues = ue_dict
|
||||||
submitlabel = "Modifier les valeurs"
|
submitlabel = "Modifier les valeurs"
|
||||||
can_change_semestre_id = (ue.modules.count() == 0) or (ue.semestre_idx is None)
|
can_change_semestre_id = (
|
||||||
|
(ue.modules.count() == 0) or (ue.semestre_idx is None)
|
||||||
|
) and ue.niveau_competence is None
|
||||||
else:
|
else:
|
||||||
ue = None
|
ue = None
|
||||||
title = "Création d'une UE"
|
title = "Création d'une UE"
|
||||||
initvalues = {
|
initvalues = {
|
||||||
"semestre_idx": default_semestre_idx,
|
"semestre_idx": default_semestre_idx,
|
||||||
"color": ue_guess_color_default(formation_id, default_semestre_idx),
|
"color": ue_guess_color_default(formation_id, default_semestre_idx),
|
||||||
|
"coef_rcue": 1.0,
|
||||||
}
|
}
|
||||||
submitlabel = "Créer cette UE"
|
submitlabel = "Créer cette UE"
|
||||||
can_change_semestre_id = True
|
can_change_semestre_id = True
|
||||||
@ -277,6 +279,11 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
<p class="help">Note: sauf exception, l'UE n'a pas de coefficient associé.
|
||||||
Seuls les <em>modules</em> ont des coefficients.
|
Seuls les <em>modules</em> ont des coefficients.
|
||||||
</p>""",
|
</p>""",
|
||||||
|
f"""
|
||||||
|
<h4>UE du semestre S{ue.semestre_idx}</h4>
|
||||||
|
"""
|
||||||
|
if is_apc and ue
|
||||||
|
else "",
|
||||||
]
|
]
|
||||||
|
|
||||||
ue_types = parcours.ALLOWED_UE_TYPES
|
ue_types = parcours.ALLOWED_UE_TYPES
|
||||||
@ -308,8 +315,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
"type": "int",
|
"type": "int",
|
||||||
"allow_null": False,
|
"allow_null": False,
|
||||||
"title": parcours.SESSION_NAME.capitalize(),
|
"title": parcours.SESSION_NAME.capitalize(),
|
||||||
"explanation": "%s de l'UE dans la formation"
|
"explanation": f"{parcours.SESSION_NAME} de l'UE dans la formation",
|
||||||
% parcours.SESSION_NAME,
|
|
||||||
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
|
"labels": ["non spécifié"] + [str(x) for x in semestres_indices],
|
||||||
"allowed_values": [""] + semestres_indices,
|
"allowed_values": [""] + semestres_indices,
|
||||||
},
|
},
|
||||||
@ -339,22 +345,43 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
"allow_null": not is_apc, # ects requis en APC
|
"allow_null": not is_apc, # ects requis en APC
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
]
|
||||||
"coefficient",
|
if is_apc: # coef pour la moyenne RCUE
|
||||||
{
|
form_descr.append(
|
||||||
"size": 4,
|
(
|
||||||
"type": "float",
|
"coef_rcue",
|
||||||
"title": "Coefficient",
|
{
|
||||||
"explanation": """les coefficients d'UE ne sont utilisés que
|
"size": 4,
|
||||||
|
"type": "float",
|
||||||
|
"title": "Coef. RCUE",
|
||||||
|
"explanation": """pondération utilisée pour le calcul de la moyenne du RCUE. Laisser à 1, sauf si votre établissement a explicitement décidé de pondérations.
|
||||||
|
""",
|
||||||
|
"defaut": 1.0,
|
||||||
|
"allow_null": False,
|
||||||
|
"enabled": is_apc,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else: # non APC, coef d'UE
|
||||||
|
form_descr.append(
|
||||||
|
(
|
||||||
|
"coefficient",
|
||||||
|
{
|
||||||
|
"size": 4,
|
||||||
|
"type": "float",
|
||||||
|
"title": "Coefficient",
|
||||||
|
"explanation": """les coefficients d'UE ne sont utilisés que
|
||||||
lorsque l'option <em>Utiliser les coefficients d'UE pour calculer
|
lorsque l'option <em>Utiliser les coefficients d'UE pour calculer
|
||||||
la moyenne générale</em> est activée. Par défaut, le coefficient
|
la moyenne générale</em> est activée. Par défaut, le coefficient
|
||||||
d'une UE est simplement la somme des coefficients des modules dans
|
d'une UE est simplement la somme des coefficients des modules dans
|
||||||
lesquels l'étudiant a des notes.
|
lesquels l'étudiant a des notes.
|
||||||
Jamais utilisé en BUT.
|
Jamais utilisé en BUT.
|
||||||
""",
|
""",
|
||||||
"enabled": not is_apc,
|
"enabled": not is_apc,
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
|
)
|
||||||
|
form_descr += [
|
||||||
(
|
(
|
||||||
"ue_code",
|
"ue_code",
|
||||||
{
|
{
|
||||||
@ -410,8 +437,12 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
form_descr,
|
form_descr,
|
||||||
initvalues=initvalues,
|
initvalues=initvalues,
|
||||||
submitlabel=submitlabel,
|
submitlabel=submitlabel,
|
||||||
|
cancelbutton="Revenir à la formation",
|
||||||
)
|
)
|
||||||
if tf[0] == 0:
|
if tf[0] == 0:
|
||||||
|
niveau_competence_div = ""
|
||||||
|
if ue and is_apc:
|
||||||
|
niveau_competence_div = apc_edit_ue.form_ue_choix_niveau(formation, ue)
|
||||||
if ue and ue.modules.count() and ue.semestre_idx is not None:
|
if ue and ue.modules.count() and ue.semestre_idx is not None:
|
||||||
modules_div = f"""<div id="ue_list_modules">
|
modules_div = f"""<div id="ue_list_modules">
|
||||||
<div><b>{ue.modules.count()} modules sont rattachés
|
<div><b>{ue.modules.count()} modules sont rattachés
|
||||||
@ -420,7 +451,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
<ul>"""
|
<ul>"""
|
||||||
for m in ue.modules:
|
for m in ue.modules:
|
||||||
modules_div += f"""<li><a class="stdlink" href="{url_for(
|
modules_div += f"""<li><a class="stdlink" href="{url_for(
|
||||||
"notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre}</a></li>"""
|
"notes.module_edit",scodoc_dept=g.scodoc_dept, module_id=m.id)}">{m.code} {m.titre or "sans titre"}</a></li>"""
|
||||||
modules_div += """</ul></div>"""
|
modules_div += """</ul></div>"""
|
||||||
else:
|
else:
|
||||||
modules_div = ""
|
modules_div = ""
|
||||||
@ -429,12 +460,13 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
return (
|
return (
|
||||||
"\n".join(H)
|
"\n".join(H)
|
||||||
+ tf[1]
|
+ tf[1]
|
||||||
|
+ niveau_competence_div
|
||||||
+ modules_div
|
+ modules_div
|
||||||
+ bonus_div
|
+ bonus_div
|
||||||
+ ue_div
|
+ ue_div
|
||||||
+ html_sco_header.sco_footer()
|
+ html_sco_header.sco_footer()
|
||||||
)
|
)
|
||||||
else:
|
elif tf[2]:
|
||||||
if create:
|
if create:
|
||||||
if not tf[2]["ue_code"]:
|
if not tf[2]["ue_code"]:
|
||||||
del tf[2]["ue_code"]
|
del tf[2]["ue_code"]
|
||||||
@ -467,14 +499,26 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
|||||||
else:
|
else:
|
||||||
do_ue_edit(tf[2])
|
do_ue_edit(tf[2])
|
||||||
flash("UE modifiée")
|
flash("UE modifiée")
|
||||||
return flask.redirect(
|
|
||||||
url_for(
|
if tf[2]:
|
||||||
"notes.ue_table",
|
dest_semestre_idx = tf[2]["semestre_idx"]
|
||||||
scodoc_dept=g.scodoc_dept,
|
elif ue:
|
||||||
formation_id=formation_id,
|
dest_semestre_idx = ue.semestre_idx
|
||||||
semestre_idx=tf[2]["semestre_idx"],
|
elif default_semestre_idx:
|
||||||
)
|
dest_semestre_idx = default_semestre_idx
|
||||||
|
elif "semestre_idx" in request.form:
|
||||||
|
dest_semestre_idx = request.form["semestre_idx"]
|
||||||
|
else:
|
||||||
|
dest_semestre_idx = 1
|
||||||
|
|
||||||
|
return flask.redirect(
|
||||||
|
url_for(
|
||||||
|
"notes.ue_table",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formation_id=formation_id,
|
||||||
|
semestre_idx=dest_semestre_idx,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _add_ue_semestre_id(ues: list[dict], is_apc):
|
def _add_ue_semestre_id(ues: list[dict], is_apc):
|
||||||
@ -646,9 +690,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||||||
],
|
],
|
||||||
page_title=f"Programme {formation.acronyme}",
|
page_title=f"Programme {formation.acronyme}",
|
||||||
),
|
),
|
||||||
f"""<h2>Formation {formation.titre} ({formation.acronyme})
|
f"""<h2>{formation.to_html()} {lockicon}
|
||||||
[version {formation.version}] code {formation.formation_code}
|
|
||||||
{lockicon}
|
|
||||||
</h2>
|
</h2>
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
@ -711,7 +753,8 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
else:
|
else:
|
||||||
descr_refcomp = f"""Référentiel de compétences:
|
descr_refcomp = f"""Référentiel de compétences:
|
||||||
<a href="{url_for('notes.refcomp_show',
|
<a href="{url_for('notes.refcomp_show',
|
||||||
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}">
|
scodoc_dept=g.scodoc_dept, refcomp_id=formation.referentiel_competence.id)}"
|
||||||
|
class="stdlink">
|
||||||
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
|
{formation.referentiel_competence.type_titre} {formation.referentiel_competence.specialite_long}
|
||||||
</a> """
|
</a> """
|
||||||
msg_refcomp = "changer"
|
msg_refcomp = "changer"
|
||||||
@ -727,7 +770,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
f"""</li>
|
f"""</li>
|
||||||
<li> <a class="stdlink" href="{
|
<li> <a class="stdlink" href="{
|
||||||
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
|
url_for('notes.edit_modules_ue_coefs', scodoc_dept=g.scodoc_dept, formation_id=formation_id, semestre_idx=semestre_idx)
|
||||||
}">éditer les coefficients des ressources et SAÉs</a>
|
}">Éditer les coefficients des ressources et SAÉs</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
"""
|
"""
|
||||||
@ -816,6 +859,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||||||
)
|
)
|
||||||
}">Créer une nouvelle version (non verrouillée)</a>
|
}">Créer une nouvelle version (non verrouillée)</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
@ -914,7 +958,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
|
|||||||
|
|
||||||
def _ue_table_ues(
|
def _ue_table_ues(
|
||||||
parcours,
|
parcours,
|
||||||
ues,
|
ues: list[dict],
|
||||||
editable,
|
editable,
|
||||||
tag_editable,
|
tag_editable,
|
||||||
has_perm_change,
|
has_perm_change,
|
||||||
@ -923,7 +967,7 @@ def _ue_table_ues(
|
|||||||
arrow_none,
|
arrow_none,
|
||||||
delete_icon,
|
delete_icon,
|
||||||
delete_disabled_icon,
|
delete_disabled_icon,
|
||||||
):
|
) -> str:
|
||||||
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
"""Édition de programme: liste des UEs (avec leurs matières et modules).
|
||||||
Pour les formations classiques (non APC/BUT)
|
Pour les formations classiques (non APC/BUT)
|
||||||
"""
|
"""
|
||||||
@ -951,9 +995,9 @@ def _ue_table_ues(
|
|||||||
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
|
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
|
||||||
lab = "Pas d'indication de semestre:"
|
lab = "Pas d'indication de semestre:"
|
||||||
else:
|
else:
|
||||||
lab = "Semestre %s:" % ue["semestre_id"]
|
lab = f"""Semestre {ue["semestre_id"]}:"""
|
||||||
H.append(
|
H.append(
|
||||||
'<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab
|
f'<div class="ue_list_div"><div class="ue_list_tit_sem">{lab}</div>'
|
||||||
)
|
)
|
||||||
H.append('<ul class="notes_ue_list">')
|
H.append('<ul class="notes_ue_list">')
|
||||||
H.append('<li class="notes_ue_list">')
|
H.append('<li class="notes_ue_list">')
|
||||||
@ -1304,8 +1348,9 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|||||||
|
|
||||||
formation = Formation.query.get(ue["formation_id"])
|
formation = Formation.query.get(ue["formation_id"])
|
||||||
if not dont_invalidate_cache:
|
if not dont_invalidate_cache:
|
||||||
# Invalide les semestres utilisant cette formation:
|
# Invalide les semestres utilisant cette formation
|
||||||
formation.invalidate_cached_sems()
|
# ainsi que les poids et coefs
|
||||||
|
formation.invalidate_module_coefs()
|
||||||
|
|
||||||
|
|
||||||
# essai edition en ligne:
|
# essai edition en ligne:
|
||||||
|
@ -59,7 +59,7 @@ class COLORS(Enum):
|
|||||||
LIGHT_YELLOW = "FFFFFF99"
|
LIGHT_YELLOW = "FFFFFF99"
|
||||||
|
|
||||||
|
|
||||||
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attributdans la liste suivante:
|
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante:
|
||||||
# font, border, number_format, fill,...
|
# font, border, number_format, fill,...
|
||||||
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
|
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ class ScoExcelSheet:
|
|||||||
value -- contenu de la cellule (texte, numérique, booléen ou date)
|
value -- contenu de la cellule (texte, numérique, booléen ou date)
|
||||||
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
||||||
"""
|
"""
|
||||||
# adapatation des valeurs si nécessaire
|
# adaptation des valeurs si nécessaire
|
||||||
if value is None:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
elif value is True:
|
elif value is True:
|
||||||
|
@ -141,11 +141,18 @@ def do_formsemestre_list(*a, **kw):
|
|||||||
|
|
||||||
|
|
||||||
def _formsemestre_enrich(sem):
|
def _formsemestre_enrich(sem):
|
||||||
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris)"""
|
"""Ajoute champs souvent utiles: titre + annee et dateord (pour tris).
|
||||||
|
XXX obsolete: préférer formsemestre.to_dict() ou, mieux, les méthodes de FormSemestre.
|
||||||
|
"""
|
||||||
# imports ici pour eviter refs circulaires
|
# imports ici pour eviter refs circulaires
|
||||||
from app.scodoc import sco_formsemestre_edit
|
from app.scodoc import sco_formsemestre_edit
|
||||||
|
|
||||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
formations = sco_formations.formation_list(
|
||||||
|
args={"formation_id": sem["formation_id"]}
|
||||||
|
)
|
||||||
|
if not formations:
|
||||||
|
raise ScoValueError("pas de formation pour ce semestre !")
|
||||||
|
F = formations[0]
|
||||||
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
parcours = sco_codes_parcours.get_parcours_from_code(F["type_parcours"])
|
||||||
# 'S1', 'S2', ... ou '' pour les monosemestres
|
# 'S1', 'S2', ... ou '' pour les monosemestres
|
||||||
if sem["semestre_id"] != NO_SEMESTRE_ID:
|
if sem["semestre_id"] != NO_SEMESTRE_ID:
|
||||||
|
@ -39,23 +39,21 @@ from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteE
|
|||||||
from app.models import ScolarNews
|
from app.models import ScolarNews
|
||||||
from app.models.formations import Formation
|
from app.models.formations import Formation
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.models.but_refcomp import ApcParcours
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_cache
|
from app.scodoc import sco_cache
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
from app.scodoc import sco_compute_moy
|
from app.scodoc import sco_compute_moy
|
||||||
from app.scodoc import sco_edit_matiere
|
|
||||||
from app.scodoc import sco_edit_module
|
from app.scodoc import sco_edit_module
|
||||||
from app.scodoc import sco_edit_ue
|
|
||||||
from app.scodoc import sco_etud
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_evaluations
|
|
||||||
from app.scodoc import sco_evaluation_db
|
from app.scodoc import sco_evaluation_db
|
||||||
from app.scodoc import sco_formations
|
from app.scodoc import sco_formations
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
@ -119,12 +117,12 @@ def formsemestre_editwithmodules(formsemestre_id):
|
|||||||
vals = scu.get_request_args()
|
vals = scu.get_request_args()
|
||||||
if not vals.get("tf_submitted", False):
|
if not vals.get("tf_submitted", False):
|
||||||
H.append(
|
H.append(
|
||||||
"""<p class="help">Seuls les modules cochés font partie de ce semestre.
|
"""<p class="help">Seuls les modules cochés font partie de ce semestre.
|
||||||
Pour les retirer, les décocher et appuyer sur le bouton "modifier".
|
Pour les retirer, les décocher et appuyer sur le bouton "modifier".
|
||||||
</p>
|
</p>
|
||||||
<p class="help">Attention : s'il y a déjà des évaluations dans un module,
|
<p class="help">Attention : s'il y a déjà des évaluations dans un module,
|
||||||
il ne peut pas être supprimé !</p>
|
il ne peut pas être supprimé !</p>
|
||||||
<p class="help">Les modules ont toujours un responsable.
|
<p class="help">Les modules ont toujours un responsable.
|
||||||
Par défaut, c'est le directeur des études.</p>
|
Par défaut, c'est le directeur des études.</p>
|
||||||
<p class="help">Un semestre ne peut comporter qu'une seule UE "bonus
|
<p class="help">Un semestre ne peut comporter qu'une seule UE "bonus
|
||||||
sport/culture"</p>
|
sport/culture"</p>
|
||||||
@ -153,7 +151,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
if not current_user.has_permission(Permission.ScoImplement):
|
if not current_user.has_permission(Permission.ScoImplement):
|
||||||
if not edit:
|
if not edit:
|
||||||
# il faut ScoImplement pour creer un semestre
|
# il faut ScoImplement pour créer un semestre
|
||||||
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
|
raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
|
||||||
else:
|
else:
|
||||||
if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
|
if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
|
||||||
@ -175,6 +173,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
formation = Formation.query.get(formation_id)
|
formation = Formation.query.get(formation_id)
|
||||||
if formation is None:
|
if formation is None:
|
||||||
raise ScoValueError("Formation inexistante !")
|
raise ScoValueError("Formation inexistante !")
|
||||||
|
is_apc = formation.is_apc()
|
||||||
if not edit:
|
if not edit:
|
||||||
initvalues = {"titre": _default_sem_title(formation)}
|
initvalues = {"titre": _default_sem_title(formation)}
|
||||||
semestre_id = int(vals["semestre_id"])
|
semestre_id = int(vals["semestre_id"])
|
||||||
@ -210,12 +209,12 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
if NB_SEM == 1:
|
if NB_SEM == 1:
|
||||||
semestre_id_list = [-1]
|
semestre_id_list = [-1]
|
||||||
else:
|
else:
|
||||||
if edit and formation.is_apc():
|
if edit and is_apc:
|
||||||
# en APC, ne permet pas de changer de semestre
|
# en APC, ne permet pas de changer de semestre
|
||||||
semestre_id_list = [formsemestre.semestre_id]
|
semestre_id_list = [formsemestre.semestre_id]
|
||||||
else:
|
else:
|
||||||
semestre_id_list = list(range(1, NB_SEM + 1))
|
semestre_id_list = list(range(1, NB_SEM + 1))
|
||||||
if not formation.is_apc():
|
if not is_apc:
|
||||||
# propose "pas de semestre" seulement en classique
|
# propose "pas de semestre" seulement en classique
|
||||||
semestre_id_list.insert(0, -1)
|
semestre_id_list.insert(0, -1)
|
||||||
|
|
||||||
@ -226,7 +225,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
else:
|
else:
|
||||||
semestre_id_labels.append(f"S{sid}")
|
semestre_id_labels.append(f"S{sid}")
|
||||||
# Liste des modules dans cette formation
|
# Liste des modules dans cette formation
|
||||||
if formation.is_apc():
|
if is_apc:
|
||||||
modules = formation.modules.order_by(Module.module_type, Module.numero)
|
modules = formation.modules.order_by(Module.module_type, Module.numero)
|
||||||
else:
|
else:
|
||||||
modules = (
|
modules = (
|
||||||
@ -318,10 +317,10 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
{
|
{
|
||||||
"size": 40,
|
"size": 40,
|
||||||
"title": "Nom de ce semestre",
|
"title": "Nom de ce semestre",
|
||||||
"explanation": """n'indiquez pas les dates, ni le semestre, ni la modalité dans
|
"explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
|
||||||
le titre: ils seront automatiquement ajoutés <input type="button"
|
le titre: ils seront automatiquement ajoutés <input type="button"
|
||||||
value="remettre titre par défaut" onClick="document.tf.titre.value='%s';"/>"""
|
value="remettre titre par défaut" onClick="document.tf.titre.value='{
|
||||||
% _default_sem_title(formation),
|
_default_sem_title(formation)}';"/>""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -343,11 +342,9 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
"allowed_values": semestre_id_list,
|
"allowed_values": semestre_id_list,
|
||||||
"labels": semestre_id_labels,
|
"labels": semestre_id_labels,
|
||||||
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
|
"explanation": "en BUT, on ne peut pas modifier le semestre après création"
|
||||||
if formation.is_apc()
|
if is_apc
|
||||||
else "",
|
|
||||||
"attributes": ['onchange="change_semestre_id();"']
|
|
||||||
if formation.is_apc()
|
|
||||||
else "",
|
else "",
|
||||||
|
"attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -386,7 +383,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
mf = mf_manual
|
mf = mf_manual
|
||||||
|
|
||||||
for n in range(1, scu.EDIT_NB_ETAPES + 1):
|
for n in range(1, scu.EDIT_NB_ETAPES + 1):
|
||||||
mf["title"] = "Etape Apogée (%d)" % n
|
mf["title"] = f"Etape Apogée ({n})"
|
||||||
modform.append(("etape_apo" + str(n), mf.copy()))
|
modform.append(("etape_apo" + str(n), mf.copy()))
|
||||||
modform.append(
|
modform.append(
|
||||||
(
|
(
|
||||||
@ -443,15 +440,19 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if edit:
|
if edit:
|
||||||
formtit = (
|
formtit = f"""
|
||||||
"""
|
<p><a href="formsemestre_edit_uecoefs?formsemestre_id={formsemestre_id}"
|
||||||
<p><a href="formsemestre_edit_uecoefs?formsemestre_id=%s">Modifier les coefficients des UE capitalisées</a></p>
|
>Modifier les coefficients des UE capitalisées</a></p>
|
||||||
<h3>Sélectionner les modules, leurs responsables et les étudiants à inscrire:</h3>
|
<h3>Sélectionner les modules, leurs responsables et les étudiants
|
||||||
|
à inscrire:</h3>
|
||||||
"""
|
"""
|
||||||
% formsemestre_id
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
formtit = """<h3>Sélectionner les modules et leurs responsables</h3><p class="help">Si vous avez des parcours (options), ne sélectionnez que les modules du tronc commun.</p>"""
|
formtit = """<h3>Sélectionner les modules et leurs responsables</h3>
|
||||||
|
<p class="help">Si vous avez des parcours (options), dans un premier
|
||||||
|
ne sélectionnez que les modules du tronc commun, puis après inscriptions,
|
||||||
|
revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants
|
||||||
|
à y inscrire.
|
||||||
|
</p>"""
|
||||||
|
|
||||||
modform += [
|
modform += [
|
||||||
(
|
(
|
||||||
@ -531,12 +532,53 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
|
"explanation": "empêcher le calcul des moyennes d'UE et générale.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
]
|
||||||
|
# Choix des parcours
|
||||||
|
if is_apc:
|
||||||
|
ref_comp = formation.referentiel_competence
|
||||||
|
if ref_comp:
|
||||||
|
modform += [
|
||||||
|
(
|
||||||
|
"parcours",
|
||||||
|
{
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"vertical": True,
|
||||||
|
"dom_id": "tf_module_parcours",
|
||||||
|
"labels": [parcour.libelle for parcour in ref_comp.parcours],
|
||||||
|
"allowed_values": [
|
||||||
|
str(parcour.id) for parcour in ref_comp.parcours
|
||||||
|
],
|
||||||
|
"explanation": """Parcours proposés dans ce semestre.
|
||||||
|
S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if edit:
|
||||||
|
sem["parcours"] = [str(parcour.id) for parcour in formsemestre.parcours]
|
||||||
|
else:
|
||||||
|
modform += [
|
||||||
|
(
|
||||||
|
"parcours",
|
||||||
|
{
|
||||||
|
"input_type": "separator",
|
||||||
|
"title": f"""<span class="fontred">{scu.EMO_WARNING }
|
||||||
|
Pas de parcours:
|
||||||
|
<a class="stdlink" href="{ url_for('notes.ue_table',
|
||||||
|
scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||||
|
}">vérifier la formation</a>
|
||||||
|
</span>""",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Choix des modules
|
||||||
|
modform += [
|
||||||
(
|
(
|
||||||
"sep",
|
"sep",
|
||||||
{
|
{
|
||||||
"input_type": "separator",
|
"input_type": "separator",
|
||||||
"title": "",
|
"title": "",
|
||||||
"template": "</table>%s<table>" % formtit,
|
"template": f"</table>{formtit}<table>",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -544,8 +586,8 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
nbmod = 0
|
nbmod = 0
|
||||||
|
|
||||||
for semestre_id in semestre_ids:
|
for semestre_id in semestre_ids:
|
||||||
if formation.is_apc():
|
if is_apc:
|
||||||
# pour restreindre l'édition aux module du semestre sélectionné
|
# pour restreindre l'édition aux modules du semestre sélectionné
|
||||||
tr_class = f'class="sem{semestre_id}"'
|
tr_class = f'class="sem{semestre_id}"'
|
||||||
else:
|
else:
|
||||||
tr_class = ""
|
tr_class = ""
|
||||||
@ -560,7 +602,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
"sep",
|
"sep",
|
||||||
{
|
{
|
||||||
"input_type": "separator",
|
"input_type": "separator",
|
||||||
"title": "<b>Semestre %s</b>" % semestre_id,
|
"title": f"<b>Semestre {semestre_id}</b>",
|
||||||
"template": templ_sep,
|
"template": templ_sep,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -568,13 +610,13 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
for mod in mods:
|
for mod in mods:
|
||||||
if mod["semestre_id"] == semestre_id and (
|
if mod["semestre_id"] == semestre_id and (
|
||||||
(not edit) # creation => tous modules
|
(not edit) # creation => tous modules
|
||||||
or (not formation.is_apc()) # pas BUT, on peut mixer les semestres
|
or (not is_apc) # pas BUT, on peut mixer les semestres
|
||||||
or (semestre_id == formsemestre.semestre_id) # module du semestre
|
or (semestre_id == formsemestre.semestre_id) # module du semestre
|
||||||
or (mod["module_id"] in module_ids_set) # module déjà présent
|
or (mod["module_id"] in module_ids_set) # module déjà présent
|
||||||
):
|
):
|
||||||
nbmod += 1
|
nbmod += 1
|
||||||
if edit:
|
if edit:
|
||||||
select_name = "%s!group_id" % mod["module_id"]
|
select_name = f"{mod['module_id']}!group_id"
|
||||||
|
|
||||||
def opt_selected(gid):
|
def opt_selected(gid):
|
||||||
if gid == vals.get(select_name):
|
if gid == vals.get(select_name):
|
||||||
@ -603,13 +645,16 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
group["group_name"],
|
group["group_name"],
|
||||||
)
|
)
|
||||||
fcg += "</select>"
|
fcg += "</select>"
|
||||||
itemtemplate = (
|
itemtemplate = f"""<tr {tr_class}>
|
||||||
f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td><td>"""
|
<td class="tf-fieldlabel">%(label)s</td>
|
||||||
+ fcg
|
<td class="tf-field">%(elem)s</td>
|
||||||
+ "</td></tr>"
|
<td>{fcg}</td>
|
||||||
)
|
</tr>"""
|
||||||
else:
|
else:
|
||||||
itemtemplate = f"""<tr {tr_class}><td class="tf-fieldlabel">%(label)s</td><td class="tf-field">%(elem)s</td></tr>"""
|
itemtemplate = f"""<tr {tr_class}>
|
||||||
|
<td class="tf-fieldlabel">%(label)s</td>
|
||||||
|
<td class="tf-field">%(elem)s</td>
|
||||||
|
</tr>"""
|
||||||
modform.append(
|
modform.append(
|
||||||
(
|
(
|
||||||
"MI" + str(mod["module_id"]),
|
"MI" + str(mod["module_id"]),
|
||||||
@ -742,7 +787,8 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
for module_id in tf[2]["tf-checked"]:
|
for module_id in tf[2]["tf-checked"]:
|
||||||
mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
|
mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
|
||||||
if mod_resp_id is None:
|
if mod_resp_id is None:
|
||||||
# Si un module n'a pas de responsable (ou inconnu), l'affecte au 1er directeur des etudes:
|
# Si un module n'a pas de responsable (ou inconnu),
|
||||||
|
# l'affecte au 1er directeur des etudes:
|
||||||
mod_resp_id = tf[2]["responsable_id"]
|
mod_resp_id = tf[2]["responsable_id"]
|
||||||
tf[2][module_id] = mod_resp_id
|
tf[2][module_id] = mod_resp_id
|
||||||
|
|
||||||
@ -763,7 +809,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
|
module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
|
||||||
_formsemestre_check_ue_bonus_unicity(module_ids_checked)
|
_formsemestre_check_ue_bonus_unicity(module_ids_checked)
|
||||||
if not edit:
|
if not edit:
|
||||||
if formation.is_apc():
|
if is_apc:
|
||||||
_formsemestre_check_module_list(
|
_formsemestre_check_module_list(
|
||||||
module_ids_checked, tf[2]["semestre_id"]
|
module_ids_checked, tf[2]["semestre_id"]
|
||||||
)
|
)
|
||||||
@ -777,14 +823,6 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
"responsable_id": tf[2][f"MI{module_id}"],
|
"responsable_id": tf[2][f"MI{module_id}"],
|
||||||
}
|
}
|
||||||
_ = sco_moduleimpl.do_moduleimpl_create(modargs)
|
_ = sco_moduleimpl.do_moduleimpl_create(modargs)
|
||||||
flash("Nouveau semestre créé")
|
|
||||||
return flask.redirect(
|
|
||||||
url_for(
|
|
||||||
"notes.formsemestre_status",
|
|
||||||
scodoc_dept=g.scodoc_dept,
|
|
||||||
formsemestre_id=formsemestre_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
# Modification du semestre:
|
# Modification du semestre:
|
||||||
# on doit creer les modules nouvellement selectionnés
|
# on doit creer les modules nouvellement selectionnés
|
||||||
@ -794,7 +832,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
module_ids_tocreate = [
|
module_ids_tocreate = [
|
||||||
x for x in module_ids_checked if not x in module_ids_existing
|
x for x in module_ids_checked if not x in module_ids_existing
|
||||||
]
|
]
|
||||||
if formation.is_apc():
|
if is_apc:
|
||||||
_formsemestre_check_module_list(
|
_formsemestre_check_module_list(
|
||||||
module_ids_tocreate, tf[2]["semestre_id"]
|
module_ids_tocreate, tf[2]["semestre_id"]
|
||||||
)
|
)
|
||||||
@ -868,27 +906,48 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||||||
modargs, formsemestre_id=formsemestre_id
|
modargs, formsemestre_id=formsemestre_id
|
||||||
)
|
)
|
||||||
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
mod = sco_edit_module.module_list({"module_id": module_id})[0]
|
||||||
|
# --- Association des parcours
|
||||||
if msg:
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
msg_html = (
|
if "parcours" in tf[2]:
|
||||||
'<div class="ue_warning"><span>Attention !<ul><li>'
|
formsemestre.parcours = [
|
||||||
+ "</li><li>".join(msg)
|
ApcParcours.query.get(int(parcour_id_str))
|
||||||
+ "</li></ul></span></div>"
|
for parcour_id_str in tf[2]["parcours"]
|
||||||
)
|
]
|
||||||
if ok:
|
db.session.add(formsemestre)
|
||||||
msg_html += "<p>Modification effectuée</p>"
|
db.session.commit()
|
||||||
else:
|
# --- Crée ou met à jour les groupes de parcours BUT
|
||||||
msg_html += "<p>Modification effectuée (<b>mais modules cités non supprimés</b>)</p>"
|
formsemestre.setup_parcours_groups()
|
||||||
msg_html += (
|
# --- Fin
|
||||||
'<a href="formsemestre_status?formsemestre_id=%s">retour au tableau de bord</a>'
|
if edit:
|
||||||
% formsemestre_id
|
if msg:
|
||||||
)
|
msg_html = (
|
||||||
return msg_html
|
'<div class="ue_warning"><span>Attention !<ul><li>'
|
||||||
|
+ "</li><li>".join(msg)
|
||||||
|
+ "</li></ul></span></div>"
|
||||||
|
)
|
||||||
|
if ok:
|
||||||
|
msg_html += "<p>Modification effectuée</p>"
|
||||||
else:
|
else:
|
||||||
return flask.redirect(
|
msg_html += "<p>Modification effectuée (<b>mais modules cités non supprimés</b>)</p>"
|
||||||
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
|
msg_html += (
|
||||||
% formsemestre_id
|
'<a href="formsemestre_status?formsemestre_id=%s">retour au tableau de bord</a>'
|
||||||
)
|
% formsemestre_id
|
||||||
|
)
|
||||||
|
return msg_html
|
||||||
|
else:
|
||||||
|
return flask.redirect(
|
||||||
|
"formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
|
||||||
|
% formsemestre_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash("Nouveau semestre créé")
|
||||||
|
return flask.redirect(
|
||||||
|
url_for(
|
||||||
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _formsemestre_check_module_list(module_ids, semestre_idx):
|
def _formsemestre_check_module_list(module_ids, semestre_idx):
|
||||||
|
@ -35,6 +35,7 @@ from flask import url_for, g, request
|
|||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre
|
from app.models import FormSemestre
|
||||||
|
from app.models.groups import GroupDescr, Partition
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
@ -257,14 +258,14 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
"""Inscrit cet etudiant à ce semestre et TOUS ses modules STANDARDS
|
||||||
(donc sauf le sport)
|
(donc sauf le sport)
|
||||||
"""
|
"""
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
# inscription au semestre
|
# inscription au semestre
|
||||||
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
|
||||||
if etat is not None:
|
if etat is not None:
|
||||||
args["etat"] = etat
|
args["etat"] = etat
|
||||||
do_formsemestre_inscription_create(args, method=method)
|
do_formsemestre_inscription_create(args, method=method)
|
||||||
log(
|
log(
|
||||||
"do_formsemestre_inscription_with_modules: etudid=%s formsemestre_id=%s"
|
f"do_formsemestre_inscription_with_modules: etudid={etudid} formsemestre_id={formsemestre_id}"
|
||||||
% (etudid, formsemestre_id)
|
|
||||||
)
|
)
|
||||||
# inscriptions aux groupes
|
# inscriptions aux groupes
|
||||||
# 1- inscrit au groupe 'tous'
|
# 1- inscrit au groupe 'tous'
|
||||||
@ -275,10 +276,16 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
# 2- inscrit aux groupes
|
# 2- inscrit aux groupes
|
||||||
for group_id in group_ids:
|
for group_id in group_ids:
|
||||||
if group_id and not group_id in gdone:
|
if group_id and not group_id in gdone:
|
||||||
sco_groups.set_group(etudid, group_id)
|
group = GroupDescr.query.get_or_404(group_id)
|
||||||
gdone[group_id] = 1
|
if group.partition.groups_editable:
|
||||||
|
sco_groups.set_group(etudid, group_id)
|
||||||
|
gdone[group_id] = 1
|
||||||
|
else:
|
||||||
|
log(
|
||||||
|
f"do_formsemestre_inscription_with_modules: group {group:r} belongs to non editable partition"
|
||||||
|
)
|
||||||
|
|
||||||
# inscription a tous les modules de ce semestre
|
# Inscription à tous les modules de ce semestre
|
||||||
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
modimpls = sco_moduleimpl.moduleimpl_withmodule_list(
|
||||||
formsemestre_id=formsemestre_id
|
formsemestre_id=formsemestre_id
|
||||||
)
|
)
|
||||||
@ -288,6 +295,8 @@ def do_formsemestre_inscription_with_modules(
|
|||||||
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
|
{"moduleimpl_id": mod["moduleimpl_id"], "etudid": etudid},
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
)
|
)
|
||||||
|
# Mise à jour des inscriptions aux parcours:
|
||||||
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_inscription_with_modules_etud(
|
def formsemestre_inscription_with_modules_etud(
|
||||||
|
@ -40,6 +40,7 @@ from app.comp import res_sem
|
|||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import Module
|
from app.models import Module
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.models.moduleimpls import ModuleImpl
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
@ -148,7 +149,10 @@ def formsemestre_status_menubar(sem):
|
|||||||
{
|
{
|
||||||
"title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
|
"title": "Voir la formation %(acronyme)s (v%(version)s)" % F,
|
||||||
"endpoint": "notes.ue_table",
|
"endpoint": "notes.ue_table",
|
||||||
"args": {"formation_id": sem["formation_id"]},
|
"args": {
|
||||||
|
"formation_id": sem["formation_id"],
|
||||||
|
"semestre_idx": sem["semestre_id"],
|
||||||
|
},
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"helpmsg": "Tableau de bord du semestre",
|
"helpmsg": "Tableau de bord du semestre",
|
||||||
},
|
},
|
||||||
@ -325,7 +329,7 @@ def formsemestre_status_menubar(sem):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Créer/modifier les partitions...",
|
"title": "Créer/modifier les partitions...",
|
||||||
"endpoint": "scolar.editPartitionForm",
|
"endpoint": "scolar.edit_partition_form",
|
||||||
"args": {"formsemestre_id": formsemestre_id},
|
"args": {"formsemestre_id": formsemestre_id},
|
||||||
"enabled": sco_groups.sco_permissions_check.can_change_groups(
|
"enabled": sco_groups.sco_permissions_check.can_change_groups(
|
||||||
formsemestre_id
|
formsemestre_id
|
||||||
@ -345,7 +349,7 @@ def formsemestre_status_menubar(sem):
|
|||||||
"title": "%s" % partition["partition_name"],
|
"title": "%s" % partition["partition_name"],
|
||||||
"endpoint": "scolar.affect_groups",
|
"endpoint": "scolar.affect_groups",
|
||||||
"args": {"partition_id": partition["partition_id"]},
|
"args": {"partition_id": partition["partition_id"]},
|
||||||
"enabled": enabled,
|
"enabled": enabled and partition["groups_editable"],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
menuGroupes.append(
|
menuGroupes.append(
|
||||||
@ -406,10 +410,9 @@ def formsemestre_status_menubar(sem):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Saisie des décisions du jury",
|
"title": "Saisie des décisions du jury",
|
||||||
"endpoint": "notes.formsemestre_recapcomplet",
|
"endpoint": "notes.formsemestre_saisie_jury",
|
||||||
"args": {
|
"args": {
|
||||||
"formsemestre_id": formsemestre_id,
|
"formsemestre_id": formsemestre_id,
|
||||||
"modejury": 1,
|
|
||||||
},
|
},
|
||||||
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
|
||||||
},
|
},
|
||||||
@ -499,20 +502,24 @@ def retreive_formsemestre_from_request() -> int:
|
|||||||
|
|
||||||
|
|
||||||
# Element HTML decrivant un semestre (barre de menu et infos)
|
# Element HTML decrivant un semestre (barre de menu et infos)
|
||||||
def formsemestre_page_title():
|
def formsemestre_page_title(formsemestre_id=None):
|
||||||
"""Element HTML decrivant un semestre (barre de menu et infos)
|
"""Element HTML decrivant un semestre (barre de menu et infos)
|
||||||
Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group)
|
Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group)
|
||||||
"""
|
"""
|
||||||
formsemestre_id = retreive_formsemestre_from_request()
|
formsemestre_id = (
|
||||||
|
formsemestre_id
|
||||||
|
if formsemestre_id is not None
|
||||||
|
else retreive_formsemestre_from_request()
|
||||||
|
)
|
||||||
#
|
#
|
||||||
if not formsemestre_id:
|
if not formsemestre_id:
|
||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
formsemestre_id = int(formsemestre_id)
|
formsemestre_id = int(formsemestre_id)
|
||||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
except ValueError:
|
||||||
except:
|
log(f"formsemestre_id: invalid type {formsemestre_id:r}")
|
||||||
log("can't find formsemestre_id %s" % formsemestre_id)
|
|
||||||
return ""
|
return ""
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
|
||||||
h = render_template(
|
h = render_template(
|
||||||
"formsemestre_page_title.html",
|
"formsemestre_page_title.html",
|
||||||
@ -578,7 +585,9 @@ def fill_formsemestre(sem):
|
|||||||
|
|
||||||
|
|
||||||
# Description du semestre sous forme de table exportable
|
# Description du semestre sous forme de table exportable
|
||||||
def formsemestre_description_table(formsemestre_id, with_evals=False):
|
def formsemestre_description_table(
|
||||||
|
formsemestre_id, with_evals=False, with_parcours=False
|
||||||
|
):
|
||||||
"""Description du semestre sous forme de table exportable
|
"""Description du semestre sous forme de table exportable
|
||||||
Liste des modules et de leurs coefficients
|
Liste des modules et de leurs coefficients
|
||||||
"""
|
"""
|
||||||
@ -618,7 +627,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
ue_info["Coef._class"] = "ue_coef"
|
ue_info["Coef._class"] = "ue_coef"
|
||||||
R.append(ue_info)
|
R.append(ue_info)
|
||||||
|
|
||||||
ModInscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||||
moduleimpl_id=M["moduleimpl_id"]
|
moduleimpl_id=M["moduleimpl_id"]
|
||||||
)
|
)
|
||||||
enseignants = ", ".join(
|
enseignants = ", ".join(
|
||||||
@ -629,7 +638,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
"Code": M["module"]["code"] or "",
|
"Code": M["module"]["code"] or "",
|
||||||
"Module": M["module"]["abbrev"] or M["module"]["titre"],
|
"Module": M["module"]["abbrev"] or M["module"]["titre"],
|
||||||
"_Module_class": "scotext",
|
"_Module_class": "scotext",
|
||||||
"Inscrits": len(ModInscrits),
|
"Inscrits": len(mod_inscrits),
|
||||||
"Responsable": sco_users.user_info(M["responsable_id"])["nomprenom"],
|
"Responsable": sco_users.user_info(M["responsable_id"])["nomprenom"],
|
||||||
"_Responsable_class": "scotext",
|
"_Responsable_class": "scotext",
|
||||||
"Enseignants": enseignants,
|
"Enseignants": enseignants,
|
||||||
@ -648,10 +657,15 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
moduleimpl_id=M["moduleimpl_id"],
|
moduleimpl_id=M["moduleimpl_id"],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
R.append(l)
|
|
||||||
if M["module"]["coefficient"]:
|
if M["module"]["coefficient"]:
|
||||||
sum_coef += M["module"]["coefficient"]
|
sum_coef += M["module"]["coefficient"]
|
||||||
|
|
||||||
|
if with_parcours:
|
||||||
|
module = Module.query.get(M["module_id"])
|
||||||
|
l["parcours"] = ", ".join(sorted([pa.code for pa in module.parcours]))
|
||||||
|
|
||||||
|
R.append(l)
|
||||||
|
|
||||||
if with_evals:
|
if with_evals:
|
||||||
# Ajoute lignes pour evaluations
|
# Ajoute lignes pour evaluations
|
||||||
evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"])
|
evals = nt.get_mod_evaluation_etat_list(M["moduleimpl_id"])
|
||||||
@ -676,7 +690,10 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
|
|
||||||
sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
|
sums = {"_css_row_class": "moyenne sortbottom", "ects": sum_ects, "Coef.": sum_coef}
|
||||||
R.append(sums)
|
R.append(sums)
|
||||||
columns_ids = ["UE", "Code", "Module", "Coef."]
|
columns_ids = ["UE", "Code", "Module"]
|
||||||
|
if with_parcours:
|
||||||
|
columns_ids += ["parcours"]
|
||||||
|
columns_ids += ["Coef."]
|
||||||
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
if sco_preferences.get_preference("bul_show_ects", formsemestre_id):
|
||||||
columns_ids += ["ects"]
|
columns_ids += ["ects"]
|
||||||
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
columns_ids += ["Inscrits", "Responsable", "Enseignants"]
|
||||||
@ -696,6 +713,7 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
titles["description"] = ""
|
titles["description"] = ""
|
||||||
titles["coefficient"] = "Coef. éval."
|
titles["coefficient"] = "Coef. éval."
|
||||||
titles["evalcomplete_str"] = "Complète"
|
titles["evalcomplete_str"] = "Complète"
|
||||||
|
titles["parcours"] = "Parcours"
|
||||||
titles["publish_incomplete_str"] = "Toujours Utilisée"
|
titles["publish_incomplete_str"] = "Toujours Utilisée"
|
||||||
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois())
|
title = "%s %s" % (parcours.SESSION_NAME.capitalize(), formsemestre.titre_mois())
|
||||||
|
|
||||||
@ -720,21 +738,26 @@ def formsemestre_description_table(formsemestre_id, with_evals=False):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_description(formsemestre_id, format="html", with_evals=False):
|
def formsemestre_description(
|
||||||
|
formsemestre_id, format="html", with_evals=False, with_parcours=False
|
||||||
|
):
|
||||||
"""Description du semestre sous forme de table exportable
|
"""Description du semestre sous forme de table exportable
|
||||||
Liste des modules et de leurs coefficients
|
Liste des modules et de leurs coefficients
|
||||||
"""
|
"""
|
||||||
with_evals = int(with_evals)
|
with_evals = int(with_evals)
|
||||||
tab = formsemestre_description_table(formsemestre_id, with_evals=with_evals)
|
tab = formsemestre_description_table(
|
||||||
tab.html_before_table = """<form name="f" method="get" action="%s">
|
formsemestre_id, with_evals=with_evals, with_parcours=with_parcours
|
||||||
<input type="hidden" name="formsemestre_id" value="%s"></input>
|
|
||||||
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()" """ % (
|
|
||||||
request.base_url,
|
|
||||||
formsemestre_id,
|
|
||||||
)
|
)
|
||||||
if with_evals:
|
tab.html_before_table = f"""
|
||||||
tab.html_before_table += "checked"
|
<form name="f" method="get" action="{request.base_url}">
|
||||||
tab.html_before_table += ">indiquer les évaluations</input></form>"
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
|
||||||
|
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
|
||||||
|
{ "checked" if with_evals else "" }
|
||||||
|
>indiquer les évaluations</input>
|
||||||
|
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
|
||||||
|
{ "checked" if with_parcours else "" }
|
||||||
|
>indiquer les parcours BUT</input>
|
||||||
|
"""
|
||||||
|
|
||||||
return tab.make_page(format=format)
|
return tab.make_page(format=format)
|
||||||
|
|
||||||
@ -854,7 +877,7 @@ def _make_listes_sem(sem, with_absences=True):
|
|||||||
H.append(
|
H.append(
|
||||||
f"""<h4><a
|
f"""<h4><a
|
||||||
href="{
|
href="{
|
||||||
url_for("scolar.editPartitionForm",
|
url_for("scolar.edit_partition_form",
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
)
|
)
|
||||||
@ -929,10 +952,18 @@ def formsemestre_status_head(formsemestre_id=None, page_title=None):
|
|||||||
}</tt></b>)"""
|
}</tt></b>)"""
|
||||||
)
|
)
|
||||||
H.append("</td></tr>")
|
H.append("</td></tr>")
|
||||||
|
if sem.parcours:
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<tr><td class="fichetitre2">Parcours: </td>
|
||||||
|
<td style="color: blue;">{', '.join(parcours.code for parcours in sem.parcours)}</td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
|
evals = sco_evaluations.do_evaluation_etat_in_sem(formsemestre_id)
|
||||||
H.append(
|
H.append(
|
||||||
'<tr><td class="fichetitre2">Evaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
|
'<tr><td class="fichetitre2">Évaluations: </td><td> %(nb_evals_completes)s ok, %(nb_evals_en_cours)s en cours, %(nb_evals_vides)s vides'
|
||||||
% evals
|
% evals
|
||||||
)
|
)
|
||||||
if evals["last_modif"]:
|
if evals["last_modif"]:
|
||||||
@ -1175,7 +1206,7 @@ def formsemestre_tableau_modules(
|
|||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
'<td class="scotext"><a href="moduleimpl_status?moduleimpl_id=%s" title="%s" class="formsemestre_status_link">%s</a></td>'
|
||||||
% (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre)
|
% (modimpl["moduleimpl_id"], mod_descr, mod.abbrev or mod.titre or "")
|
||||||
)
|
)
|
||||||
H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(mod_inscrits))
|
H.append('<td class="formsemestre_status_inscrits">%s</td>' % len(mod_inscrits))
|
||||||
H.append(
|
H.append(
|
||||||
|
@ -111,7 +111,7 @@ def formsemestre_validation_etud_form(
|
|||||||
url_tableau = url_for(
|
url_tableau = url_for(
|
||||||
"notes.formsemestre_recapcomplet",
|
"notes.formsemestre_recapcomplet",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
modejury=1,
|
mode_jury=1,
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
selected_etudid=etudid, # va a la bonne ligne
|
selected_etudid=etudid, # va a la bonne ligne
|
||||||
)
|
)
|
||||||
@ -581,19 +581,27 @@ def formsemestre_recap_parcours_table(
|
|||||||
else:
|
else:
|
||||||
pm = plusminus % sem["formsemestre_id"]
|
pm = plusminus % sem["formsemestre_id"]
|
||||||
|
|
||||||
H.append(
|
inscr = formsemestre.etuds_inscriptions.get(etudid)
|
||||||
'<td class="rcp_type_sem" style="background-color:%s;">%s%s</td>'
|
parcours_name = (
|
||||||
% (bgcolor, num_sem, pm)
|
f' <span class="code_parcours">{inscr.parcour.code}</span>'
|
||||||
|
if (inscr and inscr.parcour)
|
||||||
|
else ""
|
||||||
)
|
)
|
||||||
H.append('<td class="datedebut">%(mois_debut)s</td>' % sem)
|
|
||||||
H.append(
|
H.append(
|
||||||
'<td class="rcp_titre_sem"><a class="formsemestre_status_link" href="%sformsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" title="Bulletin de notes">%s</a></td>'
|
f"""
|
||||||
% (a_url, sem["formsemestre_id"], etudid, sem["titreannee"])
|
<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></td>
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
if decision_sem:
|
if nt.is_apc:
|
||||||
|
H.append('<td class="rcp_but">BUT</td>')
|
||||||
|
elif decision_sem:
|
||||||
H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"])
|
H.append('<td class="rcp_dec">%s</td>' % decision_sem["code"])
|
||||||
else:
|
else:
|
||||||
H.append('<td colspan="%d"><em>en cours</em></td>')
|
H.append("<td><em>en cours</em></td>")
|
||||||
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
|
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
|
||||||
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
|
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
|
||||||
ues = nt.get_ues_stat_dict(filter_sport=True)
|
ues = nt.get_ues_stat_dict(filter_sport=True)
|
||||||
@ -973,7 +981,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
|||||||
H.append("</ul>")
|
H.append("</ul>")
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a href="{url_for('notes.formsemestre_recapcomplet',
|
f"""<a href="{url_for('notes.formsemestre_recapcomplet',
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
|
||||||
}">continuer</a>"""
|
}">continuer</a>"""
|
||||||
)
|
)
|
||||||
H.append(html_sco_header.sco_footer())
|
H.append(html_sco_header.sco_footer())
|
||||||
|
@ -76,10 +76,12 @@ partitionEditor = ndb.EditableTable(
|
|||||||
"numero",
|
"numero",
|
||||||
"bul_show_rank",
|
"bul_show_rank",
|
||||||
"show_in_lists",
|
"show_in_lists",
|
||||||
|
"editable",
|
||||||
),
|
),
|
||||||
input_formators={
|
input_formators={
|
||||||
"bul_show_rank": bool,
|
"bul_show_rank": bool,
|
||||||
"show_in_lists": bool,
|
"show_in_lists": bool,
|
||||||
|
"editable": bool,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -105,14 +107,19 @@ def get_group(group_id: int):
|
|||||||
return r[0]
|
return r[0]
|
||||||
|
|
||||||
|
|
||||||
def group_delete(group, force=False):
|
def group_delete(group_id: int):
|
||||||
"""Delete a group."""
|
"""Delete a group."""
|
||||||
# if not group['group_name'] and not force:
|
# if not group['group_name'] and not force:
|
||||||
# raise ValueError('cannot suppress this group')
|
# raise ValueError('cannot suppress this group')
|
||||||
# remove memberships:
|
# remove memberships:
|
||||||
ndb.SimpleQuery("DELETE FROM group_membership WHERE group_id=%(group_id)s", group)
|
ndb.SimpleQuery(
|
||||||
|
"DELETE FROM group_membership WHERE group_id=%(group_id)s",
|
||||||
|
{"group_id": group_id},
|
||||||
|
)
|
||||||
# delete group:
|
# delete group:
|
||||||
ndb.SimpleQuery("DELETE FROM group_descr WHERE id=%(group_id)s", group)
|
ndb.SimpleQuery(
|
||||||
|
"DELETE FROM group_descr WHERE id=%(group_id)s", {"group_id": group_id}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_partition(partition_id):
|
def get_partition(partition_id):
|
||||||
@ -264,6 +271,17 @@ def get_group_members(group_id, etat=None):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def check_group_name(group_name, partition, raiser=False):
|
||||||
|
"""If groupe name exists in partition : if raiser -> Raise ScoValueError else-> return true"""
|
||||||
|
exists = group_name in [g["group_name"] for g in get_partition_groups(partition)]
|
||||||
|
if exists:
|
||||||
|
if raiser:
|
||||||
|
raise ScoValueError("Le nom de groupe existe déjà dans la partition")
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# obsolete: sco_groups_view.DisplayedGroupsInfos
|
# obsolete: sco_groups_view.DisplayedGroupsInfos
|
||||||
# def get_groups_members(group_ids, etat=None):
|
# def get_groups_members(group_ids, etat=None):
|
||||||
# """Liste les étudiants d'une liste de groupes
|
# """Liste les étudiants d'une liste de groupes
|
||||||
@ -621,10 +639,12 @@ def comp_origin(etud, cur_sem):
|
|||||||
return "" # parcours normal, ne le signale pas
|
return "" # parcours normal, ne le signale pas
|
||||||
|
|
||||||
|
|
||||||
def set_group(etudid, group_id):
|
def set_group(etudid: int, group_id: int) -> bool:
|
||||||
"""Inscrit l'étudiant au groupe.
|
"""Inscrit l'étudiant au groupe.
|
||||||
Return True if ok, False si deja inscrit.
|
Return True if ok, False si deja inscrit.
|
||||||
Warning: don't check if group_id exists (the caller should check).
|
Warning:
|
||||||
|
- don't check if group_id exists (the caller should check).
|
||||||
|
- don't check if group's partition is editable
|
||||||
"""
|
"""
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||||
@ -686,7 +706,12 @@ def change_etud_group_in_partition(etudid, group_id, partition=None):
|
|||||||
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
% (formsemestre_id, partition["partition_name"], group["group_name"]),
|
||||||
)
|
)
|
||||||
cnx.commit()
|
cnx.commit()
|
||||||
# 4- invalidate cache
|
|
||||||
|
# 5- Update parcours
|
||||||
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
|
|
||||||
|
# 6- invalidate cache
|
||||||
sco_cache.invalidate_formsemestre(
|
sco_cache.invalidate_formsemestre(
|
||||||
formsemestre_id=formsemestre_id
|
formsemestre_id=formsemestre_id
|
||||||
) # > change etud group
|
) # > change etud group
|
||||||
@ -698,14 +723,28 @@ def setGroups(
|
|||||||
groupsToCreate="", # name and members of new groups
|
groupsToCreate="", # name and members of new groups
|
||||||
groupsToDelete="", # groups to delete
|
groupsToDelete="", # groups to delete
|
||||||
):
|
):
|
||||||
"""Affect groups (Ajax request)
|
"""Affect groups (Ajax request): renvoie du XML
|
||||||
groupsLists: lignes de la forme "group_id;etudid;...\n"
|
groupsLists: lignes de la forme "group_id;etudid;...\n"
|
||||||
groupsToCreate: lignes "group_name;etudid;...\n"
|
groupsToCreate: lignes "group_name;etudid;...\n"
|
||||||
groupsToDelete: group_id;group_id;...
|
groupsToDelete: group_id;group_id;...
|
||||||
|
|
||||||
|
Ne peux pas modifier les groupes des partitions non éditables.
|
||||||
"""
|
"""
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
|
|
||||||
|
def xml_error(msg, code=404):
|
||||||
|
data = (
|
||||||
|
f'<?xml version="1.0" encoding="utf-8"?><response>Error: {msg}</response>'
|
||||||
|
)
|
||||||
|
response = make_response(data, code)
|
||||||
|
response.headers["Content-Type"] = scu.XML_MIMETYPE
|
||||||
|
return response
|
||||||
|
|
||||||
partition = get_partition(partition_id)
|
partition = get_partition(partition_id)
|
||||||
|
if not partition["groups_editable"]:
|
||||||
|
msg = "setGroups: partition non editable"
|
||||||
|
log(msg)
|
||||||
|
return xml_error(msg, code=403)
|
||||||
formsemestre_id = partition["formsemestre_id"]
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
@ -727,8 +766,8 @@ def setGroups(
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
group_id = int(group_id)
|
group_id = int(group_id)
|
||||||
except ValueError as exc:
|
except ValueError:
|
||||||
log("setGroups: ignoring invalid group_id={group_id}")
|
log(f"setGroups: ignoring invalid group_id={group_id}")
|
||||||
continue
|
continue
|
||||||
group = get_group(group_id)
|
group = get_group(group_id)
|
||||||
# Anciens membres du groupe:
|
# Anciens membres du groupe:
|
||||||
@ -778,6 +817,10 @@ def setGroups(
|
|||||||
for etudid in fs[1:-1]:
|
for etudid in fs[1:-1]:
|
||||||
change_etud_group_in_partition(etudid, group_id, partition)
|
change_etud_group_in_partition(etudid, group_id, partition)
|
||||||
|
|
||||||
|
# Update parcours
|
||||||
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
|
|
||||||
data = (
|
data = (
|
||||||
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
'<?xml version="1.0" encoding="utf-8"?><response>Groupes enregistrés</response>'
|
||||||
)
|
)
|
||||||
@ -798,15 +841,15 @@ def create_group(partition_id, group_name="", default=False) -> int:
|
|||||||
if not group_name and not default:
|
if not group_name and not default:
|
||||||
raise ValueError("invalid group name: ()")
|
raise ValueError("invalid group name: ()")
|
||||||
# checkGroupName(group_name)
|
# checkGroupName(group_name)
|
||||||
if group_name in [g["group_name"] for g in get_partition_groups(partition)]:
|
if check_group_name(group_name, partition):
|
||||||
raise ValueError(
|
raise ScoValueError(
|
||||||
"group_name %s already exists in partition" % group_name
|
f"group_name {group_name} already exists in partition"
|
||||||
) # XXX FIX: incorrect error handling (in AJAX)
|
) # XXX FIX: incorrect error handling (in AJAX)
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
group_id = groupEditor.create(
|
group_id = groupEditor.create(
|
||||||
cnx, {"partition_id": partition_id, "group_name": group_name}
|
cnx, {"partition_id": partition_id, "group_name": group_name}
|
||||||
)
|
)
|
||||||
log("create_group: created group_id=%s" % group_id)
|
log("create_group: created group_id={group_id}")
|
||||||
#
|
#
|
||||||
return group_id
|
return group_id
|
||||||
|
|
||||||
@ -817,21 +860,18 @@ def delete_group(group_id, partition_id=None):
|
|||||||
affectation aux groupes)
|
affectation aux groupes)
|
||||||
partition_id est optionnel et ne sert que pour verifier que le groupe
|
partition_id est optionnel et ne sert que pour verifier que le groupe
|
||||||
est bien dans cette partition.
|
est bien dans cette partition.
|
||||||
|
S'il s'agit d'un groupe de parcours, affecte l'inscription des étudiants aux parcours.
|
||||||
"""
|
"""
|
||||||
group = get_group(group_id)
|
group = GroupDescr.query.get_or_404(group_id)
|
||||||
if partition_id:
|
if partition_id:
|
||||||
if partition_id != group["partition_id"]:
|
if partition_id != group.partition_id:
|
||||||
raise ValueError("inconsistent partition/group")
|
raise ValueError("inconsistent partition/group")
|
||||||
else:
|
if not sco_permissions_check.can_change_groups(group.partition.formsemestre_id):
|
||||||
partition_id = group["partition_id"]
|
|
||||||
partition = get_partition(partition_id)
|
|
||||||
if not sco_permissions_check.can_change_groups(partition["formsemestre_id"]):
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
log(
|
log(f"delete_group: group={group} partition={group.partition}")
|
||||||
"delete_group: group_id=%s group_name=%s partition_name=%s"
|
formsemestre = group.partition.formsemestre
|
||||||
% (group_id, group["group_name"], partition["partition_name"])
|
group_delete(group.id)
|
||||||
)
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
group_delete(group)
|
|
||||||
|
|
||||||
|
|
||||||
def partition_create(
|
def partition_create(
|
||||||
@ -881,7 +921,7 @@ def partition_create(
|
|||||||
if redirect:
|
if redirect:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"scolar.editPartitionForm",
|
"scolar.edit_partition_form",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
)
|
)
|
||||||
@ -900,11 +940,12 @@ def get_arrow_icons_tags():
|
|||||||
return arrow_up, arrow_down, arrow_none
|
return arrow_up, arrow_down, arrow_none
|
||||||
|
|
||||||
|
|
||||||
def editPartitionForm(formsemestre_id=None):
|
def edit_partition_form(formsemestre_id=None):
|
||||||
"""Form to create/suppress partitions"""
|
"""Form to create/suppress partitions"""
|
||||||
# ad-hoc form
|
# ad-hoc form
|
||||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
partitions = get_partitions_list(formsemestre_id)
|
partitions = get_partitions_list(formsemestre_id)
|
||||||
arrow_up, arrow_down, arrow_none = get_arrow_icons_tags()
|
arrow_up, arrow_down, arrow_none = get_arrow_icons_tags()
|
||||||
suppricon = scu.icontag(
|
suppricon = scu.icontag(
|
||||||
@ -914,7 +955,7 @@ def editPartitionForm(formsemestre_id=None):
|
|||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title="Partitions...",
|
page_title="Partitions...",
|
||||||
javascripts=["js/editPartitionForm.js"],
|
javascripts=["js/edit_partition_form.js"],
|
||||||
),
|
),
|
||||||
# limite à SHORT_STR_LEN
|
# limite à SHORT_STR_LEN
|
||||||
r"""<script type="text/javascript">
|
r"""<script type="text/javascript">
|
||||||
@ -966,14 +1007,19 @@ def editPartitionForm(formsemestre_id=None):
|
|||||||
for group in get_partition_groups(p)
|
for group in get_partition_groups(p)
|
||||||
]
|
]
|
||||||
H.append(", ".join(lg))
|
H.append(", ".join(lg))
|
||||||
H.append(
|
H.append("""</td><td>""")
|
||||||
f"""</td><td><a class="stdlink" href="{
|
if p["groups_editable"]:
|
||||||
url_for("scolar.affect_groups",
|
H.append(
|
||||||
scodoc_dept=g.scodoc_dept,
|
f"""<a class="stdlink" href="{
|
||||||
partition_id=p["partition_id"])
|
url_for("scolar.affect_groups",
|
||||||
}">répartir</a></td>
|
scodoc_dept=g.scodoc_dept,
|
||||||
"""
|
partition_id=p["partition_id"])
|
||||||
)
|
}">répartir</a></td>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
H.append("""non éditable""")
|
||||||
|
H.append("""</td>""")
|
||||||
H.append(
|
H.append(
|
||||||
'<td><a class="stdlink" href="partition_rename?partition_id=%s">renommer</a></td>'
|
'<td><a class="stdlink" href="partition_rename?partition_id=%s">renommer</a></td>'
|
||||||
% p["partition_id"]
|
% p["partition_id"]
|
||||||
@ -1000,28 +1046,49 @@ def editPartitionForm(formsemestre_id=None):
|
|||||||
#
|
#
|
||||||
H.append("</tr>")
|
H.append("</tr>")
|
||||||
H.append("</table>")
|
H.append("</table>")
|
||||||
H.append('<div class="form_rename_partition">')
|
|
||||||
H.append(
|
H.append(
|
||||||
'<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
|
f"""<div class="form_rename_partition">
|
||||||
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"/>
|
||||||
|
<input type="hidden" name="redirect" value="1"/>
|
||||||
|
<input type="text" name="partition_name" size="12" onkeyup="checkname();"/>
|
||||||
|
<input type="submit" name="ok" disabled="1" value="Nouvelle partition"/>
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
H.append('<input type="hidden" name="redirect" value="1"/>')
|
if formsemestre.formation.is_apc() and scu.PARTITION_PARCOURS not in (
|
||||||
|
p["partition_name"] for p in partitions
|
||||||
|
):
|
||||||
|
# propose création partition "Parcours"
|
||||||
|
H.append(
|
||||||
|
f"""
|
||||||
|
<div style="margin-top: 10px"><a class="stdlink" href="{
|
||||||
|
url_for("scolar.create_partition_parcours", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||||
|
}">Créer une partition avec un groupe par parcours (BUT)</a>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<input type="text" name="partition_name" size="12" onkeyup="checkname();"/>'
|
"""
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
H.append('<input type="submit" name="ok" disabled="1" value="Nouvelle partition"/>')
|
|
||||||
H.append("</div></form>")
|
|
||||||
H.append(
|
H.append(
|
||||||
"""<div class="help">
|
"""<div class="help">
|
||||||
<p>Les partitions sont des découpages de l'ensemble des étudiants.
|
<p>Les partitions sont des découpages de l'ensemble des étudiants.
|
||||||
Par exemple, les "groupes de TD" sont une partition.
|
Par exemple, les "groupes de TD" sont une partition.
|
||||||
On peut créer autant de partitions que nécessaire.
|
On peut créer autant de partitions que nécessaire.
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Dans chaque partition, un nombre de groupes quelconque peuvent être créés (suivre le lien "répartir").
|
<li>Dans chaque partition, un nombre de groupes quelconque peuvent
|
||||||
<li>On peut faire afficher le classement de l'étudiant dans son groupe d'une partition en cochant "afficher rang sur bulletins" (ainsi, on peut afficher le classement en groupes de TD mais pas en groupe de TP, si ce sont deux partitions).
|
être créés (suivre le lien "répartir").
|
||||||
|
<li>On peut faire afficher le classement de l'étudiant dans son
|
||||||
|
groupe d'une partition en cochant "afficher rang sur bulletins"
|
||||||
|
(ainsi, on peut afficher le classement en groupes de TD mais pas en
|
||||||
|
groupe de TP, si ce sont deux partitions).
|
||||||
|
</li>
|
||||||
|
<li>Décocher "afficher sur noms groupes" pour ne pas que cette partition
|
||||||
|
apparaisse dans les noms de groupes
|
||||||
</li>
|
</li>
|
||||||
<li>Décocher "afficher sur noms groupes" pour ne pas que cette partition apparaisse dans les noms de groupes
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
@ -1052,11 +1119,14 @@ def partition_set_attr(partition_id, attr, value):
|
|||||||
|
|
||||||
def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
|
def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=False):
|
||||||
"""Suppress a partition (and all groups within).
|
"""Suppress a partition (and all groups within).
|
||||||
default partition cannot be suppressed (unless force)"""
|
The default partition cannot be suppressed (unless force).
|
||||||
|
Si la partition de parcours est supprimée, les étudiants sont désinscrits des parcours.
|
||||||
|
"""
|
||||||
partition = get_partition(partition_id)
|
partition = get_partition(partition_id)
|
||||||
formsemestre_id = partition["formsemestre_id"]
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
if not sco_permissions_check.can_change_groups(formsemestre_id):
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
|
||||||
if not partition["partition_name"] and not force:
|
if not partition["partition_name"] and not force:
|
||||||
raise ValueError("cannot suppress this partition")
|
raise ValueError("cannot suppress this partition")
|
||||||
@ -1075,21 +1145,23 @@ def partition_delete(partition_id, force=False, redirect=1, dialog_confirmed=Fal
|
|||||||
"""
|
"""
|
||||||
% (partition["partition_name"], grnames),
|
% (partition["partition_name"], grnames),
|
||||||
dest_url="",
|
dest_url="",
|
||||||
cancel_url="editPartitionForm?formsemestre_id=%s" % formsemestre_id,
|
cancel_url="edit_partition_form?formsemestre_id=%s" % formsemestre_id,
|
||||||
parameters={"redirect": redirect, "partition_id": partition_id},
|
parameters={"redirect": redirect, "partition_id": partition_id},
|
||||||
)
|
)
|
||||||
|
|
||||||
log("partition_delete: partition_id=%s" % partition_id)
|
log("partition_delete: partition_id=%s" % partition_id)
|
||||||
# 1- groups
|
# 1- groups
|
||||||
for group in groups:
|
for group in groups:
|
||||||
group_delete(group, force=force)
|
group_delete(group["group_id"])
|
||||||
# 2- partition
|
# 2- partition
|
||||||
partitionEditor.delete(cnx, partition_id)
|
partitionEditor.delete(cnx, partition_id)
|
||||||
|
|
||||||
|
formsemestre.update_inscriptions_parcours_from_groups()
|
||||||
|
|
||||||
# redirect to partition edit page:
|
# redirect to partition edit page:
|
||||||
if redirect:
|
if redirect:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
"editPartitionForm?formsemestre_id=" + str(formsemestre_id)
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1146,7 +1218,7 @@ def partition_move(partition_id, after=0, redirect=1):
|
|||||||
# redirect to partition edit page:
|
# redirect to partition edit page:
|
||||||
if redirect:
|
if redirect:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
"editPartitionForm?formsemestre_id=" + str(formsemestre_id)
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1169,7 +1241,8 @@ def partition_rename(partition_id):
|
|||||||
"default": partition["partition_name"],
|
"default": partition["partition_name"],
|
||||||
"allow_null": False,
|
"allow_null": False,
|
||||||
"size": 12,
|
"size": 12,
|
||||||
"validator": lambda val, _: len(val) < SHORT_STR_LEN,
|
"validator": lambda val, _: (len(val) < SHORT_STR_LEN)
|
||||||
|
and (val != scu.PARTITION_PARCOURS),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1186,7 +1259,7 @@ def partition_rename(partition_id):
|
|||||||
)
|
)
|
||||||
elif tf[0] == -1:
|
elif tf[0] == -1:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
"editPartitionForm?formsemestre_id=" + str(formsemestre_id)
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# form submission
|
# form submission
|
||||||
@ -1201,6 +1274,8 @@ def partition_set_name(partition_id, partition_name, redirect=1):
|
|||||||
partition = get_partition(partition_id)
|
partition = get_partition(partition_id)
|
||||||
if partition["partition_name"] is None:
|
if partition["partition_name"] is None:
|
||||||
raise ValueError("can't set a name to default partition")
|
raise ValueError("can't set a name to default partition")
|
||||||
|
if partition_name == scu.PARTITION_PARCOURS:
|
||||||
|
raise ScoValueError(f"nom de partition {scu.PARTITION_PARCOURS} réservé.")
|
||||||
formsemestre_id = partition["formsemestre_id"]
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
|
|
||||||
# check unicity
|
# check unicity
|
||||||
@ -1227,7 +1302,7 @@ def partition_set_name(partition_id, partition_name, redirect=1):
|
|||||||
# redirect to partition edit page:
|
# redirect to partition edit page:
|
||||||
if redirect:
|
if redirect:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
"editPartitionForm?formsemestre_id=" + str(formsemestre_id)
|
"edit_partition_form?formsemestre_id=" + str(formsemestre_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1246,7 +1321,7 @@ def group_set_name(group_id, group_name, redirect=True):
|
|||||||
redirect = int(redirect)
|
redirect = int(redirect)
|
||||||
cnx = ndb.GetDBConnexion()
|
cnx = ndb.GetDBConnexion()
|
||||||
groupEditor.edit(cnx, {"group_id": group_id, "group_name": group_name})
|
groupEditor.edit(cnx, {"group_id": group_id, "group_name": group_name})
|
||||||
|
check_group_name(group_name, get_partition(group["partition_id"]), True)
|
||||||
# redirect to partition edit page:
|
# redirect to partition edit page:
|
||||||
if redirect:
|
if redirect:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
@ -1312,6 +1387,8 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
|
|
||||||
partition = get_partition(partition_id)
|
partition = get_partition(partition_id)
|
||||||
|
if not partition["groups_editable"]:
|
||||||
|
raise AccessDenied("Partition non éditable")
|
||||||
formsemestre_id = partition["formsemestre_id"]
|
formsemestre_id = partition["formsemestre_id"]
|
||||||
formsemestre = FormSemestre.query.get(formsemestre_id)
|
formsemestre = FormSemestre.query.get(formsemestre_id)
|
||||||
# renvoie sur page édition groupes
|
# renvoie sur page édition groupes
|
||||||
@ -1368,7 +1445,7 @@ def groups_auto_repartition(partition_id=None):
|
|||||||
group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
|
group_names = sorted(set([x.strip() for x in groupNames.split(",")]))
|
||||||
# Détruit les groupes existant de cette partition
|
# Détruit les groupes existant de cette partition
|
||||||
for old_group in get_partition_groups(partition):
|
for old_group in get_partition_groups(partition):
|
||||||
group_delete(old_group)
|
group_delete(old_group["group_id"])
|
||||||
# Crée les nouveaux groupes
|
# Crée les nouveaux groupes
|
||||||
group_ids = []
|
group_ids = []
|
||||||
for group_name in group_names:
|
for group_name in group_names:
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"""
|
"""
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
|
from app.models import Partition
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_exceptions import AccessDenied
|
from app.scodoc.sco_exceptions import AccessDenied
|
||||||
@ -39,10 +40,11 @@ def affect_groups(partition_id):
|
|||||||
Permet aussi la creation et la suppression de groupes.
|
Permet aussi la creation et la suppression de groupes.
|
||||||
"""
|
"""
|
||||||
# réécrit pour 9.0.47 avec un template
|
# réécrit pour 9.0.47 avec un template
|
||||||
partition = sco_groups.get_partition(partition_id)
|
partition = Partition.query.get_or_404(partition_id)
|
||||||
formsemestre_id = partition["formsemestre_id"]
|
formsemestre_id = partition.formsemestre_id
|
||||||
if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
|
if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
|
||||||
raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
|
raise AccessDenied("vous n'avez pas la permission de modifier les groupes")
|
||||||
|
partition.formsemestre.setup_parcours_groups()
|
||||||
return render_template(
|
return render_template(
|
||||||
"scolar/affect_groups.html",
|
"scolar/affect_groups.html",
|
||||||
sco_header=html_sco_header.sco_header(
|
sco_header=html_sco_header.sco_header(
|
||||||
@ -52,8 +54,9 @@ def affect_groups(partition_id):
|
|||||||
),
|
),
|
||||||
sco_footer=html_sco_header.sco_footer(),
|
sco_footer=html_sco_header.sco_footer(),
|
||||||
partition=partition,
|
partition=partition,
|
||||||
partitions_list=sco_groups.get_partitions_list(
|
# Liste des partitions sans celle par defaut:
|
||||||
formsemestre_id, with_default=False
|
partitions_list=partition.formsemestre.partitions.filter(
|
||||||
|
Partition.partition_name != None
|
||||||
),
|
),
|
||||||
formsemestre_id=formsemestre_id,
|
formsemestre_id=formsemestre_id,
|
||||||
)
|
)
|
||||||
|
@ -33,14 +33,13 @@ import io
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from flask import g, url_for
|
from flask import g, url_for
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
from app import log
|
||||||
from app.models import ScolarNews
|
from app.models import ScolarNews, GroupDescr
|
||||||
|
|
||||||
from app.scodoc.sco_excel import COLORS
|
from app.scodoc.sco_excel import COLORS
|
||||||
from app.scodoc.sco_formsemestre_inscriptions import (
|
from app.scodoc.sco_formsemestre_inscriptions import (
|
||||||
@ -718,9 +717,17 @@ def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None
|
|||||||
)
|
)
|
||||||
|
|
||||||
for group_id in group_ids:
|
for group_id in group_ids:
|
||||||
sco_groups.change_etud_group_in_partition(
|
group = GroupDescr.query.get(group_id)
|
||||||
args["etudid"], group_id
|
if group.partition.groups_editable:
|
||||||
)
|
sco_groups.change_etud_group_in_partition(
|
||||||
|
args["etudid"], group_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log("scolars_import_admission: partition non editable")
|
||||||
|
diag.append(
|
||||||
|
f"Attention: partition {group.partition} non editable (ignorée)"
|
||||||
|
)
|
||||||
|
|
||||||
#
|
#
|
||||||
diag.append("import de %s" % (etud["nomprenom"]))
|
diag.append("import de %s" % (etud["nomprenom"]))
|
||||||
n_import += 1
|
n_import += 1
|
||||||
|
@ -150,22 +150,22 @@ def import_users(users, force=""):
|
|||||||
* ok: import ok or aborted
|
* ok: import ok or aborted
|
||||||
* messages: the list of messages
|
* messages: the list of messages
|
||||||
* the # of users created
|
* the # of users created
|
||||||
"""
|
|
||||||
""" Implémentation:
|
Implémentation:
|
||||||
Pour chaque utilisateur à créer:
|
Pour chaque utilisateur à créer:
|
||||||
* vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
|
* vérifier données (y compris que le même nom d'utilisateur n'est pas utilisé plusieurs fois)
|
||||||
* générer mot de passe aléatoire
|
* générer mot de passe aléatoire
|
||||||
* créer utilisateur et mettre le mot de passe
|
* créer utilisateur et mettre le mot de passe
|
||||||
* envoyer mot de passe par mail
|
* envoyer mot de passe par mail
|
||||||
Les utilisateurs à créer sont stockés dans un dictionnaire.
|
Les utilisateurs à créer sont stockés dans un dictionnaire.
|
||||||
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
|
L'ajout effectif ne se fait qu'en fin de fonction si aucune erreur n'a été détectée
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
created = {} # uid créés
|
||||||
if len(users) == 0:
|
if len(users) == 0:
|
||||||
import_ok = False
|
import_ok = False
|
||||||
msg_list = ["Feuille vide ou illisible"]
|
msg_list = ["Feuille vide ou illisible"]
|
||||||
else:
|
else:
|
||||||
created = {} # liste de uid créés
|
|
||||||
msg_list = []
|
msg_list = []
|
||||||
line = 1 # start from excel line #2
|
line = 1 # start from excel line #2
|
||||||
import_ok = True
|
import_ok = True
|
||||||
@ -217,7 +217,7 @@ def import_users(users, force=""):
|
|||||||
else:
|
else:
|
||||||
import_ok = False
|
import_ok = False
|
||||||
except ScoValueError as value_error:
|
except ScoValueError as value_error:
|
||||||
log("import_users: exception: abort create %s" % str(created.keys()))
|
log(f"import_users: exception: abort create {str(created.keys())}")
|
||||||
raise ScoValueError(msg) from value_error
|
raise ScoValueError(msg) from value_error
|
||||||
if import_ok:
|
if import_ok:
|
||||||
for u in created.values():
|
for u in created.values():
|
||||||
@ -228,7 +228,7 @@ def import_users(users, force=""):
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
mail_password(u)
|
mail_password(u)
|
||||||
else:
|
else:
|
||||||
created = [] # reset # of created users to 0
|
created = {} # reset # of created users to 0
|
||||||
return import_ok, msg_list, len(created)
|
return import_ok, msg_list, len(created)
|
||||||
|
|
||||||
|
|
||||||
|
@ -219,11 +219,12 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
|
|||||||
|
|
||||||
# inscrit aux groupes
|
# inscrit aux groupes
|
||||||
for partition_group in partition_groups:
|
for partition_group in partition_groups:
|
||||||
sco_groups.change_etud_group_in_partition(
|
if partition_group["groups_editable"]:
|
||||||
etudid,
|
sco_groups.change_etud_group_in_partition(
|
||||||
partition_group["group_id"],
|
etudid,
|
||||||
partition_group,
|
partition_group["group_id"],
|
||||||
)
|
partition_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def do_desinscrit(sem, etudids):
|
def do_desinscrit(sem, etudids):
|
||||||
|
@ -121,7 +121,8 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
|
|||||||
:return: le résultat de la recherche ou None si aucune image trouvée
|
:return: le résultat de la recherche ou None si aucune image trouvée
|
||||||
"""
|
"""
|
||||||
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
|
allowed_ext = "|".join(scu.LOGOS_IMAGES_ALLOWED_TYPES)
|
||||||
filename_parser = re.compile(f"{prefix}([^.]*).({allowed_ext})")
|
# parse filename 'logo_<logoname>.<ext> . be carefull: logoname may include '.'
|
||||||
|
filename_parser = re.compile(f"{prefix}(([^.]*.)+)({allowed_ext})")
|
||||||
logos = {}
|
logos = {}
|
||||||
path_dir = Path(scu.SCODOC_LOGOS_DIR)
|
path_dir = Path(scu.SCODOC_LOGOS_DIR)
|
||||||
if dept_id:
|
if dept_id:
|
||||||
@ -135,7 +136,7 @@ def _list_dept_logos(dept_id=None, prefix=scu.LOGO_FILE_PREFIX):
|
|||||||
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
|
if os.access(path_dir.joinpath(entry).absolute(), os.R_OK):
|
||||||
result = filename_parser.match(entry.name)
|
result = filename_parser.match(entry.name)
|
||||||
if result:
|
if result:
|
||||||
logoname = result.group(1)
|
logoname = result.group(1)[:-1] # retreive logoname from filename (less final dot)
|
||||||
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
|
logos[logoname] = Logo(logoname=logoname, dept_id=dept_id).select()
|
||||||
return logos if len(logos.keys()) > 0 else None
|
return logos if len(logos.keys()) > 0 else None
|
||||||
|
|
||||||
@ -191,6 +192,9 @@ class Logo:
|
|||||||
)
|
)
|
||||||
self.mm = "Not initialized: call the select or create function before access"
|
self.mm = "Not initialized: call the select or create function before access"
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Logo(logoname='{self.logoname}', filename='{self.filename}')"
|
||||||
|
|
||||||
def _set_format(self, fmt):
|
def _set_format(self, fmt):
|
||||||
self.suffix = fmt
|
self.suffix = fmt
|
||||||
self.filepath = self.basepath + "." + fmt
|
self.filepath = self.basepath + "." + fmt
|
||||||
|
@ -192,10 +192,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
"""Tableau de bord module (liste des evaluations etc)"""
|
"""Tableau de bord module (liste des evaluations etc)"""
|
||||||
if not isinstance(moduleimpl_id, int):
|
if not isinstance(moduleimpl_id, int):
|
||||||
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
||||||
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
M = modimpl.to_dict()
|
M = modimpl.to_dict()
|
||||||
formsemestre_id = M["formsemestre_id"]
|
formsemestre_id = modimpl.formsemestre_id
|
||||||
Mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
||||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||||
|
@ -36,6 +36,7 @@ from flask_login import current_user
|
|||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.but import jury_but_view
|
||||||
from app.models.etudiants import make_etud_args
|
from app.models.etudiants import make_etud_args
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
@ -445,6 +446,10 @@ def ficheEtud(etudid=None):
|
|||||||
else:
|
else:
|
||||||
info["groupes_row"] = ""
|
info["groupes_row"] = ""
|
||||||
info["menus_etud"] = menus_etud(etudid)
|
info["menus_etud"] = menus_etud(etudid)
|
||||||
|
|
||||||
|
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche...
|
||||||
|
info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
|
||||||
|
|
||||||
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
||||||
<div class="ficheEtud" id="ficheEtud"><table>
|
<div class="ficheEtud" id="ficheEtud"><table>
|
||||||
<tr><td>
|
<tr><td>
|
||||||
@ -477,6 +482,8 @@ def ficheEtud(etudid=None):
|
|||||||
|
|
||||||
%(inscriptions_mkup)s
|
%(inscriptions_mkup)s
|
||||||
|
|
||||||
|
%(but_infos_mkup)s
|
||||||
|
|
||||||
<div class="ficheadmission">
|
<div class="ficheadmission">
|
||||||
%(adm_data)s
|
%(adm_data)s
|
||||||
|
|
||||||
@ -513,7 +520,7 @@ def ficheEtud(etudid=None):
|
|||||||
"""
|
"""
|
||||||
header = html_sco_header.sco_header(
|
header = html_sco_header.sco_header(
|
||||||
page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
|
page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
|
||||||
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css"],
|
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css"],
|
||||||
javascripts=[
|
javascripts=[
|
||||||
"libjs/jinplace-1.2.1.min.js",
|
"libjs/jinplace-1.2.1.min.js",
|
||||||
"js/ue_list.js",
|
"js/ue_list.js",
|
||||||
|
@ -109,10 +109,14 @@ class DecisionSem(object):
|
|||||||
# log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
|
# log('%s: %s %s %s %s %s' % (self.codechoice,code_etat,new_code_prev,formsemestre_id_utilise_pour_compenser,devenir,assiduite) )
|
||||||
|
|
||||||
|
|
||||||
def SituationEtudParcours(etud, formsemestre_id):
|
def SituationEtudParcours(etud: dict, formsemestre_id: int):
|
||||||
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
|
"""renvoie une instance de SituationEtudParcours (ou sous-classe spécialisée)"""
|
||||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
|
||||||
|
# if formsemestre.formation.is_apc():
|
||||||
|
# return SituationEtudParcoursBUT(etud, formsemestre_id, nt)
|
||||||
|
|
||||||
parcours = nt.parcours
|
parcours = nt.parcours
|
||||||
#
|
#
|
||||||
if parcours.ECTS_ONLY:
|
if parcours.ECTS_ONLY:
|
||||||
@ -121,10 +125,10 @@ def SituationEtudParcours(etud, formsemestre_id):
|
|||||||
return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
|
return SituationEtudParcoursGeneric(etud, formsemestre_id, nt)
|
||||||
|
|
||||||
|
|
||||||
class SituationEtudParcoursGeneric(object):
|
class SituationEtudParcoursGeneric:
|
||||||
"Semestre dans un parcours"
|
"Semestre dans un parcours"
|
||||||
|
|
||||||
def __init__(self, etud, formsemestre_id, nt):
|
def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
|
||||||
"""
|
"""
|
||||||
etud: dict filled by fill_etuds_info()
|
etud: dict filled by fill_etuds_info()
|
||||||
"""
|
"""
|
||||||
@ -132,7 +136,7 @@ class SituationEtudParcoursGeneric(object):
|
|||||||
self.etudid = etud["etudid"]
|
self.etudid = etud["etudid"]
|
||||||
self.formsemestre_id = formsemestre_id
|
self.formsemestre_id = formsemestre_id
|
||||||
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
self.nt = nt
|
self.nt: NotesTableCompat = nt
|
||||||
self.formation = self.nt.formsemestre.formation
|
self.formation = self.nt.formsemestre.formation
|
||||||
self.parcours = self.nt.parcours
|
self.parcours = self.nt.parcours
|
||||||
# Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)
|
# Ce semestre est-il le dernier de la formation ? (e.g. semestre 4 du DUT)
|
||||||
|
@ -52,7 +52,8 @@ from reportlab.platypus import Paragraph
|
|||||||
from reportlab.lib import styles
|
from reportlab.lib import styles
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import url_for, g, request
|
from flask import flash, redirect, url_for
|
||||||
|
from flask import g, request
|
||||||
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
@ -274,7 +275,10 @@ def dict_pvjury(
|
|||||||
_codes.add(ue["ue_code"])
|
_codes.add(ue["ue_code"])
|
||||||
|
|
||||||
d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq])
|
d["decisions_ue_descr"] = ", ".join([ue["acronyme"] for ue in ue_uniq])
|
||||||
d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"])
|
if nt.is_apc:
|
||||||
|
d["decision_sem_descr"] = "" # pas de validation de semestre en BUT
|
||||||
|
else:
|
||||||
|
d["decision_sem_descr"] = _descr_decision_sem(d["etat"], d["decision_sem"])
|
||||||
|
|
||||||
d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription(
|
d["autorisations"] = sco_parcours_dut.formsemestre_get_autorisation_inscription(
|
||||||
etudid, formsemestre_id
|
etudid, formsemestre_id
|
||||||
@ -492,9 +496,18 @@ def pvjury_table(
|
|||||||
|
|
||||||
|
|
||||||
def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
|
def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
|
||||||
"""Page récapitulant les décisions de jury
|
"""Page récapitulant les décisions de jury"""
|
||||||
dpv: result of dict_pvjury
|
|
||||||
"""
|
# Bretelle provisoire pour BUT 9.3.0
|
||||||
|
# XXX TODO
|
||||||
|
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
|
if formsemestre.formation.is_apc() and formsemestre.semestre_id % 2 == 0:
|
||||||
|
from app.but import jury_but_recap
|
||||||
|
|
||||||
|
return jury_but_recap.formsemestre_saisie_jury_but(
|
||||||
|
formsemestre, read_only=True, mode="recap"
|
||||||
|
)
|
||||||
|
# /XXX
|
||||||
footer = html_sco_header.sco_footer()
|
footer = html_sco_header.sco_footer()
|
||||||
|
|
||||||
dpv = dict_pvjury(formsemestre_id, with_prev=True)
|
dpv = dict_pvjury(formsemestre_id, with_prev=True)
|
||||||
@ -786,7 +799,7 @@ def descrform_pvjury(sem):
|
|||||||
|
|
||||||
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
||||||
"Lettres avis jury en PDF"
|
"Lettres avis jury en PDF"
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||||
if not group_ids:
|
if not group_ids:
|
||||||
# tous les inscrits du semestre
|
# tous les inscrits du semestre
|
||||||
group_ids = [sco_groups.get_default_group(formsemestre_id)]
|
group_ids = [sco_groups.get_default_group(formsemestre_id)]
|
||||||
@ -802,10 +815,15 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
|||||||
cssstyles=sco_groups_view.CSSSTYLES,
|
cssstyles=sco_groups_view.CSSSTYLES,
|
||||||
init_qtip=True,
|
init_qtip=True,
|
||||||
),
|
),
|
||||||
"""<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
|
f"""<p class="help">Utiliser cette page pour éditer des versions provisoires des PV.
|
||||||
<span class="fontred">Il est recommandé d'archiver les versions définitives: <a href="formsemestre_archive?formsemestre_id=%s">voir cette page</a></span></p>
|
<span class="fontred">Il est recommandé d'archiver les versions définitives: <a
|
||||||
"""
|
href="{url_for(
|
||||||
% formsemestre_id,
|
"notes.formsemestre_archive",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
)}"
|
||||||
|
>voir cette page</a></span></p>
|
||||||
|
""",
|
||||||
]
|
]
|
||||||
F = html_sco_header.sco_footer()
|
F = html_sco_header.sco_footer()
|
||||||
descr = descrform_lettres_individuelles()
|
descr = descrform_lettres_individuelles()
|
||||||
@ -830,7 +848,11 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
|||||||
return "\n".join(H) + "\n" + tf[1] + F
|
return "\n".join(H) + "\n" + tf[1] + F
|
||||||
elif tf[0] == -1:
|
elif tf[0] == -1:
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
"formsemestre_pvjury?formsemestre_id=%s" % (formsemestre_id)
|
url_for(
|
||||||
|
"notes.formsemestre_pvjury",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# submit
|
# submit
|
||||||
@ -848,15 +870,17 @@ def formsemestre_lettres_individuelles(formsemestre_id, group_ids=[]):
|
|||||||
finally:
|
finally:
|
||||||
PDFLOCK.release()
|
PDFLOCK.release()
|
||||||
if not pdfdoc:
|
if not pdfdoc:
|
||||||
|
flash("Aucun étudiant n'a de décision de jury !")
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
"formsemestre_status?formsemestre_id={}&head_message=Aucun%20%C3%A9tudiant%20n%27a%20de%20d%C3%A9cision%20de%20jury".format(
|
url_for(
|
||||||
formsemestre_id
|
"notes.formsemestre_status",
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formsemestre_id=formsemestre_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
||||||
dt = time.strftime("%Y-%m-%d")
|
|
||||||
groups_filename = "-" + groups_infos.groups_filename
|
groups_filename = "-" + groups_infos.groups_filename
|
||||||
filename = "lettres-%s%s-%s.pdf" % (sem["titre_num"], groups_filename, dt)
|
filename = f"""lettres-{formsemestre.titre_num()}{groups_filename}-{time.strftime("%Y-%m-%d")}.pdf"""
|
||||||
return scu.sendPDFFile(pdfdoc, filename)
|
return scu.sendPDFFile(pdfdoc, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@ -45,13 +45,14 @@ from flask import g
|
|||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_bulletins_pdf
|
from app.scodoc import sco_bulletins_pdf
|
||||||
from app.scodoc import sco_codes_parcours
|
from app.scodoc import sco_codes_parcours
|
||||||
|
from app.scodoc import sco_etud
|
||||||
from app.scodoc import sco_formsemestre
|
from app.scodoc import sco_formsemestre
|
||||||
from app.scodoc import sco_pdf
|
from app.scodoc import sco_pdf
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_etud
|
|
||||||
import sco_version
|
|
||||||
from app.scodoc.sco_logos import find_logo
|
from app.scodoc.sco_logos import find_logo
|
||||||
|
from app.scodoc.sco_parcours_dut import SituationEtudParcours
|
||||||
from app.scodoc.sco_pdf import SU
|
from app.scodoc.sco_pdf import SU
|
||||||
|
import sco_version
|
||||||
|
|
||||||
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
|
LOGO_FOOTER_ASPECT = scu.CONFIG.LOGO_FOOTER_ASPECT # XXX A AUTOMATISER
|
||||||
LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
|
LOGO_FOOTER_HEIGHT = scu.CONFIG.LOGO_FOOTER_HEIGHT * mm
|
||||||
@ -62,7 +63,7 @@ LOGO_HEADER_HEIGHT = scu.CONFIG.LOGO_HEADER_HEIGHT * mm
|
|||||||
LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
|
LOGO_HEADER_WIDTH = LOGO_HEADER_HEIGHT * scu.CONFIG.LOGO_HEADER_ASPECT
|
||||||
|
|
||||||
|
|
||||||
def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
|
def page_footer(canvas, doc, logo, preferences, with_page_numbers=True):
|
||||||
"Add footer on page"
|
"Add footer on page"
|
||||||
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
|
width = doc.pagesize[0] # - doc.pageTemplate.left_p - doc.pageTemplate.right_p
|
||||||
foot = Frame(
|
foot = Frame(
|
||||||
@ -78,24 +79,24 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
|
|||||||
showBoundary=0,
|
showBoundary=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
LeftFootStyle = reportlab.lib.styles.ParagraphStyle({})
|
left_foot_style = reportlab.lib.styles.ParagraphStyle({})
|
||||||
LeftFootStyle.fontName = preferences["SCOLAR_FONT"]
|
left_foot_style.fontName = preferences["SCOLAR_FONT"]
|
||||||
LeftFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
left_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||||
LeftFootStyle.leftIndent = 0
|
left_foot_style.leftIndent = 0
|
||||||
LeftFootStyle.firstLineIndent = 0
|
left_foot_style.firstLineIndent = 0
|
||||||
LeftFootStyle.alignment = TA_RIGHT
|
left_foot_style.alignment = TA_RIGHT
|
||||||
RightFootStyle = reportlab.lib.styles.ParagraphStyle({})
|
right_foot_style = reportlab.lib.styles.ParagraphStyle({})
|
||||||
RightFootStyle.fontName = preferences["SCOLAR_FONT"]
|
right_foot_style.fontName = preferences["SCOLAR_FONT"]
|
||||||
RightFootStyle.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
right_foot_style.fontSize = preferences["SCOLAR_FONT_SIZE_FOOT"]
|
||||||
RightFootStyle.alignment = TA_RIGHT
|
right_foot_style.alignment = TA_RIGHT
|
||||||
|
|
||||||
p = sco_pdf.makeParas(
|
p = sco_pdf.makeParas(
|
||||||
"""<para>%s</para><para>%s</para>"""
|
f"""<para>{preferences["INSTITUTION_NAME"]}</para><para>{
|
||||||
% (preferences["INSTITUTION_NAME"], preferences["INSTITUTION_ADDRESS"]),
|
preferences["INSTITUTION_ADDRESS"]}</para>""",
|
||||||
LeftFootStyle,
|
left_foot_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
np = Paragraph('<para fontSize="14">%d</para>' % doc.page, RightFootStyle)
|
np = Paragraph(f'<para fontSize="14">{doc.page}</para>', right_foot_style)
|
||||||
tabstyle = TableStyle(
|
tabstyle = TableStyle(
|
||||||
[
|
[
|
||||||
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
||||||
@ -123,7 +124,7 @@ def pageFooter(canvas, doc, logo, preferences, with_page_numbers=True):
|
|||||||
canvas.restoreState()
|
canvas.restoreState()
|
||||||
|
|
||||||
|
|
||||||
def pageHeader(canvas, doc, logo, preferences, only_on_first_page=False):
|
def page_header(canvas, doc, logo, preferences, only_on_first_page=False):
|
||||||
if only_on_first_page and int(doc.page) > 1:
|
if only_on_first_page and int(doc.page) > 1:
|
||||||
return
|
return
|
||||||
height = doc.pagesize[1]
|
height = doc.pagesize[1]
|
||||||
@ -260,7 +261,7 @@ class CourrierIndividuelTemplate(PageTemplate):
|
|||||||
|
|
||||||
# ---- Header/Footer
|
# ---- Header/Footer
|
||||||
if self.with_header:
|
if self.with_header:
|
||||||
pageHeader(
|
page_header(
|
||||||
canvas,
|
canvas,
|
||||||
doc,
|
doc,
|
||||||
self.logo_header,
|
self.logo_header,
|
||||||
@ -268,7 +269,7 @@ class CourrierIndividuelTemplate(PageTemplate):
|
|||||||
self.header_only_on_first_page,
|
self.header_only_on_first_page,
|
||||||
)
|
)
|
||||||
if self.with_footer:
|
if self.with_footer:
|
||||||
pageFooter(
|
page_footer(
|
||||||
canvas,
|
canvas,
|
||||||
doc,
|
doc,
|
||||||
self.logo_footer,
|
self.logo_footer,
|
||||||
@ -427,7 +428,7 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
|
|||||||
"""
|
"""
|
||||||
#
|
#
|
||||||
formsemestre_id = sem["formsemestre_id"]
|
formsemestre_id = sem["formsemestre_id"]
|
||||||
Se = decision["Se"]
|
Se: SituationEtudParcours = decision["Se"]
|
||||||
t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal)
|
t, s = _descr_jury(sem, Se.parcours_validated() or not Se.semestre_non_terminal)
|
||||||
objects = []
|
objects = []
|
||||||
style = reportlab.lib.styles.ParagraphStyle({})
|
style = reportlab.lib.styles.ParagraphStyle({})
|
||||||
@ -446,8 +447,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
|
|||||||
else:
|
else:
|
||||||
params["decisions_ue_descr_plural"] = ""
|
params["decisions_ue_descr_plural"] = ""
|
||||||
|
|
||||||
params["INSTITUTION_CITY"] = sco_preferences.get_preference(
|
params["INSTITUTION_CITY"] = (
|
||||||
"INSTITUTION_CITY", formsemestre_id
|
sco_preferences.get_preference("INSTITUTION_CITY", formsemestre_id) or ""
|
||||||
)
|
)
|
||||||
if decision["prev_decision_sem"]:
|
if decision["prev_decision_sem"]:
|
||||||
params["prev_semestre_id"] = decision["prev"]["semestre_id"]
|
params["prev_semestre_id"] = decision["prev"]["semestre_id"]
|
||||||
@ -528,8 +529,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
|
|||||||
sco_preferences.get_preference(
|
sco_preferences.get_preference(
|
||||||
"PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
|
"PV_LETTER_PASSAGE_SIGNATURE", formsemestre_id
|
||||||
)
|
)
|
||||||
% params
|
or ""
|
||||||
)
|
) % params
|
||||||
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
|
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
|
||||||
objects += sco_pdf.makeParas(
|
objects += sco_pdf.makeParas(
|
||||||
(
|
(
|
||||||
@ -545,8 +546,8 @@ def pdf_lettre_individuelle(sem, decision, etud, params, signature=None):
|
|||||||
sco_preferences.get_preference(
|
sco_preferences.get_preference(
|
||||||
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
|
"PV_LETTER_DIPLOMA_SIGNATURE", formsemestre_id
|
||||||
)
|
)
|
||||||
% params
|
or ""
|
||||||
)
|
) % params
|
||||||
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
|
sig = _simulate_br(sig, '<para leftindent="%(htab1)s">')
|
||||||
objects += sco_pdf.makeParas(
|
objects += sco_pdf.makeParas(
|
||||||
(
|
(
|
||||||
@ -731,7 +732,7 @@ def _pvjury_pdf_type(
|
|||||||
"""
|
"""
|
||||||
% (
|
% (
|
||||||
titre_jury,
|
titre_jury,
|
||||||
sco_preferences.get_preference("DeptName", formsemestre_id),
|
sco_preferences.get_preference("DeptName", formsemestre_id) or "(sans nom)",
|
||||||
sem["anneescolaire"],
|
sem["anneescolaire"],
|
||||||
),
|
),
|
||||||
style,
|
style,
|
||||||
@ -761,7 +762,7 @@ def _pvjury_pdf_type(
|
|||||||
|
|
||||||
objects += sco_pdf.makeParas(
|
objects += sco_pdf.makeParas(
|
||||||
"<para>"
|
"<para>"
|
||||||
+ sco_preferences.get_preference("PV_INTRO", formsemestre_id)
|
+ (sco_preferences.get_preference("PV_INTRO", formsemestre_id) or "")
|
||||||
% {
|
% {
|
||||||
"Decnum": numeroArrete,
|
"Decnum": numeroArrete,
|
||||||
"VDICode": VDICode,
|
"VDICode": VDICode,
|
||||||
|
@ -57,7 +57,7 @@ from app.scodoc import sco_preferences
|
|||||||
|
|
||||||
def formsemestre_recapcomplet(
|
def formsemestre_recapcomplet(
|
||||||
formsemestre_id=None,
|
formsemestre_id=None,
|
||||||
modejury=False,
|
mode_jury=False,
|
||||||
tabformat="html",
|
tabformat="html",
|
||||||
sortcol=None,
|
sortcol=None,
|
||||||
xml_with_decisions=False,
|
xml_with_decisions=False,
|
||||||
@ -78,7 +78,7 @@ def formsemestre_recapcomplet(
|
|||||||
xml, json : concaténation de tous les bulletins, au format demandé
|
xml, json : concaténation de tous les bulletins, au format demandé
|
||||||
pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable)
|
pdf : NON SUPPORTE (car tableau trop grand pour générer un pdf utilisable)
|
||||||
|
|
||||||
modejury: cache modules, affiche lien saisie decision jury
|
mode_jury: cache modules, affiche lien saisie decision jury
|
||||||
xml_with_decisions: publie décisions de jury dans xml et json
|
xml_with_decisions: publie décisions de jury dans xml et json
|
||||||
force_publishing: publie les xml et json même si bulletins non publiés
|
force_publishing: publie les xml et json même si bulletins non publiés
|
||||||
selected_etudid: etudid sélectionné (pour scroller au bon endroit)
|
selected_etudid: etudid sélectionné (pour scroller au bon endroit)
|
||||||
@ -91,14 +91,14 @@ def formsemestre_recapcomplet(
|
|||||||
if tabformat not in supported_formats:
|
if tabformat not in supported_formats:
|
||||||
raise ScoValueError(f"Format non supporté: {tabformat}")
|
raise ScoValueError(f"Format non supporté: {tabformat}")
|
||||||
is_file = tabformat in file_formats
|
is_file = tabformat in file_formats
|
||||||
modejury = int(modejury)
|
mode_jury = int(mode_jury)
|
||||||
xml_with_decisions = int(xml_with_decisions)
|
xml_with_decisions = int(xml_with_decisions)
|
||||||
force_publishing = int(force_publishing)
|
force_publishing = int(force_publishing)
|
||||||
|
|
||||||
data = _do_formsemestre_recapcomplet(
|
data = _do_formsemestre_recapcomplet(
|
||||||
formsemestre_id,
|
formsemestre_id,
|
||||||
format=tabformat,
|
format=tabformat,
|
||||||
modejury=modejury,
|
mode_jury=mode_jury,
|
||||||
sortcol=sortcol,
|
sortcol=sortcol,
|
||||||
xml_with_decisions=xml_with_decisions,
|
xml_with_decisions=xml_with_decisions,
|
||||||
force_publishing=force_publishing,
|
force_publishing=force_publishing,
|
||||||
@ -123,9 +123,9 @@ def formsemestre_recapcomplet(
|
|||||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
if modejury:
|
if mode_jury:
|
||||||
H.append(
|
H.append(
|
||||||
f'<input type="hidden" name="modejury" value="{modejury}"></input>'
|
f'<input type="hidden" name="mode_jury" value="{mode_jury}"></input>'
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
||||||
@ -163,7 +163,7 @@ def formsemestre_recapcomplet(
|
|||||||
)
|
)
|
||||||
if sco_permissions_check.can_validate_sem(formsemestre_id):
|
if sco_permissions_check.can_validate_sem(formsemestre_id):
|
||||||
H.append("<p>")
|
H.append("<p>")
|
||||||
if modejury:
|
if mode_jury:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
|
f"""<a class="stdlink" href="{url_for('notes.formsemestre_validation_auto',
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
|
||||||
@ -172,7 +172,7 @@ def formsemestre_recapcomplet(
|
|||||||
else:
|
else:
|
||||||
H.append(
|
H.append(
|
||||||
f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
|
f"""<a class="stdlink" href="{url_for('notes.formsemestre_recapcomplet',
|
||||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, modejury=1)
|
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, mode_jury=1)
|
||||||
}">Saisie des décisions du jury</a>"""
|
}">Saisie des décisions du jury</a>"""
|
||||||
)
|
)
|
||||||
H.append("</p>")
|
H.append("</p>")
|
||||||
@ -196,7 +196,7 @@ def _do_formsemestre_recapcomplet(
|
|||||||
formsemestre_id=None,
|
formsemestre_id=None,
|
||||||
format="html", # html, xml, xls, xlsall, json
|
format="html", # html, xml, xls, xlsall, json
|
||||||
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
|
xml_nodate=False, # format XML sans dates (sert pour debug cache: comparaison de XML)
|
||||||
modejury=False, # saisie décisions jury
|
mode_jury=False, # saisie décisions jury
|
||||||
sortcol=None, # indice colonne a trier dans table T
|
sortcol=None, # indice colonne a trier dans table T
|
||||||
xml_with_decisions=False,
|
xml_with_decisions=False,
|
||||||
force_publishing=True,
|
force_publishing=True,
|
||||||
@ -215,7 +215,7 @@ def _do_formsemestre_recapcomplet(
|
|||||||
formsemestre,
|
formsemestre,
|
||||||
res,
|
res,
|
||||||
include_evaluations=(format == "evals"),
|
include_evaluations=(format == "evals"),
|
||||||
modejury=modejury,
|
mode_jury=mode_jury,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
selected_etudid=selected_etudid,
|
selected_etudid=selected_etudid,
|
||||||
)
|
)
|
||||||
@ -368,34 +368,34 @@ def gen_formsemestre_recapcomplet_html(
|
|||||||
formsemestre: FormSemestre,
|
formsemestre: FormSemestre,
|
||||||
res: NotesTableCompat,
|
res: NotesTableCompat,
|
||||||
include_evaluations=False,
|
include_evaluations=False,
|
||||||
modejury=False,
|
mode_jury=False,
|
||||||
filename="",
|
filename="",
|
||||||
selected_etudid=None,
|
selected_etudid=None,
|
||||||
):
|
):
|
||||||
"""Construit table recap pour le BUT
|
"""Construit table recap pour le BUT
|
||||||
Cache le résultat pour le semestre (sauf en mode jury).
|
Cache le résultat pour le semestre (sauf en mode jury).
|
||||||
|
|
||||||
Si modejury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury
|
Si mode_jury, cache colonnes modules et affiche un lien vers la saisie de la décision de jury
|
||||||
|
|
||||||
Return: data, filename
|
Return: data, filename
|
||||||
data est une chaine, le <div>...</div> incluant le tableau.
|
data est une chaine, le <div>...</div> incluant le tableau.
|
||||||
"""
|
"""
|
||||||
table_html = None
|
table_html = None
|
||||||
if not (modejury or selected_etudid):
|
if not (mode_jury or selected_etudid):
|
||||||
if include_evaluations:
|
if include_evaluations:
|
||||||
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
|
table_html = sco_cache.TableRecapWithEvalsCache.get(formsemestre.id)
|
||||||
else:
|
else:
|
||||||
table_html = sco_cache.TableRecapCache.get(formsemestre.id)
|
table_html = sco_cache.TableRecapCache.get(formsemestre.id)
|
||||||
if modejury or (table_html is None):
|
if mode_jury or (table_html is None):
|
||||||
table_html = _gen_formsemestre_recapcomplet_html(
|
table_html = _gen_formsemestre_recapcomplet_html(
|
||||||
formsemestre,
|
formsemestre,
|
||||||
res,
|
res,
|
||||||
include_evaluations,
|
include_evaluations,
|
||||||
modejury,
|
mode_jury,
|
||||||
filename,
|
filename,
|
||||||
selected_etudid=selected_etudid,
|
selected_etudid=selected_etudid,
|
||||||
)
|
)
|
||||||
if not modejury:
|
if not mode_jury:
|
||||||
if include_evaluations:
|
if include_evaluations:
|
||||||
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
|
sco_cache.TableRecapWithEvalsCache.set(formsemestre.id, table_html)
|
||||||
else:
|
else:
|
||||||
@ -408,13 +408,15 @@ def _gen_formsemestre_recapcomplet_html(
|
|||||||
formsemestre: FormSemestre,
|
formsemestre: FormSemestre,
|
||||||
res: NotesTableCompat,
|
res: NotesTableCompat,
|
||||||
include_evaluations=False,
|
include_evaluations=False,
|
||||||
modejury=False,
|
mode_jury=False,
|
||||||
filename: str = "",
|
filename: str = "",
|
||||||
selected_etudid=None,
|
selected_etudid=None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Génère le html"""
|
"""Génère le html"""
|
||||||
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
||||||
convert_values=True, include_evaluations=include_evaluations, modejury=modejury
|
convert_values=True,
|
||||||
|
include_evaluations=include_evaluations,
|
||||||
|
mode_jury=mode_jury,
|
||||||
)
|
)
|
||||||
if not rows:
|
if not rows:
|
||||||
return (
|
return (
|
||||||
@ -423,7 +425,7 @@ def _gen_formsemestre_recapcomplet_html(
|
|||||||
H = [
|
H = [
|
||||||
f"""<div class="table_recap"><table class="table_recap {
|
f"""<div class="table_recap"><table class="table_recap {
|
||||||
'apc' if formsemestre.formation.is_apc() else 'classic'
|
'apc' if formsemestre.formation.is_apc() else 'classic'
|
||||||
} {'jury' if modejury else ''}"
|
} {'jury' if mode_jury else ''}"
|
||||||
data-filename="{filename}">"""
|
data-filename="{filename}">"""
|
||||||
]
|
]
|
||||||
# header
|
# header
|
||||||
|
@ -235,7 +235,7 @@ def module_tag_list(module_id=""):
|
|||||||
|
|
||||||
def module_tag_set(module_id="", taglist=None):
|
def module_tag_set(module_id="", taglist=None):
|
||||||
"""taglist may either be:
|
"""taglist may either be:
|
||||||
a string with tag names separated by commas ("un;deux")
|
a string with tag names separated by commas ("un,deux")
|
||||||
or a list of strings (["un", "deux"])
|
or a list of strings (["un", "deux"])
|
||||||
"""
|
"""
|
||||||
if not taglist:
|
if not taglist:
|
||||||
@ -243,7 +243,7 @@ def module_tag_set(module_id="", taglist=None):
|
|||||||
elif isinstance(taglist, str):
|
elif isinstance(taglist, str):
|
||||||
taglist = taglist.split(",")
|
taglist = taglist.split(",")
|
||||||
taglist = [t.strip() for t in taglist]
|
taglist = [t.strip() for t in taglist]
|
||||||
# log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist))
|
log("module_tag_set: module_id=%s taglist=%s" % (module_id, taglist))
|
||||||
# Sanity check:
|
# Sanity check:
|
||||||
Mod = sco_edit_module.module_list(args={"module_id": module_id})
|
Mod = sco_edit_module.module_list(args={"module_id": module_id})
|
||||||
if not Mod:
|
if not Mod:
|
||||||
|
@ -151,10 +151,8 @@ def trombino_html(groups_infos):
|
|||||||
if sco_photos.etud_photo_is_local(t, size="small"):
|
if sco_photos.etud_photo_is_local(t, size="small"):
|
||||||
foto = sco_photos.etud_photo_html(t, title="")
|
foto = sco_photos.etud_photo_html(t, title="")
|
||||||
else: # la photo n'est pas immédiatement dispo
|
else: # la photo n'est pas immédiatement dispo
|
||||||
foto = (
|
foto = f"""<span class="unloaded_img" id="{t["etudid"]
|
||||||
'<span class="unloaded_img" id="%s"><img border="0" height="90" alt="en cours" src="/ScoDoc/static/icons/loading.jpg"/></span>'
|
}"><img border="0" height="90" alt="en cours" src="{scu.STATIC_DIR}/icons/loading.jpg"/></span>"""
|
||||||
% t["etudid"]
|
|
||||||
)
|
|
||||||
H.append(
|
H.append(
|
||||||
'<a href="%s">%s</a>'
|
'<a href="%s">%s</a>'
|
||||||
% (
|
% (
|
||||||
|
@ -180,7 +180,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
les évaluations du semestre.
|
les évaluations du semestre.
|
||||||
"""
|
"""
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||||
r = ndb.SimpleDictFetch(
|
rows = ndb.SimpleDictFetch(
|
||||||
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id
|
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id
|
||||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
||||||
notes_modules mod, identite i, "user" u
|
notes_modules mod, identite i, "user" u
|
||||||
@ -194,6 +194,10 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
""",
|
""",
|
||||||
{"formsemestre_id": formsemestre_id},
|
{"formsemestre_id": formsemestre_id},
|
||||||
)
|
)
|
||||||
|
# Formatte les notes
|
||||||
|
keep_numeric = format in scu.FORMATS_NUMERIQUES
|
||||||
|
for row in rows:
|
||||||
|
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
|
||||||
columns_ids = (
|
columns_ids = (
|
||||||
"date",
|
"date",
|
||||||
"code_nip",
|
"code_nip",
|
||||||
@ -223,7 +227,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||||||
tab = GenTable(
|
tab = GenTable(
|
||||||
titles=titles,
|
titles=titles,
|
||||||
columns_ids=columns_ids,
|
columns_ids=columns_ids,
|
||||||
rows=r,
|
rows=rows,
|
||||||
html_title="<h2>Saisies de notes dans %s</h2>" % sem["titreannee"],
|
html_title="<h2>Saisies de notes dans %s</h2>" % sem["titreannee"],
|
||||||
html_class="table_leftalign table_coldate gt_table_searchable",
|
html_class="table_leftalign table_coldate gt_table_searchable",
|
||||||
html_class_ignore_default=True,
|
html_class_ignore_default=True,
|
||||||
|
@ -63,6 +63,8 @@ from app.scodoc import sco_exceptions
|
|||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
|
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
||||||
|
STATIC_DIR = "/ScoDoc/static/links/" + sco_version.SCOVERSION
|
||||||
|
|
||||||
# ----- CALCUL ET PRESENTATION DES NOTES
|
# ----- CALCUL ET PRESENTATION DES NOTES
|
||||||
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
|
NOTES_PRECISION = 1e-4 # evite eventuelles erreurs d'arrondis
|
||||||
@ -111,6 +113,8 @@ MODULE_TYPE_NAMES = {
|
|||||||
None: "Module",
|
None: "Module",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PARTITION_PARCOURS = "Parcours"
|
||||||
|
|
||||||
MALUS_MAX = 20.0
|
MALUS_MAX = 20.0
|
||||||
MALUS_MIN = -20.0
|
MALUS_MIN = -20.0
|
||||||
|
|
||||||
@ -397,6 +401,9 @@ XLSX_SUFFIX = ".xlsx"
|
|||||||
XML_MIMETYPE = "text/xml"
|
XML_MIMETYPE = "text/xml"
|
||||||
XML_SUFFIX = ".xml"
|
XML_SUFFIX = ".xml"
|
||||||
|
|
||||||
|
# Format pour lesquels on exporte sans formattage des nombres (pas de perte de précision)
|
||||||
|
FORMATS_NUMERIQUES = {"csv", "xls", "xlsx", "xml", "json"}
|
||||||
|
|
||||||
|
|
||||||
def get_mime_suffix(format_code: str) -> tuple[str, str]:
|
def get_mime_suffix(format_code: str) -> tuple[str, str]:
|
||||||
"""Returns (MIME, SUFFIX) from format_code == "xls", "xml", ...
|
"""Returns (MIME, SUFFIX) from format_code == "xls", "xml", ...
|
||||||
@ -581,7 +588,7 @@ def purge_chars(s, allowed_chars=""):
|
|||||||
return s.translate(PurgeChars(allowed_chars=allowed_chars))
|
return s.translate(PurgeChars(allowed_chars=allowed_chars))
|
||||||
|
|
||||||
|
|
||||||
def sanitize_string(s):
|
def sanitize_string(s, remove_spaces=True):
|
||||||
"""s is an ordinary string, encoding given by SCO_ENCODING"
|
"""s is an ordinary string, encoding given by SCO_ENCODING"
|
||||||
suppress accents and chars interpreted in XML
|
suppress accents and chars interpreted in XML
|
||||||
Irreversible (not a quote)
|
Irreversible (not a quote)
|
||||||
@ -589,8 +596,10 @@ def sanitize_string(s):
|
|||||||
For ids and some filenames
|
For ids and some filenames
|
||||||
"""
|
"""
|
||||||
# Table suppressing some chars:
|
# Table suppressing some chars:
|
||||||
trans = str.maketrans("", "", "'`\"<>!&\\ ")
|
to_del = "'`\"<>!&\\ " if remove_spaces else "'`\"<>!&"
|
||||||
return suppress_accents(s.translate(trans)).replace(" ", "_").replace("\t", "_")
|
trans = str.maketrans("", "", to_del)
|
||||||
|
|
||||||
|
return suppress_accents(s.translate(trans)).replace("\t", "_")
|
||||||
|
|
||||||
|
|
||||||
_BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\&[]*?'")
|
_BAD_FILENAME_CHARS = str.maketrans("", "", ":/\\&[]*?'")
|
||||||
@ -863,7 +872,7 @@ def annee_scolaire_repr(year, month):
|
|||||||
return "%s - %s" % (year - 1, year)
|
return "%s - %s" % (year - 1, year)
|
||||||
|
|
||||||
|
|
||||||
def annee_scolaire_debut(year, month):
|
def annee_scolaire_debut(year, month) -> int:
|
||||||
"""Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord..."""
|
"""Annee scolaire de debut (septembre): heuristique pour l'hémisphère nord..."""
|
||||||
if int(month) > 7:
|
if int(month) > 7:
|
||||||
return int(year)
|
return int(year)
|
||||||
@ -952,12 +961,7 @@ def icontag(name, file_format="png", no_size=False, **attrs):
|
|||||||
if "alt" not in attrs:
|
if "alt" not in attrs:
|
||||||
attrs["alt"] = "logo %s" % name
|
attrs["alt"] = "logo %s" % name
|
||||||
s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs])
|
s = " ".join(['%s="%s"' % (k, attrs[k]) for k in attrs])
|
||||||
return '<img class="%s" %s src="/ScoDoc/static/icons/%s.%s" />' % (
|
return f'<img class="{name}" {s} src="{STATIC_DIR}/icons/{name}.{file_format}" />'
|
||||||
name,
|
|
||||||
s,
|
|
||||||
name,
|
|
||||||
file_format,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
|
ICON_PDF = icontag("pdficon16x20_img", title="Version PDF")
|
||||||
@ -966,6 +970,8 @@ ICON_XLS = icontag("xlsicon_img", title="Version tableur")
|
|||||||
# HTML emojis
|
# HTML emojis
|
||||||
EMO_WARNING = "⚠️" # warning /!\
|
EMO_WARNING = "⚠️" # warning /!\
|
||||||
EMO_RED_TRIANGLE_DOWN = "🔻" # red triangle pointed down
|
EMO_RED_TRIANGLE_DOWN = "🔻" # red triangle pointed down
|
||||||
|
EMO_PREV_ARROW = "❮"
|
||||||
|
EMO_NEXT_ARROW = "❯"
|
||||||
|
|
||||||
|
|
||||||
def sort_dates(L, reverse=False):
|
def sort_dates(L, reverse=False):
|
||||||
@ -1095,6 +1101,10 @@ def gen_cell(key: str, row: dict, elt="td", with_col_class=False):
|
|||||||
if with_col_class:
|
if with_col_class:
|
||||||
klass = key + " " + klass
|
klass = key + " " + klass
|
||||||
attrs = f'class="{klass}"' if klass else ""
|
attrs = f'class="{klass}"' if klass else ""
|
||||||
|
data = row.get(f"_{key}_data") # dict
|
||||||
|
if data:
|
||||||
|
for k in data:
|
||||||
|
attrs += f' data-{k}="{data[k]}"'
|
||||||
order = row.get(f"_{key}_order")
|
order = row.get(f"_{key}_order")
|
||||||
if order:
|
if order:
|
||||||
attrs += f' data-order="{order}"'
|
attrs += f' data-order="{order}"'
|
||||||
|
178
app/static/css/jury_but.css
Normal file
178
app/static/css/jury_but.css
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/* Saisie décision de jury BUT */
|
||||||
|
|
||||||
|
.jury_but form {
|
||||||
|
font-family: Verdana, Geneva, Tahoma, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jury_but .titre_parcours {
|
||||||
|
font-size: 130%;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jury_but .nom_etud {
|
||||||
|
font-size: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_annee {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: repeat(4, auto);
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_annee_caption {
|
||||||
|
grid-column: 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_annee_caption,
|
||||||
|
.but_niveau_titre {
|
||||||
|
background: #09c !important;
|
||||||
|
color: #FFF;
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_annee>* {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px 16px;
|
||||||
|
background: #FFF;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_annee>div.titre {
|
||||||
|
background: rgb(242, 242, 238);
|
||||||
|
border: none;
|
||||||
|
border-radius: 0px;
|
||||||
|
border-bottom: 1px solid gray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_niveau_ue>div:nth-child(1),
|
||||||
|
.but_note {
|
||||||
|
border-right: 1px solid #aaa;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_annee select {
|
||||||
|
padding: 8px 8px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_niveau_rcue,
|
||||||
|
.but_niveau_rcue>* {
|
||||||
|
border-color: #09c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.but_navigation {
|
||||||
|
padding-top: 16px;
|
||||||
|
margin-left: 50px;
|
||||||
|
margin-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_navigation div {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 50px;
|
||||||
|
margin-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_section_annee {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_settings {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.but_explanation {
|
||||||
|
color: blueviolet;
|
||||||
|
font-style: italic;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:disabled {
|
||||||
|
font-weight: bold;
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:invalid {
|
||||||
|
background: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.but_code option.recorded {
|
||||||
|
color: rgb(3, 157, 3);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_niveau_ue.recorded,
|
||||||
|
div.but_niveau_rcue.recorded {
|
||||||
|
border-color: rgb(136, 252, 136);
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_niveau_ue.modified {
|
||||||
|
background-color: rgb(255, 214, 254);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_buttons {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_buttons span {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc_codes {
|
||||||
|
margin: 16px;
|
||||||
|
background-color: rgb(227, 254, 254);
|
||||||
|
font-size: 75%;
|
||||||
|
border: 2px solid rgb(4, 4, 118);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc_section {
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 125%;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-family: Tahoma, Geneva, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table td {
|
||||||
|
padding: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table thead td {
|
||||||
|
background-color: #54585d;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid #54585d;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table tbody td {
|
||||||
|
color: #636363;
|
||||||
|
border: 1px solid #dddfe1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table tbody tr {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table tbody tr:nth-child(odd) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.but_doc table tr td.amue {
|
||||||
|
color: rgb(127, 127, 206);
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
@ -189,10 +189,10 @@ section>div:nth-child(1){
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
#ects_tot {
|
#ects_tot, .decision, .decision_annee {
|
||||||
margin-left: 8px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.enteteSemestre{
|
.enteteSemestre{
|
||||||
color: black;
|
color: black;
|
||||||
|
@ -2066,6 +2066,10 @@ span.notes_module_list_buts {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.formation_apc_infos ul li:not(:last-child) {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
div.ue_list_tit {
|
div.ue_list_tit {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
@ -2206,7 +2210,7 @@ ul.notes_module_list {
|
|||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div#ue_list_modules {
|
div.ue_choix_niveau {
|
||||||
background-color: rgb(191, 242, 255);
|
background-color: rgb(191, 242, 255);
|
||||||
border: 1px solid blue;
|
border: 1px solid blue;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -2215,6 +2219,15 @@ div#ue_list_modules {
|
|||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#ue_list_modules {
|
||||||
|
background-color: rgb(251, 225, 165);
|
||||||
|
border: 1px solid blue;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
div#ue_list_etud_validations {
|
div#ue_list_etud_validations {
|
||||||
background-color: rgb(220, 250, 220);
|
background-color: rgb(220, 250, 220);
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
@ -2258,6 +2271,22 @@ span.missing_value {
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span.code_parcours {
|
||||||
|
color: white;
|
||||||
|
background-color: rgb(254, 95, 246);
|
||||||
|
padding-left: 4px;
|
||||||
|
padding-right: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr#tf_module_parcours>td {
|
||||||
|
background-color: rgb(229, 229, 229);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr#tf_module_app_critiques>td {
|
||||||
|
background-color: rgb(194, 209, 228);
|
||||||
|
}
|
||||||
|
|
||||||
/* tableau recap notes */
|
/* tableau recap notes */
|
||||||
table.notes_recapcomplet {
|
table.notes_recapcomplet {
|
||||||
border: 2px solid blue;
|
border: 2px solid blue;
|
||||||
@ -2346,7 +2375,6 @@ td.recap_col_ue_inf {
|
|||||||
padding-right: 1.2em;
|
padding-right: 1.2em;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: bold;
|
|
||||||
color: rgb(255, 0, 0);
|
color: rgb(255, 0, 0);
|
||||||
border-left: 1px solid blue;
|
border-left: 1px solid blue;
|
||||||
}
|
}
|
||||||
@ -2355,7 +2383,6 @@ td.recap_col_ue_val {
|
|||||||
padding-right: 1.2em;
|
padding-right: 1.2em;
|
||||||
padding-left: 4px;
|
padding-left: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: bold;
|
|
||||||
color: rgb(0, 140, 0);
|
color: rgb(0, 140, 0);
|
||||||
border-left: 1px solid blue;
|
border-left: 1px solid blue;
|
||||||
}
|
}
|
||||||
@ -2953,7 +2980,8 @@ td.rcp_dec {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.rcp_nonass {
|
td.rcp_nonass,
|
||||||
|
td.rcp_but {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3626,6 +3654,13 @@ span.sco_tag_edit .tag-editor {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.sco_tag_module_edit span.sco_tag_edit .tag-editor {
|
||||||
|
background-color: rgb(210, 210, 210);
|
||||||
|
border: 0px;
|
||||||
|
margin-left: 0px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
span.sco_tag_edit .tag-editor-delete {
|
span.sco_tag_edit .tag-editor-delete {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
@ -3736,11 +3771,43 @@ table.table_recap .rang {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap .col_ue,
|
table.table_recap .col_ue,
|
||||||
|
table.table_recap .col_ue_code,
|
||||||
table.table_recap .col_moy_gen,
|
table.table_recap .col_moy_gen,
|
||||||
table.table_recap .group {
|
table.table_recap .group {
|
||||||
border-left: 1px solid blue;
|
border-left: 1px solid blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.table_recap .col_ue {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table_recap.jury .col_ue {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table_recap.jury .col_rcue,
|
||||||
|
table.table_recap.jury .col_rcue_code {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table_recap.jury tr.even td.col_rcue,
|
||||||
|
table.table_recap.jury tr.even td.col_rcue_code {
|
||||||
|
background-color: #b0d4f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table_recap.jury tr.odd td.col_rcue,
|
||||||
|
table.table_recap.jury tr.odd td.col_rcue_code {
|
||||||
|
background-color: #abcdef;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table_recap.jury tr.odd td.col_rcues_validables {
|
||||||
|
background-color: #e1d3c5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.table_recap.jury tr.even td.col_rcues_validables {
|
||||||
|
background-color: #fcebda !important;
|
||||||
|
}
|
||||||
|
|
||||||
table.table_recap .group {
|
table.table_recap .group {
|
||||||
border-left: 1px dashed rgb(160, 160, 160);
|
border-left: 1px dashed rgb(160, 160, 160);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -3767,6 +3834,12 @@ table.table_recap a:visited {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.table_recap a.stdlink:link,
|
||||||
|
table.table_recap a.stdlink:visited {
|
||||||
|
color: blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
table.table_recap tfoot th,
|
table.table_recap tfoot th,
|
||||||
table.table_recap thead th {
|
table.table_recap thead th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -3779,18 +3852,15 @@ table.table_recap td.moy_inf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap td.moy_ue_valid {
|
table.table_recap td.moy_ue_valid {
|
||||||
font-weight: bold;
|
|
||||||
color: rgb(0, 140, 0);
|
color: rgb(0, 140, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap td.moy_ue_warning {
|
table.table_recap td.moy_ue_warning {
|
||||||
font-weight: bold;
|
|
||||||
color: rgb(255, 0, 0);
|
color: rgb(255, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
table.table_recap td.col_ues_validables {
|
table.table_recap td.col_ues_validables {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: bold;
|
|
||||||
font-style: normal !important;
|
font-style: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3917,6 +3987,11 @@ table.table_recap td.evaluation.non_inscrit {
|
|||||||
color: rgb(101, 101, 101);
|
color: rgb(101, 101, 101);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.table_jury_but_links {
|
||||||
|
margin-top: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------- Tableau etat evals ------------ */
|
/* ------------- Tableau etat evals ------------ */
|
||||||
|
|
||||||
div.evaluations_recap table.evaluations_recap {
|
div.evaluations_recap table.evaluations_recap {
|
||||||
|
@ -25,10 +25,28 @@ function update_bonus_description() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update_ue_list() {
|
function update_ue_list() {
|
||||||
var ue_id = $("#tf_ue_id")[0].value;
|
let ue_id = $("#tf_ue_id")[0].value;
|
||||||
var ue_code = $("#tf_ue_code")[0].value;
|
let ue_code = $("#tf_ue_code")[0].value;
|
||||||
var query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id;
|
let query = SCO_URL + "/Notes/ue_sharing_code?ue_code=" + ue_code + "&hide_ue_id=" + ue_id + "&ue_id=" + ue_id;
|
||||||
$.get(query, '', function (data) {
|
$.get(query, '', function (data) {
|
||||||
$("#ue_list_code").html(data);
|
$("#ue_list_code").html(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function set_ue_niveau_competence() {
|
||||||
|
let ue_id = document.querySelector("#tf_ue_id").value;
|
||||||
|
let select = document.querySelector("#form_ue_choix_niveau select");
|
||||||
|
let niveau_id = select.value;
|
||||||
|
let set_ue_niveau_competence_url = select.dataset.setter;
|
||||||
|
$.post(set_ue_niveau_competence_url,
|
||||||
|
{
|
||||||
|
ue_id: ue_id,
|
||||||
|
niveau_id: niveau_id,
|
||||||
|
},
|
||||||
|
function (result) {
|
||||||
|
alert("niveau de compétence enregistré"); // XXX #frontend à améliorer
|
||||||
|
// obj.classList.remove("sco_wait");
|
||||||
|
// obj.classList.add("sco_modified");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
62
app/static/js/jury_but.js
Normal file
62
app/static/js/jury_but.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
|
||||||
|
// active les menus des codes "manuels" (année, RCUEs)
|
||||||
|
function enable_manual_codes(elt) {
|
||||||
|
$(".jury_but select.manual").prop("disabled", !elt.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// changement menu code:
|
||||||
|
function change_menu_code(elt) {
|
||||||
|
elt.parentElement.parentElement.classList.remove("recorded");
|
||||||
|
// TODO: comparer avec valeur enregistrée (à mettre en data-orig ?)
|
||||||
|
// et colorer en fonction
|
||||||
|
elt.parentElement.parentElement.classList.add("modified");
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
// Recupère la liste ordonnées des etudids
|
||||||
|
// pour avoir le "suivant" etr le "précédent"
|
||||||
|
// (liens de navigation)
|
||||||
|
const url = new URL(document.URL);
|
||||||
|
const frags = url.pathname.split("/"); // .../formsemestre_validation_but/formsemestre_id/etudid
|
||||||
|
const etudid = frags[frags.length - 1];
|
||||||
|
const formsemestre_id = frags[frags.length - 2];
|
||||||
|
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
|
||||||
|
const etudids_str = localStorage.getItem(etudids_key);
|
||||||
|
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
|
||||||
|
const noms_str = localStorage.getItem(noms_key);
|
||||||
|
if (etudids_str && noms_str) {
|
||||||
|
const etudids = JSON.parse(etudids_str);
|
||||||
|
const noms = JSON.parse(noms_str);
|
||||||
|
const cur_idx = etudids.indexOf(etudid);
|
||||||
|
let prev_idx = -1;
|
||||||
|
let next_idx = -1
|
||||||
|
if (cur_idx != -1) {
|
||||||
|
if (cur_idx > 0) {
|
||||||
|
prev_idx = cur_idx - 1;
|
||||||
|
}
|
||||||
|
if (cur_idx < etudids.length - 1) {
|
||||||
|
next_idx = cur_idx + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (prev_idx != -1) {
|
||||||
|
let elem = document.querySelector("div.prev a");
|
||||||
|
if (elem) {
|
||||||
|
elem.href = elem.href.replace("PREV", etudids[prev_idx]);
|
||||||
|
elem.innerHTML = noms[prev_idx];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.querySelector("div.prev").innerHTML = "";
|
||||||
|
}
|
||||||
|
if (next_idx != -1) {
|
||||||
|
let elem = document.querySelector("div.next a");
|
||||||
|
if (elem) {
|
||||||
|
elem.href = elem.href.replace("NEXT", etudids[next_idx]);
|
||||||
|
elem.innerHTML = noms[next_idx];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.querySelector("div.next").innerHTML = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
8
app/static/js/module_edit.js
Normal file
8
app/static/js/module_edit.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/* Page édition module */
|
||||||
|
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -83,7 +83,9 @@ class releveBUT extends HTMLElement {
|
|||||||
<div>
|
<div>
|
||||||
<div class=infoSemestre></div>
|
<div class=infoSemestre></div>
|
||||||
<div>
|
<div>
|
||||||
<div><span class=decision></span><span class="ects" id="ects_tot"></span></div>
|
<div class=decision_annee></div>
|
||||||
|
<div class=decision></div>
|
||||||
|
<div class="ects" id="ects_tot"></div>
|
||||||
<div class=dateInscription>Inscrit le </div>
|
<div class=dateInscription>Inscrit le </div>
|
||||||
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
|
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
|
||||||
</div>
|
</div>
|
||||||
@ -192,6 +194,20 @@ class releveBUT extends HTMLElement {
|
|||||||
/* Information sur le semestre */
|
/* Information sur le semestre */
|
||||||
/*******************************/
|
/*******************************/
|
||||||
showSemestre(data) {
|
showSemestre(data) {
|
||||||
|
let correspondanceCodes = {
|
||||||
|
"ADM": "Admis",
|
||||||
|
"AJD": "Admis par décision de jury",
|
||||||
|
"PASD": "Passage de droit : tout n'est pas validé, mais d'après les règles du BUT, vous passez",
|
||||||
|
"PAS1NCI": "Vous passez par décision de jury mais attention, vous n'avez pas partout le niveau suffisant",
|
||||||
|
"RED": "Ajourné mais autorisé à redoubler",
|
||||||
|
"NAR": "Non admis et non autorisé à redoubler : réorientation",
|
||||||
|
"DEM": "Démission",
|
||||||
|
"ABAN": "Abandon constaté sans lettre de démission",
|
||||||
|
"RAT": "En attente d'un rattrapage",
|
||||||
|
"EXCLU": "Exclusion dans le cadre d'une décision disciplinaire",
|
||||||
|
"DEF": "Défaillance : non évalué par manque d'assiduité",
|
||||||
|
"ABL": "Année blanche"
|
||||||
|
}
|
||||||
|
|
||||||
this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `;
|
this.shadow.querySelector("#identite_etudiant").innerHTML = ` <a href="${data.etudiant.fiche_url}">${data.etudiant.nomprenom}</a> `;
|
||||||
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
|
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
|
||||||
@ -224,10 +240,15 @@ class releveBUT extends HTMLElement {
|
|||||||
}).join("")
|
}).join("")
|
||||||
}*/
|
}*/
|
||||||
this.shadow.querySelector(".infoSemestre").innerHTML = output;
|
this.shadow.querySelector(".infoSemestre").innerHTML = output;
|
||||||
if (data.semestre.decision?.code) {
|
if(data.semestre.decision_annee?.code){
|
||||||
this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
|
this.shadow.querySelector(".decision_annee").innerHTML = "Décision année : " + data.semestre.decision_annee.code + " - " + correspondanceCodes[data.semestre.decision_annee.code];
|
||||||
}
|
}
|
||||||
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis || "-") + " / " + (data.semestre.ECTS?.total || "-");
|
|
||||||
|
this.shadow.querySelector(".decision").innerHTML = data.semestre.situation || "";
|
||||||
|
/*if (data.semestre.decision?.code) {
|
||||||
|
this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
|
||||||
|
}*/
|
||||||
|
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS : " + (data.semestre.ECTS?.acquis ?? "-") + " / " + (data.semestre.ECTS?.total ?? "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************/
|
/*******************************/
|
||||||
@ -254,13 +275,13 @@ class releveBUT extends HTMLElement {
|
|||||||
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
|
${ue}${(dataUE.titre) ? " - " + dataUE.titre : ""}
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<div class=moyenne>Moyenne : ${dataUE.moyenne?.value || "-"}</div>
|
<div class=moyenne>Moyenne : ${dataUE.moyenne?.value ?? "-"}</div>
|
||||||
<div class=rang>Rang : ${dataUE.moyenne?.rang} / ${dataUE.moyenne?.total}</div>
|
<div class=rang>Rang : ${dataUE.moyenne?.rang} / ${dataUE.moyenne?.total}</div>
|
||||||
<div class=info>
|
<div class=info>
|
||||||
Bonus : ${dataUE.bonus || 0} -
|
Bonus : ${dataUE.bonus || 0} -
|
||||||
Malus : ${dataUE.malus || 0}
|
Malus : ${dataUE.malus || 0}
|
||||||
<span class=ects> -
|
<span class=ects> -
|
||||||
ECTS : ${dataUE.ECTS?.acquis || "-"} / ${dataUE.ECTS?.total || "-"}
|
ECTS : ${dataUE.ECTS?.acquis ?? "-"} / ${dataUE.ECTS?.total ?? "-"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
@ -11,6 +11,7 @@ function build_table(data) {
|
|||||||
let output = "";
|
let output = "";
|
||||||
let sumsUE = {};
|
let sumsUE = {};
|
||||||
let sumsRessources = {};
|
let sumsRessources = {};
|
||||||
|
let value;
|
||||||
|
|
||||||
data.forEach((cellule) => {
|
data.forEach((cellule) => {
|
||||||
output += `
|
output += `
|
||||||
@ -31,13 +32,16 @@ function build_table(data) {
|
|||||||
--y:${cellule.y};
|
--y:${cellule.y};
|
||||||
--nbX:${cellule.nbX || 1};
|
--nbX:${cellule.nbX || 1};
|
||||||
--nbY: ${cellule.nbY || 1};
|
--nbY: ${cellule.nbY || 1};
|
||||||
">
|
">${cellule.data}</div>`; // ne pas mettre d'espace car c'est utilisé par :not(:empty) après
|
||||||
${cellule.data}
|
|
||||||
</div>`;
|
if (cellule.style.includes("champs")) {
|
||||||
|
if (cellule.editable == true && cellule.data) {
|
||||||
if (cellule.editable) {
|
value = parseFloat(cellule.data) *100;
|
||||||
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + (parseFloat(cellule.data) || 0);
|
} else {
|
||||||
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + (parseFloat(cellule.data) || 0);
|
value = 0;
|
||||||
|
}
|
||||||
|
sumsRessources[cellule.y] = (sumsRessources[cellule.y] ?? 0) + value;
|
||||||
|
sumsUE[cellule.x] = (sumsUE[cellule.x] ?? 0) + value;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -65,7 +69,7 @@ function showSums(sumsRessources, sumsUE) {
|
|||||||
--nbX:1;
|
--nbX:1;
|
||||||
--nbY:1;
|
--nbY:1;
|
||||||
">
|
">
|
||||||
${value}
|
${value / 100}
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -82,7 +86,7 @@ function showSums(sumsRessources, sumsUE) {
|
|||||||
--nbX:1;
|
--nbX:1;
|
||||||
--nbY:1;
|
--nbY:1;
|
||||||
">
|
">
|
||||||
${value}
|
${value / 100}
|
||||||
</div>`;
|
</div>`;
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -186,16 +190,16 @@ function keyCell(event) {
|
|||||||
|
|
||||||
function processSums() {
|
function processSums() {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]`).forEach(e => {
|
document.querySelectorAll(`[data-editable="true"][data-x="${this.dataset.x}"]:not(:empty)`).forEach(e => {
|
||||||
sum += parseFloat(e.innerText) || 0;
|
sum += parseFloat(e.innerText) * 100;
|
||||||
})
|
})
|
||||||
document.querySelector(`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`).innerText = sum;
|
document.querySelector(`.sums[data-x="${this.dataset.x}"][data-y="${lastY}"]`).innerText = sum / 100;
|
||||||
|
|
||||||
sum = 0;
|
sum = 0;
|
||||||
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]`).forEach(e => {
|
document.querySelectorAll(`[data-editable="true"][data-y="${this.dataset.y}"]:not(:empty)`).forEach(e => {
|
||||||
sum += parseFloat(e.innerText) || 0;
|
sum += parseFloat(e.innerText) * 100;
|
||||||
})
|
})
|
||||||
document.querySelector(`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`).innerText = sum;
|
document.querySelector(`.sums[data-x="${lastX}"][data-y="${this.dataset.y}"]`).innerText = sum / 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************/
|
/******************************/
|
||||||
|
@ -1,6 +1,30 @@
|
|||||||
// Tableau recap notes
|
// Tableau recap notes
|
||||||
$(function () {
|
$(function () {
|
||||||
$(function () {
|
$(function () {
|
||||||
|
let hidden_colums = ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"];
|
||||||
|
let mode_jury_but_bilan = $('table.table_recap').hasClass("table_jury_but_bilan");
|
||||||
|
if (mode_jury_but_bilan) {
|
||||||
|
// table bilan décisions: cache les notes
|
||||||
|
hidden_colums = hidden_colums.concat(["col_ue", "col_rcue", "col_lien_saisie_but"]);
|
||||||
|
} else {
|
||||||
|
hidden_colums = hidden_colums.concat(["recorded_code"]);
|
||||||
|
}
|
||||||
|
// Etat (tri des colonnes) de la table:
|
||||||
|
|
||||||
|
const url = new URL(document.URL);
|
||||||
|
const formsemestre_id = url.searchParams.get("formsemestre_id");
|
||||||
|
const order_info_key = JSON.stringify([url.pathname, formsemestre_id]);
|
||||||
|
let order_info;
|
||||||
|
if (formsemestre_id) {
|
||||||
|
const x = localStorage.getItem(order_info_key);
|
||||||
|
if (x) {
|
||||||
|
try {
|
||||||
|
order_info = JSON.parse(x);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Les boutons dépendent du mode BUT ou classique:
|
// Les boutons dépendent du mode BUT ou classique:
|
||||||
let buttons = [
|
let buttons = [
|
||||||
{
|
{
|
||||||
@ -22,16 +46,33 @@ $(function () {
|
|||||||
dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes");
|
dt.buttons('toggle_partitions:name').text(visible ? "Montrer groupes" : "Cacher les groupes");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "toggle_partitions_rangs",
|
|
||||||
text: "Rangs groupes",
|
|
||||||
action: function (e, dt, node, config) {
|
|
||||||
let rangs_visible = dt.columns(".partition_rangs").visible()[0];
|
|
||||||
dt.columns(".partition_rangs").visible(!rangs_visible);
|
|
||||||
dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
// Bouton "rangs groupes", sauf pour table jury BUT
|
||||||
|
if (!$('table.table_recap').hasClass("table_jury_but")) {
|
||||||
|
buttons.push(
|
||||||
|
{
|
||||||
|
name: "toggle_partitions_rangs",
|
||||||
|
text: "Rangs groupes",
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
let rangs_visible = dt.columns(".partition_rangs").visible()[0];
|
||||||
|
dt.columns(".partition_rangs").visible(!rangs_visible);
|
||||||
|
dt.buttons('toggle_partitions_rangs:name').text(rangs_visible ? "Rangs groupes" : "Cacher rangs groupes");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// table jury BUT: avec ou sans codes enregistrés
|
||||||
|
buttons.push(
|
||||||
|
{
|
||||||
|
name: "toggle_recorded_code",
|
||||||
|
text: "Code jury enregistrés",
|
||||||
|
action: function (e, dt, node, config) {
|
||||||
|
let visible = dt.columns(".recorded_code").visible()[0];
|
||||||
|
dt.columns(".recorded_code").visible(!visible);
|
||||||
|
dt.buttons('toggle_recorded_code:name').text(visible ? "Code jury enregistrés" : "Cacher codes jury");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!$('table.table_recap').hasClass("jury")) {
|
if (!$('table.table_recap').hasClass("jury")) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
$('table.table_recap').hasClass("apc") ?
|
$('table.table_recap').hasClass("apc") ?
|
||||||
@ -80,16 +121,19 @@ $(function () {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
buttons.push({
|
// Boutons admission, sauf pour table jury BUT
|
||||||
name: "toggle_admission",
|
if (!$('table.table_recap').hasClass("table_jury_but")) {
|
||||||
text: "Montrer infos admission",
|
buttons.push({
|
||||||
action: function (e, dt, node, config) {
|
name: "toggle_admission",
|
||||||
let visible = dt.columns(".admission").visible()[0];
|
text: "Montrer infos admission",
|
||||||
dt.columns(".admission").visible(!visible);
|
action: function (e, dt, node, config) {
|
||||||
dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission");
|
let visible = dt.columns(".admission").visible()[0];
|
||||||
}
|
dt.columns(".admission").visible(!visible);
|
||||||
})
|
dt.buttons('toggle_admission:name').text(visible ? "Montrer infos admission" : "Cacher infos admission");
|
||||||
$('table.table_recap').DataTable(
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let table = $('table.table_recap').DataTable(
|
||||||
{
|
{
|
||||||
paging: false,
|
paging: false,
|
||||||
searching: true,
|
searching: true,
|
||||||
@ -102,15 +146,16 @@ $(function () {
|
|||||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||||
aaSorting: [], // Prevent initial sorting
|
aaSorting: [], // Prevent initial sorting
|
||||||
colReorder: true,
|
colReorder: true,
|
||||||
|
stateSave: true, // enregistre état de la table (tris, ...)
|
||||||
"columnDefs": [
|
"columnDefs": [
|
||||||
{
|
{
|
||||||
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
|
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
|
||||||
targets: ["codes", "identite_detail", "partition_aux", "partition_rangs", "admission", "col_empty"],
|
targets: hidden_colums,
|
||||||
visible: false,
|
visible: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Elimine les 0 à gauche pour les exports excel et les "copy"
|
// Elimine les 0 à gauche pour les exports excel et les "copy"
|
||||||
targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae", "evaluation"],
|
targets: ["col_mod", "col_moy_gen", "col_ue", "col_res", "col_sae", "evaluation", "col_rcue"],
|
||||||
render: function (data, type, row) {
|
render: function (data, type, row) {
|
||||||
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
|
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
|
||||||
}
|
}
|
||||||
@ -141,10 +186,29 @@ $(function () {
|
|||||||
autoClose: true,
|
autoClose: true,
|
||||||
buttons: buttons,
|
buttons: buttons,
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
"drawCallback": function (settings) {
|
||||||
|
// permet de conserver l'ordre de tri des colonnes
|
||||||
|
let order_info = JSON.stringify($('table.table_recap').DataTable().order());
|
||||||
|
if (formsemestre_id) {
|
||||||
|
localStorage.setItem(order_info_key, order_info);
|
||||||
|
}
|
||||||
|
let etudids = [];
|
||||||
|
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
|
||||||
|
etudids.push(e.dataset.etudid);
|
||||||
|
});
|
||||||
|
let noms = [];
|
||||||
|
document.querySelectorAll("td.col_rcues_validables").forEach(e => {
|
||||||
|
noms.push(e.dataset.nomprenom);
|
||||||
|
});
|
||||||
|
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
|
||||||
|
localStorage.setItem(etudids_key, JSON.stringify(etudids));
|
||||||
|
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
|
||||||
|
localStorage.setItem(noms_key, JSON.stringify(noms));
|
||||||
|
},
|
||||||
|
"order": order_info,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
});
|
});
|
||||||
$('table.table_recap tbody').on('click', 'tr', function () {
|
$('table.table_recap tbody').on('click', 'tr', function () {
|
||||||
if ($(this).hasClass('selected')) {
|
if ($(this).hasClass('selected')) {
|
||||||
@ -155,10 +219,13 @@ $(function () {
|
|||||||
$(this).addClass('selected');
|
$(this).addClass('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Pour montrer et highlihter l'étudiant sélectionné:
|
// Pour montrer et surligner l'étudiant sélectionné:
|
||||||
$(function () {
|
$(function () {
|
||||||
document.querySelector("#row_selected").scrollIntoView();
|
let row_selected = document.querySelector("#row_selected");
|
||||||
window.scrollBy(0, -50);
|
if (row_selected) {
|
||||||
document.querySelector("#row_selected").classList.add("selected");
|
/*row_selected.scrollIntoView();
|
||||||
|
window.scrollBy(0, -50);*/
|
||||||
|
row_selected.classList.add("selected");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
1
app/static/links/9.3.15
Symbolic link
1
app/static/links/9.3.15
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
..
|
3
app/static/links/README.md
Normal file
3
app/static/links/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Liens symboliques utilises pour les URL vers les fichiers statiques.
|
||||||
|
|
||||||
|
Le lien est cree par scodoc.py au lancement de l'application.
|
@ -10,7 +10,7 @@
|
|||||||
{% include 'bul_head.html' %}
|
{% include 'bul_head.html' %}
|
||||||
|
|
||||||
<releve-but></releve-but>
|
<releve-but></releve-but>
|
||||||
<script src="/ScoDoc/static/js/releve-but.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/js/releve-but.js"></script>
|
||||||
|
|
||||||
{% include 'bul_foot.html' %}
|
{% include 'bul_foot.html' %}
|
||||||
|
|
||||||
|
266
app/templates/but/documentation_codes_jury.html
Normal file
266
app/templates/but/documentation_codes_jury.html
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
<div class="but_doc_codes">
|
||||||
|
<p><em>Ci-dessous la signification de chaque code est expliquée,
|
||||||
|
ainsi que la correspondance avec certains codes préconisés par
|
||||||
|
l'AMUE et l'ADIUT pour Apogée.
|
||||||
|
</em>
|
||||||
|
On distingue les codes ScoDoc (utilisés ci-dessus et dans les différentes
|
||||||
|
tables générées par ScoDoc) et leur transcription vers Apogée lors des exports
|
||||||
|
(transcription paramétrable par votre administrateur ScoDoc).
|
||||||
|
</p>
|
||||||
|
<div class="but_doc_section">Codes d'année</div>
|
||||||
|
<div class="but_doc">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>ScoDoc</th>
|
||||||
|
<th>{{nom_univ}}</th>
|
||||||
|
<th>AMUE</th>
|
||||||
|
<th>Signification</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ADM</td>
|
||||||
|
<td>{{codes["ADM"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Admis</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ADJ</td>
|
||||||
|
<td>{{codes["ADJ"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Admis par décision jury</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>PASD</td>
|
||||||
|
<td>{{codes["PASD"]}}</td>
|
||||||
|
<td class="amue">PASD</td>
|
||||||
|
<td>Non admis, mais passage de droit</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>PAS1NCI</td>
|
||||||
|
<td>{{codes["PAS1NCI"]}}</td>
|
||||||
|
<td class="amue">PAS1NCI</td>
|
||||||
|
<td>Non admis, mais passage par décision de jury (Passage en Année
|
||||||
|
Supérieure avec au moins 1 Niveau de Compétence Insuffisant (RCUE<8))
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>RED</td>
|
||||||
|
<td>{{codes["RED"]}}</td>
|
||||||
|
<td class="amue">RED</td>
|
||||||
|
<td>Ajourné, mais autorisé à redoubler</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>NAR</td>
|
||||||
|
<td>{{codes["NAR"]}}</td>
|
||||||
|
<td class="amue">REO</td>
|
||||||
|
<td>Non admis, réorientation</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DEM</td>
|
||||||
|
<td>{{codes["DEM"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Démission</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ABAN</td>
|
||||||
|
<td>{{codes["ABAN"]}}</td>
|
||||||
|
<td class="amue">ABAN</td>
|
||||||
|
<td>ABANdon constaté (sans lettre de démission)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ATJ</td>
|
||||||
|
<td>{{codes["ATJ"]}}</td>
|
||||||
|
<td class="amue">nd</td>
|
||||||
|
<td>Non validé pour une autre raison, voir règlement local</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>RAT</td>
|
||||||
|
<td>{{codes["RAT"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>En attente d’un rattrapage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>EXCLU</td>
|
||||||
|
<td>{{codes["EXCLU"]}}</td>
|
||||||
|
<td class="amue">EXC</td>
|
||||||
|
<td>EXClusion, décision réservée à des décisions disciplinaires</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DEF</td>
|
||||||
|
<td>{{codes["DEF"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>(défaillance) Non évalué par manque assiduité</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ABL</td>
|
||||||
|
<td>{{codes["ABL"]}}</td>
|
||||||
|
<td class="amue">ABL</td>
|
||||||
|
<td>Année Blanche</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="but_doc_section">Codes RCUE (niveaux de compétences annuels)</div>
|
||||||
|
|
||||||
|
<div class="but_doc">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>ScoDoc</th>
|
||||||
|
<th>{{nom_univ}}</th>
|
||||||
|
<th>AMUE</th>
|
||||||
|
<th>Signification</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>ADM</td>
|
||||||
|
<td>{{codes["ADM"]}}</td>
|
||||||
|
<th class="amue">VAL</td>
|
||||||
|
<th>Acquis</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>CMP</td>
|
||||||
|
<td>{{codes["CMP"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Acquis par compensation annuelle</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ADJ</td>
|
||||||
|
<td>{{codes["ADJ"]}}</td>
|
||||||
|
<td class="amue">CODJ</td>
|
||||||
|
<td>Acquis par décision du jury</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>AJ</td>
|
||||||
|
<td>{{codes["AJ"]}}</td>
|
||||||
|
<td class="amue">AJ</td>
|
||||||
|
<td>Attente pour problème de moyenne</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ATJ</td>
|
||||||
|
<td>{{codes["ATJ"]}}</td>
|
||||||
|
<td class="amue">nd</td>
|
||||||
|
<td>Non validé pour une autre raison, voir règlement local</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>RAT</td>
|
||||||
|
<td>{{codes["RAT"]}}</td>
|
||||||
|
<td></td>
|
||||||
|
<td>En attente d’un rattrapage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DEF</td>
|
||||||
|
<td>{{codes["DEF"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Défaillant</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ABAN</td>
|
||||||
|
<td>{{codes["ABAN"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Non évalué pour manque assiduité</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="but_doc_section">Codes des Unités d'Enseignement (UE)</div>
|
||||||
|
|
||||||
|
<div class="but_doc">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>ScoDoc</th>
|
||||||
|
<th>{{nom_univ}}</th>
|
||||||
|
<th>AMUE</th>
|
||||||
|
<th>Signification</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ADM</td>
|
||||||
|
<td>{{codes["ADM"]}}</td>
|
||||||
|
<td class="amue">VAL</td>
|
||||||
|
<td>Acquis (ECTS acquis)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>CMP</td>
|
||||||
|
<td>{{codes["CMP"]}}</td>
|
||||||
|
<td class="amue">COMP</td>
|
||||||
|
<td>Acquis par compensation UE compensée avec l’UE de même compétence et de même année (ECTS acquis)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ADJ</td>
|
||||||
|
<td>{{codes["ADJ"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Acquis par décision de jury (ECTS acquis)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>AJ</td>
|
||||||
|
<td>{{codes["AJ"]}}</td>
|
||||||
|
<td class="amue">AJ</td>
|
||||||
|
<td>Attente pour problème de moyenne</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ATJ</td>
|
||||||
|
<td>{{codes["ATJ"]}}</td>
|
||||||
|
<td class="amue">nd</td>
|
||||||
|
<td>Non validé pour une autre raison, voir règlement local</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>RAT</td>
|
||||||
|
<td>{{codes["RAT"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>En attente d’un rattrapage</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DEF</td>
|
||||||
|
<td>{{codes["DEF"]}}</td>
|
||||||
|
<td class="amue">ABAN</td>
|
||||||
|
<td>Défaillant Pas ou peu de notes par arrêt de la formation</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>ABAN</td>
|
||||||
|
<td>{{codes["ABAN"]}}</td>
|
||||||
|
<td class="amue">ABAN</td>
|
||||||
|
<td>Non évalué pour manque d’assiduité Non présentation des notes de l’étudiant au jury</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>DEM</td>
|
||||||
|
<td>{{codes["DEM"]}}</td>
|
||||||
|
<td class="amue"></td>
|
||||||
|
<td>Démission</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>UEBSL</td>
|
||||||
|
<td>{{codes["UEBSL"]}}</td>
|
||||||
|
<td class="amue">UEBSL</td>
|
||||||
|
<td>UE blanchie </td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="but_doc_section">Rappels de l'arrêté BUT (extraits)</div>
|
||||||
|
<div class="but_doc">
|
||||||
|
<ul>
|
||||||
|
<li>Au sein de chaque regroupement cohérent d’UE, la compensation est intégrale.
|
||||||
|
Si une UE n’a pas été acquise en raison d’une moyenne inférieure à 10,
|
||||||
|
cette UE sera acquise par compensation si et seulement si l’étudiant
|
||||||
|
a obtenu la moyenne au regroupement cohérent auquel l’UE appartient.</li>
|
||||||
|
<li>La poursuite d'études dans un semestre pair d’une même année est de droit
|
||||||
|
pour tout étudiant.
|
||||||
|
La poursuite d’études dans un semestre impair est possible
|
||||||
|
<em>si et seulement si</em> l’étudiant a obtenu :
|
||||||
|
<ul>
|
||||||
|
<li>la moyenne à plus de la moitié des regroupements cohérents d’UE</li>
|
||||||
|
<li>et une moyenne égale ou supérieure à 8 sur 20 à chaque regroupement cohérent d’UE.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>La poursuite d'études dans le semestre 5 nécessite de plus la validation de toutes les UE des
|
||||||
|
semestres 1 et 2 dans les conditions de validation des points 4.3 et 4.4, ou par décision de jury.</li>
|
||||||
|
</ul>
|
||||||
|
<b>Textes de référence:</b>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://www.enseignementsup-recherche.gouv.fr/fr/bo/21/Special4/ESRS2114777A.htm">Bulletin
|
||||||
|
officiel spécial n°4 du 17 juin 2021</a></li>
|
||||||
|
<li><a
|
||||||
|
href="https://cache.media.enseignementsup-recherche.gouv.fr//file/SPE4-MESRI-17-6-2021/19/4/SP4_ESR_17_6_2021_1413194.pdf">version
|
||||||
|
pdf complète</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
30
app/templates/but/formsemestre_validation_auto_but.html
Normal file
30
app/templates/but/formsemestre_validation_auto_but.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{# -*- mode: jinja-html -*- #}
|
||||||
|
{% extends "sco_page.html" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<h2>Calcul automatique des décisions de jury annuelle BUT</h2>
|
||||||
|
<ul>
|
||||||
|
<li>Seuls les étudiants qui valident l'année seront affectés:
|
||||||
|
tous les niveaux de compétences (RCUE) validables
|
||||||
|
(moyenne annuelle au dessus de 10);
|
||||||
|
</li>
|
||||||
|
<li>l'assiduité n'est <b>pas</b> prise en compte;</li>
|
||||||
|
</ul>
|
||||||
|
<p class="warning">
|
||||||
|
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-5">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -6,11 +6,25 @@
|
|||||||
<h1>Associer un référentiel de compétences</h1>
|
<h1>Associer un référentiel de compétences</h1>
|
||||||
<div class="help">
|
<div class="help">
|
||||||
Association d'un référentiel de compétence à la formation
|
Association d'un référentiel de compétence à la formation
|
||||||
{{formation.titre}} ({{formation.acronyme}})
|
<a href="{{
|
||||||
|
url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||||
|
}}">{{formation.titre}} ({{formation.acronyme}})</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div style="margin-top: 20px; margin-bottom: 20px;">
|
||||||
<div class="col-md-4">
|
|
||||||
{{ wtf.quick_form(form) }}
|
Référentiel actuellement associé:
|
||||||
|
{% if formation.referentiel_competence is not none %}
|
||||||
|
<b>{{ formation.referentiel_competence.specialite_long }}</b>
|
||||||
|
<a href="{{
|
||||||
|
url_for('notes.refcomp_desassoc_formation', scodoc_dept=g.scodoc_dept, formation_id=formation.id)
|
||||||
|
}}" class="stdlink">supprimer</a>
|
||||||
|
{% else %}
|
||||||
|
<b>aucun</b>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row" style="margin-top: 20px;">
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ wtf.quick_form(form) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
<ref-competences></ref-competences>
|
<ref-competences></ref-competences>
|
||||||
|
|
||||||
<script src="/ScoDoc/static/js/ref_competences.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/js/ref_competences.js"></script>
|
||||||
|
|
||||||
<div class="help">
|
<div class="help">
|
||||||
Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à
|
Référentiel chargé le {{ref.scodoc_date_loaded.strftime("%d/%m/%Y à %H:%M") if ref.scodoc_date_loaded else ""}} à
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
|
|
||||||
<div class="help">
|
<div class="help">
|
||||||
<p>Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury
|
<p>Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury
|
||||||
et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées
|
et les validations de semestres ou d'UE.
|
||||||
dans les exports Apogée.
|
Les valeurs indiquées ici sont utilisées dans les exports Apogée.
|
||||||
<p>
|
<p>
|
||||||
<p>Ne les modifier que si vous savez ce que vous faites !
|
<p>Ne les modifier que si vous savez ce que vous faites !
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
22
app/templates/confirm_dialog.html
Normal file
22
app/templates/confirm_dialog.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{# -*- mode: jinja-html -*- #}
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
{{ explanation }}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<form method="post">
|
||||||
|
<input type="submit" value="OK" />
|
||||||
|
{% if cancel_url %}
|
||||||
|
<input type="button" value="Annuler" style="margin-left: 16px;"
|
||||||
|
onClick="document.location='{{ cancel_url }}';" />
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -84,17 +84,29 @@
|
|||||||
url_for("notes.module_create",
|
url_for("notes.module_create",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
module_type=module_type|int,
|
module_type=module_type|int,
|
||||||
matiere_id=matiere_parent.id
|
matiere_id=matiere_parent.id,
|
||||||
|
semestre_id=semestre_id,
|
||||||
)}}"
|
)}}"
|
||||||
{% else %}"{{
|
{% else %}"{{
|
||||||
url_for("notes.module_create",
|
url_for("notes.module_create",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
module_type=module_type|int,
|
module_type=module_type|int,
|
||||||
formation_id=formation.id
|
formation_id=formation.id,
|
||||||
|
semestre_id=semestre_id,
|
||||||
)}}"
|
)}}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
>{{create_element_msg}}</a>
|
>{{create_element_msg}}</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if module_type==scu.ModuleType.STANDARD %}
|
||||||
|
<li><a href="{{
|
||||||
|
url_for('notes.formation_add_malus_modules',
|
||||||
|
scodoc_dept=g.scodoc_dept,
|
||||||
|
formation_id=formation.id,
|
||||||
|
semestre_id=semestre_id)
|
||||||
|
}}" class="stdlink">ajouter un module de malus dans chaque UE du S{{semestre_id}}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -2,10 +2,12 @@
|
|||||||
<h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2>
|
<h2>{% if not read_only %}Édition des c{% else %}C{%endif%}oefficients des modules vers les UEs</h2>
|
||||||
<div class="help">
|
<div class="help">
|
||||||
{% if not read_only %}
|
{% if not read_only %}
|
||||||
Double-cliquer pour changer une valeur.
|
<p>Double-cliquer pour changer une valeur.
|
||||||
Les valeurs sont automatiquement enregistrées au fur et à mesure.
|
Les valeurs sont automatiquement enregistrées au fur et à mesure.
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<p>Chaque ligne représente une ressource ou SAÉ, et chaque colonne une Unité d'Enseignement (UE).
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form class="semestre_selector">Semestre:
|
<form class="semestre_selector">Semestre:
|
||||||
<select onchange="this.form.submit()"" name="semestre_idx" id="semestre_idx">
|
<select onchange="this.form.submit()"" name="semestre_idx" id="semestre_idx">
|
||||||
|
@ -32,14 +32,30 @@
|
|||||||
ue.color if ue.color is not none else 'blue'}}"></span>
|
ue.color if ue.color is not none else 'blue'}}"></span>
|
||||||
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
|
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
|
||||||
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
|
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
|
||||||
|
title="{{ue.acronyme}}: {{
|
||||||
|
('pas de compétence associée'
|
||||||
|
if ue.niveau_competence is none
|
||||||
|
else 'compétence ' + ue.niveau_competence.annee + ' ' + ue.niveau_competence.competence.titre_long)
|
||||||
|
if ue.type == 0
|
||||||
|
else ''
|
||||||
|
}}"
|
||||||
>{{ue.titre}}</a>
|
>{{ue.titre}}</a>
|
||||||
{% set virg = joiner(", ") %}
|
{% set virg = joiner(", ") %}
|
||||||
<span class="ue_code">(
|
<span class="ue_code">(
|
||||||
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
|
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
|
||||||
{{ virg() }}{{ue.ects if ue.ects is not none
|
{{ virg() }}
|
||||||
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS)
|
{%- if ue.type == 0 -%}
|
||||||
|
{{ue.ects
|
||||||
|
if ue.ects is not none
|
||||||
|
else '<span class="missing_ue_ects">aucun</span>'|safe
|
||||||
|
}} ECTS
|
||||||
|
{%- endif -%}
|
||||||
|
)
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
{% if (ue.niveau_competence is none) and ue.type == 0 %}
|
||||||
|
<span class="fontred">pas de compétence associée</span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if editable and not ue.is_locked() %}
|
{% if editable and not ue.is_locked() %}
|
||||||
<a class="stdlink" href="{{ url_for('notes.ue_edit',
|
<a class="stdlink" href="{{ url_for('notes.ue_edit',
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
<li>Code: <tt>{{ue.ue_code}}</tt></li>
|
<li>Code: <tt>{{ue.ue_code}}</tt></li>
|
||||||
<li>Type: {{ue.type}}</li>
|
<li>Type: {{ue.type}}</li>
|
||||||
<li>Externe: {{ "oui" if ue.is_external else "non" }}</li>
|
<li>Externe: {{ "oui" if ue.is_external else "non" }}</li>
|
||||||
<li>Code Apogée: {{ue.code_apogee}}</li>
|
<li>Code Apogée: {{ue.code_apogee or "aucun"}}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Formation:
|
<li>Formation:
|
||||||
|
@ -4,13 +4,14 @@
|
|||||||
{% block styles %}
|
{% block styles %}
|
||||||
{{super()}}
|
{{super()}}
|
||||||
<link type="text/css" rel="stylesheet"
|
<link type="text/css" rel="stylesheet"
|
||||||
href="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
href="{{sco.scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
||||||
<link rel="stylesheet" href="/ScoDoc/static/css/scodoc.css">
|
<link rel="stylesheet" href="{{sco.scu.STATIC_DIR}}/css/scodoc.css">
|
||||||
<link href="/ScoDoc/static/css/menu.css" rel="stylesheet" type="text/css" />
|
<link href="{{sco.scu.STATIC_DIR}}/css/menu.css" rel="stylesheet" type="text/css" />
|
||||||
<link href="/ScoDoc/static/css/gt_table.css" rel="stylesheet" type="text/css" />
|
<link href="{{sco.scu.STATIC_DIR}}/css/gt_table.css" rel="stylesheet" type="text/css" />
|
||||||
<link type="text/css" rel="stylesheet" href="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
<link type="text/css" rel="stylesheet" href="{{sco.scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||||
{# <link href="/ScoDoc/static/css/tooltip.css" rel="stylesheet" type="text/css" /> #}
|
{#
|
||||||
<link rel="stylesheet" type="text/css" href="/ScoDoc/static/DataTables/datatables.min.css" />
|
<link href="{{sco.scu.STATIC_DIR}}/css/tooltip.css" rel="stylesheet" type="text/css" /> #}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{sco.scu.STATIC_DIR}}/DataTables/datatables.min.css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}
|
||||||
@ -25,9 +26,9 @@
|
|||||||
<div id="gtrcontent" class="gtrcontent">
|
<div id="gtrcontent" class="gtrcontent">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
|
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% if sco.sem %}
|
{% if sco.sem %}
|
||||||
@ -46,16 +47,16 @@
|
|||||||
{{ super() }}
|
{{ super() }}
|
||||||
{{ moment.include_moment() }}
|
{{ moment.include_moment() }}
|
||||||
{{ moment.lang(g.locale) }}
|
{{ moment.lang(g.locale) }}
|
||||||
<script src="/ScoDoc/static/libjs/menu.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/libjs/menu.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/bubble.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/libjs/bubble.js"></script>
|
||||||
<script src="/ScoDoc/static/jQuery/jquery.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/jQuery/jquery.js"></script>
|
||||||
<script src="/ScoDoc/static/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/jQuery/jquery-migrate-1.2.0.min.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/jquery.field.min.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/libjs/jquery.field.min.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||||
<script src="/ScoDoc/static/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||||
|
|
||||||
<script src="/ScoDoc/static/js/scodoc.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/js/scodoc.js"></script>
|
||||||
<script src="/ScoDoc/static/DataTables/datatables.min.js"></script>
|
<script src="{{sco.scu.STATIC_DIR}}/DataTables/datatables.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
window.onload = function () { enableTooltips("gtrcontent") };
|
window.onload = function () { enableTooltips("gtrcontent") };
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
{# -*- mode: jinja-html -*- #}
|
{# -*- mode: jinja-html -*- #}
|
||||||
{{ sco_header|safe }}
|
{{ sco_header|safe }}
|
||||||
<h2 class="formsemestre">Affectation aux groupes de {{ partition["partition_name"] }}</h2>
|
<h2 class="formsemestre">Affectation aux groupes de {{ partition.partition_name }}</h2>
|
||||||
|
|
||||||
<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne
|
<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne
|
||||||
sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>".
|
sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>".
|
||||||
Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien
|
Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien
|
||||||
"suppr." en haut à droite de sa boite.
|
"suppr." en haut à droite de sa boite.
|
||||||
Vous pouvez aussi <a class="stdlink"
|
Vous pouvez aussi <a class="stdlink"
|
||||||
href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition['partition_id']) }}"
|
href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition.id) }}"
|
||||||
>répartir automatiquement les groupes</a>.
|
>répartir automatiquement les groupes</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -15,24 +15,24 @@ href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, pa
|
|||||||
<div id="ginfo"></div>
|
<div id="ginfo"></div>
|
||||||
<div id="savedinfo"></div>
|
<div id="savedinfo"></div>
|
||||||
<form name="formGroup" id="formGroup" onSubmit="return false;">
|
<form name="formGroup" id="formGroup" onSubmit="return false;">
|
||||||
<input type="hidden" name="partition_id" value="{{ partition['partition_id'] }}"/>
|
<input type="hidden" name="partition_id" value="{{ partition.id }}"/>
|
||||||
<input name="groupName" size="6"/>
|
<input name="groupName" size="6"/>
|
||||||
<input type="button" onClick="createGroup();" value="Créer groupe"/>
|
<input type="button" onClick="createGroup();" value="Créer groupe"/>
|
||||||
|
|
||||||
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
|
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
|
||||||
|
|
||||||
<input type="button"
|
<input type="button"
|
||||||
onClick="document.location = '{{ url_for( 'notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }}'"
|
onClick="document.location = '{{ url_for( 'notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }}'"
|
||||||
value="Annuler" /> Éditer groupes de
|
value="Annuler" /> Éditer groupes de
|
||||||
<select name="other_partition_id" onchange="GotoAnother();">
|
<select name="other_partition_id" onchange="GotoAnother();">
|
||||||
{% for p in partitions_list %}
|
{% for p in partitions_list %}
|
||||||
<option value="{{ p['id'] }}" {{
|
<option value="{{ p.id }}" {{
|
||||||
"selected" if p['partition_id'] == partition['partition_id']
|
"selected" if p.id == partition.id
|
||||||
}}>{{
|
}}>{{
|
||||||
p["partition_name"]
|
p.partition_name
|
||||||
}}</option>
|
}}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div id="groups">
|
<div id="groups">
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user