forked from ScoDoc/ScoDoc
Merge branch 'master' into feuille_jury
This commit is contained in:
commit
5ba720baee
144
app/api/jury.py
144
app/api/jury.py
|
@ -12,11 +12,20 @@ from flask_json import as_json
|
|||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.but import jury_but_results
|
||||
from app.models import FormSemestre
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
|
@ -36,3 +45,134 @@ def decisions_jury(formsemestre_id: int):
|
|||
return rows
|
||||
else:
|
||||
raise ScoException("non implemente")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_formsemestre/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
# c'est la même chose (formations classiques)
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
def _validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation (semestres classiques ou UEs)"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ScolarFormSemestreValidation.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/autorisation_inscription/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ScolarAutorisationInscription.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"autorisation_inscription_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationRCUE.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_annee_but/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
return "ok"
|
||||
|
|
|
@ -14,6 +14,7 @@ Classe raccordant avec ScoDoc 7:
|
|||
|
||||
"""
|
||||
import collections
|
||||
from operator import attrgetter
|
||||
from typing import Union
|
||||
|
||||
from flask import g, url_for
|
||||
|
@ -23,8 +24,6 @@ from app import log
|
|||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
from app.comp import res_sem
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
|
@ -45,7 +44,7 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
|||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import RED, UE_STANDARD
|
||||
from app.scodoc.codes_cursus import code_ue_validant, RED, UE_STANDARD
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
|
@ -104,7 +103,7 @@ class EtudCursusBUT:
|
|||
self.parcour: ApcParcours = self.inscriptions[-1].parcour
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
"{ annee:int : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
|
@ -118,21 +117,6 @@ class EtudCursusBUT:
|
|||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
# Probablement inutile:
|
||||
# # Cherche les validations de jury enregistrées pour chaque niveau
|
||||
# self.validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
# " { niveau_id : [ ApcValidationRCUE ] }"
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# self.validations_by_niveau[validation_rcue.niveau().id].append(
|
||||
# validation_rcue
|
||||
# )
|
||||
# self.validation_by_niveau = {
|
||||
# niveau_id: sorted(
|
||||
# validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||
# )[0]
|
||||
# for niveau_id, validations in self.validations_by_niveau.items()
|
||||
# }
|
||||
# "{ niveau_id : meilleure validation pour ce niveau }"
|
||||
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
|
@ -146,7 +130,7 @@ class EtudCursusBUT:
|
|||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDERED[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation["code"]]
|
||||
> sco_codes.BUT_CODES_ORDERED[previous_validation.code]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
|
@ -206,6 +190,23 @@ class EtudCursusBUT:
|
|||
)
|
||||
return d
|
||||
|
||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||
meilleure validation pour ce niveau
|
||||
"""
|
||||
validations_by_niveau = collections.defaultdict(lambda: [])
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=self.etud):
|
||||
validations_by_niveau[validation_rcue.niveau().id].append(validation_rcue)
|
||||
validation_by_niveau = {
|
||||
niveau_id: sorted(
|
||||
validations, key=lambda v: sco_codes.BUT_CODES_ORDERED[v.code]
|
||||
)[0]
|
||||
for niveau_id, validations in validations_by_niveau.items()
|
||||
if validations
|
||||
}
|
||||
return validation_by_niveau
|
||||
|
||||
|
||||
class FormSemestreCursusBUT:
|
||||
"""L'état des étudiants d'un formsemestre dans leur cursus BUT
|
||||
|
@ -358,6 +359,40 @@ class FormSemestreCursusBUT:
|
|||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def etud_ues_de_but1_non_validees(
|
||||
etud: Identite, formation: Formation, parcour: ApcParcours
|
||||
) -> list[UniteEns]:
|
||||
"""Vrai si cet étudiant a validé toutes ses UEs de S1 et S2, dans son parcours"""
|
||||
# Les UEs avec décisions, dans les S1 ou S2 d'une formation de même code:
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.filter(ScolarFormSemestreValidation.ue_id != None)
|
||||
.join(UniteEns)
|
||||
.filter(db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2))
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
)
|
||||
codes_validations_by_ue = collections.defaultdict(list)
|
||||
for v in validations:
|
||||
codes_validations_by_ue[v.ue_id].append(v.code)
|
||||
|
||||
# Les UEs du parcours en S1 et S2:
|
||||
ues = formation.query_ues_parcour(parcour).filter(
|
||||
db.or_(UniteEns.semestre_idx == 1, UniteEns.semestre_idx == 2)
|
||||
)
|
||||
# Liste triée des ues non validées
|
||||
return sorted(
|
||||
[
|
||||
ue
|
||||
for ue in ues
|
||||
if any(
|
||||
(not code_ue_validant(code) for code in codes_validations_by_ue[ue.id])
|
||||
)
|
||||
],
|
||||
key=attrgetter("numero", "acronyme"),
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_warning_apc_setup(
|
||||
formsemestre: FormSemestre, res: ResultatsSemestreBUT
|
||||
) -> str:
|
||||
|
|
|
@ -58,6 +58,7 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
|
|||
DecisionsProposeesRCUE appelera .set_compensable()
|
||||
si on a la possibilité de la compenser dans le RCUE.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import html
|
||||
from operator import attrgetter
|
||||
import re
|
||||
|
@ -68,15 +69,15 @@ from flask import flash, g, url_for
|
|||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.but import cursus_but
|
||||
from app.but.cursus_but import EtudCursusBUT
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp import res_sem
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcParcoursNiveauCompetence,
|
||||
)
|
||||
from app.models import Scolog, ScolarAutorisationInscription
|
||||
from app.models.but_validations import (
|
||||
|
@ -86,12 +87,13 @@ from app.models.but_validations import (
|
|||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import (
|
||||
code_rcue_validant,
|
||||
BUT_CODES_ORDERED,
|
||||
CODES_RCUE_VALIDES,
|
||||
CODES_UE_CAPITALISANTS,
|
||||
|
@ -275,7 +277,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
if self.formsemestre_impair is not None:
|
||||
self.validation = ApcValidationAnnee.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
formsemestre_id=formsemestre_impair.id,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
ordre=self.annee_but,
|
||||
).first()
|
||||
else:
|
||||
|
@ -360,15 +362,33 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
"Vrai si plus de la moitié des RCUE validables"
|
||||
self.passage_de_droit = self.valide_moitie_rcue and (self.nb_rcues_under_8 == 0)
|
||||
"Vrai si peut passer dans l'année BUT suivante: plus de la moitié validables et tous > 8"
|
||||
# XXX TODO ajouter condition pour passage en S5
|
||||
explanation = ""
|
||||
# Cas particulier du passage en BUT 3: nécessité d’avoir validé toutes les UEs du BUT 1.
|
||||
if self.passage_de_droit and self.annee_but == 2:
|
||||
inscription = formsemestre.etuds_inscriptions.get(etud.id)
|
||||
if inscription:
|
||||
ues_but1_non_validees = cursus_but.etud_ues_de_but1_non_validees(
|
||||
etud, formation, inscription.parcour
|
||||
)
|
||||
self.passage_de_droit = not ues_but1_non_validees
|
||||
explanation += (
|
||||
f"""UEs de BUT1 non validées: <b>{
|
||||
', '.join(ue.acronyme for ue in ues_but1_non_validees)
|
||||
}</b>. """
|
||||
if ues_but1_non_validees
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
# pas inscrit dans le semestre courant ???
|
||||
self.passage_de_droit = False
|
||||
|
||||
# Enfin calcule les codes des UE:
|
||||
# Enfin calcule les codes des UEs:
|
||||
for dec_ue in self.decisions_ues.values():
|
||||
dec_ue.compute_codes()
|
||||
|
||||
# Reste à attribuer ADM, ADJ, PASD, PAS1NCI, RED, NAR
|
||||
plural = self.nb_validables > 1
|
||||
expl_rcues = f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
|
||||
explanation += f"""{self.nb_validables} niveau{"x" if plural else ""} validable{
|
||||
"s" if plural else ""} sur {self.nb_competences}"""
|
||||
if self.admis:
|
||||
self.codes = [sco_codes.ADM] + self.codes
|
||||
|
@ -387,7 +407,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
sco_codes.ABL,
|
||||
sco_codes.EXCLU,
|
||||
]
|
||||
expl_rcues = ""
|
||||
explanation = ""
|
||||
elif self.passage_de_droit:
|
||||
self.codes = [sco_codes.PASD, sco_codes.ADJ] + self.codes
|
||||
elif self.valide_moitie_rcue: # mais au moins 1 rcue insuffisante
|
||||
|
@ -397,7 +417,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
sco_codes.PAS1NCI,
|
||||
sco_codes.ADJ,
|
||||
] + self.codes
|
||||
expl_rcues += f" et {self.nb_rcues_under_8} < 8"
|
||||
explanation += f" et {self.nb_rcues_under_8} < 8"
|
||||
else:
|
||||
self.codes = [
|
||||
sco_codes.RED,
|
||||
|
@ -406,14 +426,17 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
sco_codes.ADJ,
|
||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||
] + self.codes
|
||||
expl_rcues += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
explanation += f""" et {self.nb_rcues_under_8} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
|
||||
# Si l'un des semestres est extérieur, propose ADM
|
||||
if (
|
||||
self.formsemestre_impair and self.formsemestre_impair.modalite == "EXT"
|
||||
) or (self.formsemestre_pair and self.formsemestre_pair.modalite == "EXT"):
|
||||
self.codes.insert(0, sco_codes.ADM)
|
||||
self.explanation = f"<div>{expl_rcues}</div>"
|
||||
# Si validée par niveau supérieur:
|
||||
if self.code_valide == sco_codes.ADSUP:
|
||||
self.codes.insert(0, sco_codes.ADSUP)
|
||||
self.explanation = f"<div>{explanation}</div>"
|
||||
messages = self.descr_pb_coherence()
|
||||
if messages:
|
||||
self.explanation += (
|
||||
|
@ -448,7 +471,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
scodoc_dept=g.scodoc_dept,
|
||||
semestre_idx=formsemestre.semestre_id,
|
||||
formation_id=formsemestre.formation.id)}">
|
||||
{formsemestre.formation.to_html()} ({
|
||||
{formsemestre.formation.html()} ({
|
||||
formsemestre.formation.id})</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -726,16 +749,18 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
dec_ue.record(code)
|
||||
for dec_rcue, code in codes_rcues:
|
||||
dec_rcue.record(code)
|
||||
self.record(code_annee)
|
||||
self.record(code_annee, mark_recorded=False)
|
||||
self.record_autorisation_inscription(code_annee)
|
||||
self.record_all()
|
||||
self.recorded = True
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def record(self, code: str, no_overwrite=False) -> bool:
|
||||
def record(self, code: str, no_overwrite=False, mark_recorded: bool = True) -> bool:
|
||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||
Si no_overwrite, ne fait rien si un code est déjà enregistré.
|
||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||
Si mark_recorded est vrai, positionne self.recorded
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
return False
|
||||
|
@ -755,6 +780,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
self.validation = ApcValidationAnnee(
|
||||
etudid=self.etud.id,
|
||||
formsemestre=self.formsemestre_impair,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
ordre=self.annee_but,
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
code=code,
|
||||
|
@ -766,7 +792,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||
)
|
||||
self.recorded = True
|
||||
if mark_recorded:
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
return True
|
||||
|
||||
|
@ -873,7 +900,10 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
pour cette année: décisions d'UE, de RCUE, d'année,
|
||||
et autorisations d'inscription émises.
|
||||
Efface même si étudiant DEM ou DEF.
|
||||
Si à cheval, n'efface que pour le semestre d'origine du deca.
|
||||
Si à cheval ou only_one_sem, n'efface que les décisions UE et les
|
||||
autorisations de passage du semestre d'origine du deca.
|
||||
|
||||
Dans tous les cas, efface les validations de l'année en cours.
|
||||
(commite la session.)
|
||||
"""
|
||||
if only_one_sem or self.a_cheval:
|
||||
|
@ -888,8 +918,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
else:
|
||||
for dec_ue in self.decisions_ues.values():
|
||||
dec_ue.erase()
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
dec_rcue.erase()
|
||||
|
||||
if self.formsemestre_impair:
|
||||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
self.etud.id, self.formsemestre_impair.id
|
||||
|
@ -898,18 +927,27 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
|||
ScolarAutorisationInscription.delete_autorisation_etud(
|
||||
self.etud.id, self.formsemestre_pair.id
|
||||
)
|
||||
validations = ApcValidationAnnee.query.filter_by(
|
||||
# Efface les RCUEs
|
||||
for dec_rcue in self.decisions_rcue_by_niveau.values():
|
||||
dec_rcue.erase()
|
||||
|
||||
# Efface les validations concernant l'année BUT
|
||||
# de ce semestre
|
||||
validations = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
formsemestre_id=self.formsemestre_impair.id,
|
||||
ordre=self.annee_but,
|
||||
)
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=self.formsemestre.formation.formation_code)
|
||||
)
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
|
||||
# Efface éventuelles validations de semestre
|
||||
# (en principe inutilisées en BUT)
|
||||
|
@ -1035,6 +1073,9 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
|||
):
|
||||
super().__init__(etud=dec_prop_annee.etud)
|
||||
self.deca = dec_prop_annee
|
||||
self.referentiel_competence_id = (
|
||||
self.deca.formsemestre.formation.referentiel_competence_id
|
||||
)
|
||||
self.rcue = rcue
|
||||
if rcue is None: # RCUE non dispo, eg un seul semestre
|
||||
self.codes = []
|
||||
|
@ -1139,7 +1180,8 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
|||
dec_ue.record(sco_codes.ADJR)
|
||||
|
||||
# Valide les niveaux inférieurs de la compétence (code ADSUP)
|
||||
# TODO
|
||||
if code in CODES_RCUE_VALIDES:
|
||||
self.valide_niveau_inferieur()
|
||||
|
||||
if self.rcue.formsemestre_1 is not None:
|
||||
sco_cache.invalidate_formsemestre(
|
||||
|
@ -1177,6 +1219,191 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
|||
return f"{niveau_titre}-{ordre}"
|
||||
return ""
|
||||
|
||||
def valide_niveau_inferieur(self) -> None:
|
||||
"""Appelé juste après la validation d'un RCUE.
|
||||
*La validation des deux UE du niveau d’une compétence emporte la validation de
|
||||
l’ensemble des UEs du niveau inférieur de cette même compétence.*
|
||||
"""
|
||||
if not self.rcue or not self.rcue.ue_1 or not self.rcue.ue_1.niveau_competence:
|
||||
return
|
||||
competence: ApcCompetence = self.rcue.ue_1.niveau_competence.competence
|
||||
ordre_inferieur = self.rcue.ue_1.niveau_competence.ordre - 1
|
||||
if ordre_inferieur < 1:
|
||||
return # pas de niveau inferieur
|
||||
|
||||
# --- Si le RCUE inférieur est déjà validé, ne fait rien
|
||||
validations_rcue = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=self.etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.join(ApcNiveau)
|
||||
.filter_by(ordre=ordre_inferieur)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(id=competence.id)
|
||||
.all()
|
||||
)
|
||||
if [v for v in validations_rcue if code_rcue_validant(v.code)]:
|
||||
return # déjà validé
|
||||
|
||||
# --- Validations des UEs
|
||||
ues, ue1, ue2 = self._get_ues_inferieures(competence, ordre_inferieur)
|
||||
# Pour chaque UE inférieure non validée, valide:
|
||||
for ue in ues:
|
||||
validations_ue = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, ue_id=ue.id
|
||||
).all()
|
||||
if [
|
||||
validation
|
||||
for validation in validations_ue
|
||||
if sco_codes.code_ue_validant(validation.code)
|
||||
]:
|
||||
continue # on a déjà une validation
|
||||
# aucune validation validante
|
||||
validation_ue = validations_ue[0] if validations_ue else None
|
||||
if validation_ue:
|
||||
# Modifie validation existante
|
||||
validation_ue.code = sco_codes.ADSUP
|
||||
validation_ue.event_date = datetime.now()
|
||||
if validation_ue.formsemestre_id is not None:
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=validation_ue.formsemestre_id
|
||||
)
|
||||
log(f"updating {validation_ue}")
|
||||
else:
|
||||
# Ajoute une validation,
|
||||
# pas de formsemestre ni de note car pas une capitalisation
|
||||
validation_ue = ScolarFormSemestreValidation(
|
||||
etudid=self.etud.id,
|
||||
code=sco_codes.ADSUP,
|
||||
ue_id=ue.id,
|
||||
is_external=True, # pas rattachée à un formsemestre
|
||||
)
|
||||
log(f"recording {validation_ue}")
|
||||
db.session.add(validation_ue)
|
||||
|
||||
# Valide le RCUE inférieur
|
||||
if validations_rcue:
|
||||
# Met à jour validation existante
|
||||
validation_rcue = validations_rcue[0]
|
||||
validation_rcue.code = sco_codes.ADSUP
|
||||
validation_rcue.date = datetime.now()
|
||||
db.session.add(validation_rcue)
|
||||
db.session.commit()
|
||||
log(f"updating {validation_rcue}")
|
||||
if validation_rcue.formsemestre_id is not None:
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=validation_rcue.formsemestre_id
|
||||
)
|
||||
elif ue1 and ue2:
|
||||
# Crée nouvelle validation
|
||||
validation_rcue = ApcValidationRCUE(
|
||||
etudid=self.etud.id, ue1_id=ue1.id, ue2_id=ue2.id, code=sco_codes.ADSUP
|
||||
)
|
||||
db.session.add(validation_rcue)
|
||||
db.session.commit()
|
||||
log(f"recording {validation_rcue}")
|
||||
self.valide_annee_inferieure()
|
||||
|
||||
def valide_annee_inferieure(self) -> None:
|
||||
"""Si tous les RCUEs de l'année inférieure sont validés, la valide"""
|
||||
# Indice de l'année inférieure:
|
||||
annee_courante = self.rcue.ue_1.niveau_competence.annee # "BUT2"
|
||||
if not re.match(r"^BUT\d$", annee_courante):
|
||||
log("Warning: valide_annee_inferieure invalid annee_courante")
|
||||
return
|
||||
annee_inferieure = int(annee_courante[3]) - 1
|
||||
if annee_inferieure < 1:
|
||||
return
|
||||
# Garde-fou: Année déjà validée ?
|
||||
validations_annee: ApcValidationAnnee = ApcValidationAnnee.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
ordre=annee_inferieure,
|
||||
formation_id=self.rcue.formsemestre_1.formation_id,
|
||||
).all()
|
||||
if len(validations_annee) > 1:
|
||||
log(
|
||||
f"warning: {len(validations_annee)} validations d'année\n{validations_annee}"
|
||||
)
|
||||
if [
|
||||
validation_annee
|
||||
for validation_annee in validations_annee
|
||||
if sco_codes.code_annee_validant(validation_annee.code)
|
||||
]:
|
||||
return # déja valide
|
||||
validation_annee = validations_annee[0] if validations_annee else None
|
||||
# Liste des niveaux à valider:
|
||||
# ici on sort l'artillerie lourde
|
||||
cursus: EtudCursusBUT = EtudCursusBUT(
|
||||
self.etud, self.rcue.formsemestre_1.formation
|
||||
)
|
||||
niveaux_a_valider = cursus.niveaux_by_annee[annee_inferieure]
|
||||
# Pour chaque niveau, cherche validation RCUE
|
||||
validations_by_niveau = cursus.load_validation_by_niveau()
|
||||
ok = True
|
||||
for niveau in niveaux_a_valider:
|
||||
validation_niveau: ApcValidationRCUE = validations_by_niveau.get(niveau.id)
|
||||
if not validation_niveau or not sco_codes.code_rcue_validant(
|
||||
validation_niveau.code
|
||||
):
|
||||
ok = False
|
||||
|
||||
# Si tous OK, émet validation année
|
||||
if validation_annee: # Modifie la validation antérieure (non validante)
|
||||
validation_annee.code = sco_codes.ADSUP
|
||||
validation_annee.date = datetime.now()
|
||||
log(f"updating {validation_annee}")
|
||||
else:
|
||||
validation_annee = ApcValidationAnnee(
|
||||
etudid=self.etud.id,
|
||||
ordre=annee_inferieure,
|
||||
code=sco_codes.ADSUP,
|
||||
formation_id=self.rcue.formsemestre_1.formation_id,
|
||||
# met cette validation sur l'année scolaire actuelle, pas la précédente (??)
|
||||
annee_scolaire=self.rcue.formsemestre_1.annee_scolaire(),
|
||||
)
|
||||
log(f"recording {validation_annee}")
|
||||
db.session.add(validation_annee)
|
||||
db.session.commit()
|
||||
|
||||
def _get_ues_inferieures(
|
||||
self, competence: ApcCompetence, ordre_inferieur: int
|
||||
) -> tuple[list[UniteEns], UniteEns, UniteEns]:
|
||||
"""Les UEs de cette formation associées au niveau de compétence inférieur ?
|
||||
Note: on ne cherche que dans la formation courante, pas les UEs de
|
||||
même code d'autres formations.
|
||||
"""
|
||||
formation: Formation = self.rcue.formsemestre_1.formation
|
||||
ues: list[UniteEns] = (
|
||||
UniteEns.query.filter_by(formation_id=formation.id)
|
||||
.filter(UniteEns.semestre_idx != None)
|
||||
.join(ApcNiveau)
|
||||
.filter_by(ordre=ordre_inferieur)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(id=competence.id)
|
||||
.all()
|
||||
)
|
||||
log(f"valide_niveau_inferieur: {competence} UEs inférieures: {ues}")
|
||||
if len(ues) != 2: # on n'a pas 2 UE associées au niveau inférieur !
|
||||
flash(
|
||||
"Impossible de valider le niveau de compétence inférieur: pas 2 UEs associées'",
|
||||
"warning",
|
||||
)
|
||||
return [], None, None
|
||||
ues_impaires = [ue for ue in ues if ue.semestre_idx % 2]
|
||||
if len(ues_impaires) != 1:
|
||||
flash(
|
||||
"Impossible de valider le niveau de compétence inférieur: pas d'UE impaire associée"
|
||||
)
|
||||
return [], None, None
|
||||
ue1 = ues_impaires[0]
|
||||
ues_paires = [ue for ue in ues if not ue.semestre_idx % 2]
|
||||
if len(ues_paires) != 1:
|
||||
flash(
|
||||
"Impossible de valider le niveau de compétence inférieur: pas d'UE paire associée"
|
||||
)
|
||||
return [], None, None
|
||||
ue2 = ues_paires[0]
|
||||
return ues, ue1, ue2
|
||||
|
||||
|
||||
class DecisionsProposeesUE(DecisionsProposees):
|
||||
"""Décisions de jury sur une UE du BUT
|
||||
|
@ -1383,23 +1610,29 @@ class BUTCursusEtud: # WIP TODO
|
|||
for competence in self.competences_du_parcours()
|
||||
)
|
||||
|
||||
def est_diplome(self) -> bool:
|
||||
"""Vrai si BUT déjà validé"""
|
||||
# vrai si la troisième année est validée
|
||||
# On cherche les validations de 3ieme annee (ordre=3) avec le même référentiel
|
||||
# de formation que nous.
|
||||
def est_annee_validee(self, ordre: int) -> bool:
|
||||
"""Vrai si l'année BUT ordre est validée"""
|
||||
# On cherche les validations d'annee avec le même
|
||||
# code formation que nous.
|
||||
return (
|
||||
ApcValidationAnnee.query.filter_by(etudid=self.etud.id, ordre=3)
|
||||
.join(FormSemestre, FormSemestre.id == ApcValidationAnnee.formsemestre_id)
|
||||
.join(Formation, FormSemestre.formation_id == Formation.id)
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=self.etud.id,
|
||||
ordre=ordre,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
)
|
||||
.join(Formation)
|
||||
.filter(
|
||||
Formation.referentiel_competence_id
|
||||
== self.formsemestre.formation.referentiel_competence_id
|
||||
Formation.formation_code == self.formsemestre.formation.formation_code
|
||||
)
|
||||
.count()
|
||||
> 0
|
||||
)
|
||||
|
||||
def est_diplome(self) -> bool:
|
||||
"""Vrai si BUT déjà validé"""
|
||||
# vrai si la troisième année est validée
|
||||
return self.est_annee_validee(3)
|
||||
|
||||
def competences_du_parcours(self) -> list[ApcCompetence]:
|
||||
"""Construit liste des compétences du parcours, qui doivent être
|
||||
validées pour obtenir le diplôme.
|
||||
|
|
|
@ -246,7 +246,7 @@ def _gen_but_rcue(dec_rcue: DecisionsProposeesRCUE, niveau: ApcNiveau) -> str:
|
|||
|
||||
scoplement = (
|
||||
f"""<div class="scoplement">{
|
||||
dec_rcue.validation.to_html()
|
||||
dec_rcue.validation.html()
|
||||
}</div>"""
|
||||
if dec_rcue.validation
|
||||
else ""
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
import flask
|
||||
from flask import flash, render_template, url_for
|
||||
from flask import g, request
|
||||
|
||||
from app import db
|
||||
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def jury_delete_manual(etud: Identite):
|
||||
"""Vue (réservée au chef de dept.)
|
||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant de les supprimer une à une.
|
||||
"""
|
||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, ue_id=None
|
||||
).order_by(ScolarFormSemestreValidation.event_date)
|
||||
ue_vals = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.order_by(ScolarFormSemestreValidation.event_date, UniteEns.numero)
|
||||
)
|
||||
autorisations = ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id
|
||||
).order_by(
|
||||
ScolarAutorisationInscription.semestre_id, ScolarAutorisationInscription.date
|
||||
)
|
||||
rcue_vals = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.order_by(UniteEns.semestre_idx, UniteEns.numero, ApcValidationRCUE.date)
|
||||
)
|
||||
annee_but_vals = ApcValidationAnnee.query.filter_by(etudid=etud.id).order_by(
|
||||
ApcValidationAnnee.ordre, ApcValidationAnnee.date
|
||||
)
|
||||
return render_template(
|
||||
"jury/jury_delete_manual.j2",
|
||||
etud=etud,
|
||||
sem_vals=sem_vals,
|
||||
ue_vals=ue_vals,
|
||||
autorisations=autorisations,
|
||||
rcue_vals=rcue_vals,
|
||||
annee_but_vals=annee_but_vals,
|
||||
sco=ScoData(),
|
||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||
)
|
|
@ -10,8 +10,17 @@ import pandas as pd
|
|||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||
from app.comp.res_cache import ResultatsCache
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
|
||||
|
@ -81,7 +90,7 @@ class ValidationsSemestre(ResultatsCache):
|
|||
|
||||
# UEs: { etudid : { ue_id : {"code":, "ects":, "event_date":} }}
|
||||
decisions_jury_ues = {}
|
||||
# Parcours les décisions d'UE:
|
||||
# Parcoure les décisions d'UE:
|
||||
for decision in (
|
||||
decisions_jury_q.filter(db.text("ue_id is not NULL"))
|
||||
.join(UniteEns)
|
||||
|
@ -172,3 +181,80 @@ def formsemestre_get_ue_capitalisees(formsemestre: FormSemestre) -> pd.DataFrame
|
|||
with db.engine.begin() as connection:
|
||||
df = pd.read_sql_query(query, connection, params=params, index_col="etudid")
|
||||
return df
|
||||
|
||||
|
||||
def erase_decisions_annee_formation(
|
||||
etud: Identite, formation: Formation, annee: int, delete=False
|
||||
) -> list:
|
||||
"""Efface toutes les décisions de jury de l'étudiant dans les formations de même code
|
||||
que celle donnée pour cette année de la formation:
|
||||
UEs, RCUEs de l'année BUT, année BUT, passage vers l'année suivante.
|
||||
Ne considère pas l'origine de la décision.
|
||||
annee: entier, 1, 2, 3, ...
|
||||
Si delete est faux, renvoie la liste des validations qu'il faudrait effacer, sans y toucher.
|
||||
"""
|
||||
sem1, sem2 = annee * 2 - 1, annee * 2
|
||||
# UEs
|
||||
validations = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.filter(db.or_(UniteEns.semestre_idx == sem1, UniteEns.semestre_idx == sem2))
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.order_by(
|
||||
UniteEns.acronyme, UniteEns.numero
|
||||
) # acronyme d'abord car 2 semestres
|
||||
.all()
|
||||
)
|
||||
# RCUEs (a priori inutile de matcher sur l'ue2_id)
|
||||
validations += (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.filter_by(semestre_idx=sem1)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.order_by(UniteEns.acronyme, UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
# Validation de semestres classiques
|
||||
validations += (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id, ue_id=None)
|
||||
.join(
|
||||
FormSemestre,
|
||||
FormSemestre.id == ScolarFormSemestreValidation.formsemestre_id,
|
||||
)
|
||||
.filter(
|
||||
db.or_(FormSemestre.semestre_id == sem1, FormSemestre.semestre_id == sem2)
|
||||
)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.all()
|
||||
)
|
||||
# Année BUT
|
||||
validations += (
|
||||
ApcValidationAnnee.query.filter_by(etudid=etud.id, ordre=annee)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=formation.formation_code)
|
||||
.all()
|
||||
)
|
||||
# Autorisations vers les semestres suivants ceux de l'année:
|
||||
validations += (
|
||||
ScolarAutorisationInscription.query.filter_by(
|
||||
etudid=etud.id, formation_code=formation.formation_code
|
||||
)
|
||||
.filter(
|
||||
db.or_(
|
||||
ScolarAutorisationInscription.semestre_id == sem1 + 1,
|
||||
ScolarAutorisationInscription.semestre_id == sem2 + 1,
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
if delete:
|
||||
for validation in validations:
|
||||
db.session.delete(validation)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
return []
|
||||
return validations
|
||||
|
|
|
@ -10,17 +10,17 @@ import time
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app import log
|
||||
from app import db, log
|
||||
from app.comp import moy_ue, moy_sem, inscr_mod
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.bonus_spo import BonusSport
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import Formation, FormSemestreInscription, ScoDocSiteConfig
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
from app.models.but_refcomp import ApcParcours, ApcNiveau
|
||||
from app.models.ues import DispenseUE, UniteEns
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
from app.scodoc.codes_cursus import BUT_CODES_ORDERED, UE_SPORT
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
|
||||
|
@ -44,7 +44,8 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
"""Parcours de chaque étudiant { etudid : parcour_id }"""
|
||||
self.ues_ids_by_parcour: dict[set[int]] = {}
|
||||
"""{ parcour_id : set }, ue_id de chaque parcours"""
|
||||
|
||||
self.validations_annee: dict[int, ApcValidationAnnee] = {}
|
||||
"""chargé par get_validations_annee: jury annuel BUT"""
|
||||
if not self.load_cached():
|
||||
t0 = time.time()
|
||||
self.compute()
|
||||
|
@ -307,9 +308,10 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
return ues_ids
|
||||
|
||||
def etud_has_decision(self, etudid) -> bool:
|
||||
"""True s'il y a une décision de jury pour cet étudiant émanant de ce formsemestre.
|
||||
"""True s'il y a une décision (quelconque) de jury
|
||||
émanant de ce formsemestre pour cet étudiant.
|
||||
prend aussi en compte les autorisations de passage.
|
||||
Sous-classée en BUT pour les RCUEs et années.
|
||||
Ici sous-classée (BUT) pour les RCUEs et années.
|
||||
"""
|
||||
return bool(
|
||||
super().etud_has_decision(etudid)
|
||||
|
@ -320,3 +322,42 @@ class ResultatsSemestreBUT(NotesTableCompat):
|
|||
formsemestre_id=self.formsemestre.id, etudid=etudid
|
||||
).count()
|
||||
)
|
||||
|
||||
def get_validations_annee(self) -> dict[int, ApcValidationAnnee]:
|
||||
"""Les validations des étudiants de ce semestre
|
||||
pour l'année BUT d'une formation compatible avec celle de ce semestre.
|
||||
Attention:
|
||||
1) la validation ne provient pas nécessairement de ce semestre
|
||||
(redoublants, pair/impair, extérieurs).
|
||||
2) l'étudiant a pu démissionner ou défaillir.
|
||||
3) S'il y a plusieurs validations pour le même étudiant, prend la "meilleure".
|
||||
|
||||
Mémorise le résultat (dans l'instance, pas en cache: TODO voir au profiler)
|
||||
"""
|
||||
if self.validations_annee:
|
||||
return self.validations_annee
|
||||
annee_but = (self.formsemestre.semestre_id + 1) // 2
|
||||
validations = (
|
||||
ApcValidationAnnee.query.filter_by(ordre=annee_but)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=self.formsemestre.formation.formation_code)
|
||||
.join(
|
||||
FormSemestreInscription,
|
||||
db.and_(
|
||||
FormSemestreInscription.etudid == ApcValidationAnnee.etudid,
|
||||
FormSemestreInscription.formsemestre_id == self.formsemestre.id,
|
||||
),
|
||||
)
|
||||
)
|
||||
validation_by_etud = {}
|
||||
for validation in validations:
|
||||
if validation.etudid in validation_by_etud:
|
||||
# keep the "best"
|
||||
if BUT_CODES_ORDERED.get(validation.code, 0) > BUT_CODES_ORDERED.get(
|
||||
validation_by_etud[validation.etudid].code, 0
|
||||
):
|
||||
validation_by_etud[validation.etudid] = validation
|
||||
else:
|
||||
validation_by_etud[validation.etudid] = validation
|
||||
self.validations_annee = validation_by_etud
|
||||
return self.validations_annee
|
||||
|
|
|
@ -66,7 +66,7 @@ class ApcValidationRCUE(db.Model):
|
|||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}: {
|
||||
self.code} enregistrée le {self.date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
def html(self) -> str:
|
||||
"description en HTML"
|
||||
return f"""Décision sur RCUE {self.ue1.acronyme}/{self.ue2.acronyme}:
|
||||
<b>{self.code}</b>
|
||||
|
@ -320,7 +320,12 @@ class ApcValidationAnnee(db.Model):
|
|||
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
|
||||
formation_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formations.id"),
|
||||
nullable=False,
|
||||
)
|
||||
annee_scolaire = db.Column(db.Integer, nullable=False) # eg 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)
|
||||
|
||||
|
@ -343,20 +348,31 @@ class ApcValidationAnnee(db.Model):
|
|||
"ordre": self.ordre,
|
||||
}
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
return f"""Validation <b>année BUT{self.ordre}</b> émise par
|
||||
{self.formsemestre.html_link_status() if self.formsemestre else "-"}
|
||||
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
|
||||
def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
"""
|
||||
Un dict avec les décisions de jury BUT enregistrées:
|
||||
- decision_rcue : list[dict]
|
||||
- decision_annee : dict
|
||||
- decision_annee : dict (décision issue de ce semestre seulement (à confirmer ?))
|
||||
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
|
||||
validations_rcues = (
|
||||
ApcValidationRCUE.query.filter_by(
|
||||
etudid=etud.id, formsemestre_id=formsemestre.id
|
||||
)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue1_id)
|
||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||
titres_rcues = []
|
||||
|
@ -383,8 +399,7 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
|||
etudid=etud.id,
|
||||
annee_scolaire=formsemestre.annee_scolaire(),
|
||||
)
|
||||
.join(ApcValidationAnnee.formsemestre)
|
||||
.join(FormSemestre.formation)
|
||||
.join(Formation)
|
||||
.filter(Formation.formation_code == formsemestre.formation.formation_code)
|
||||
.first()
|
||||
)
|
||||
|
|
|
@ -78,6 +78,12 @@ class Identite(db.Model):
|
|||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||
)
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||
"""Étudiant à partir de l'etudid ou du code_nip, soit
|
||||
|
|
|
@ -60,7 +60,7 @@ class Formation(db.Model):
|
|||
return f"""<{self.__class__.__name__}(id={self.id}, dept_id={
|
||||
self.dept_id}, acronyme={self.acronyme!r}, version={self.version})>"""
|
||||
|
||||
def to_html(self) -> str:
|
||||
def html(self) -> str:
|
||||
"titre complet pour affichage"
|
||||
return f"""Formation {self.titre} ({self.acronyme}) [version {self.version}] code {self.formation_code}"""
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from operator import attrgetter
|
|||
|
||||
from flask_login import current_user
|
||||
|
||||
from flask import flash, g
|
||||
from flask import flash, g, url_for
|
||||
from sqlalchemy.sql import text
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -163,6 +163,14 @@ class FormSemestre(db.Model):
|
|||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} {self.titre_annee()}>"
|
||||
|
||||
def html_link_status(self) -> str:
|
||||
"html link to status page"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.id,)
|
||||
}">{self.titre_mois()}</a>
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def get_formsemestre(cls, formsemestre_id: int) -> "FormSemestre":
|
||||
""" "FormSemestre ou 404, cherche uniquement dans le département courant"""
|
||||
|
@ -859,7 +867,7 @@ class FormSemestre(db.Model):
|
|||
.order_by(UniteEns.numero)
|
||||
.all()
|
||||
)
|
||||
vals_annee = (
|
||||
vals_annee = ( # issues de ce formsemestre seulement
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid,
|
||||
annee_scolaire=self.annee_scolaire(),
|
||||
|
|
|
@ -242,7 +242,7 @@ class Module(db.Model):
|
|||
"les coefs d'UE, trié par numéro et acronyme d'UE"
|
||||
# je n'ai pas su mettre un order_by sur le backref sans avoir
|
||||
# à redéfinir les relationships...
|
||||
return sorted(self.ue_coefs, key=attrgetter("numero", "acronyme"))
|
||||
return sorted(self.ue_coefs, key=lambda uc: (uc.ue.numero, uc.ue.acronyme))
|
||||
|
||||
def ue_coefs_list(
|
||||
self, include_zeros=True, ues: list["UniteEns"] = None
|
||||
|
|
|
@ -11,7 +11,7 @@ from app.models.events import Scolog
|
|||
|
||||
|
||||
class ScolarFormSemestreValidation(db.Model):
|
||||
"""Décisions de jury"""
|
||||
"""Décisions de jury (sur semestre ou UEs)"""
|
||||
|
||||
__tablename__ = "scolar_formsemestre_validation"
|
||||
# Assure unicité de la décision:
|
||||
|
@ -59,13 +59,16 @@ class ScolarFormSemestreValidation(db.Model):
|
|||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"
|
||||
return f"""{self.__class__.__name__}(sem={self.formsemestre_id}, etuid={
|
||||
self.etudid}, code={self.code}, ue={self.ue}, moy_ue={self.moy_ue})"""
|
||||
|
||||
def __str__(self):
|
||||
if self.ue_id:
|
||||
# Note: si l'objet vient d'être créé, ue_id peut exister mais pas ue !
|
||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id}: {self.code}"""
|
||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {self.event_date.strftime("%d/%m/%Y")}"""
|
||||
return f"""décision sur UE {self.ue.acronyme if self.ue else self.ue_id
|
||||
}: {self.code}"""
|
||||
return f"""décision sur semestre {self.formsemestre.titre_mois()} du {
|
||||
self.event_date.strftime("%d/%m/%Y")}"""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
|
@ -73,6 +76,28 @@ class ScolarFormSemestreValidation(db.Model):
|
|||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def html(self, detail=False) -> str:
|
||||
"Affichage html"
|
||||
if self.ue_id is not None:
|
||||
return f"""Validation de l'UE <b>{self.ue.acronyme}</b>
|
||||
{('parcours <span class="parcours">'
|
||||
+ ", ".join([p.code for p in self.ue.parcours]))
|
||||
+ "</span>"
|
||||
if self.ue.parcours else ""}
|
||||
de {self.ue.formation.acronyme}
|
||||
{("émise par " + self.formsemestre.html_link_status())
|
||||
if self.formsemestre else ""}
|
||||
: <b>{self.code}</b>
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
else:
|
||||
return f"""Validation du semestre S{
|
||||
self.formsemestre.semestre_id if self.formsemestre else "?"}
|
||||
{self.formsemestre.html_link_status() if self.formsemestre else ""}
|
||||
: <b>{self.code}</b>
|
||||
le {self.event_date.strftime("%d/%m/%Y")} à {self.event_date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
|
||||
class ScolarAutorisationInscription(db.Model):
|
||||
"""Autorisation d'inscription dans un semestre"""
|
||||
|
@ -93,6 +118,7 @@ class ScolarAutorisationInscription(db.Model):
|
|||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
origin_formsemestre = db.relationship("FormSemestre", lazy="select", uselist=False)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"""{self.__class__.__name__}(id={self.id}, etudid={
|
||||
|
@ -104,6 +130,15 @@ class ScolarAutorisationInscription(db.Model):
|
|||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
return f"""Autorisation de passage vers <b>S{self.semestre_id}</b> émise par
|
||||
{self.origin_formsemestre.html_link_status()
|
||||
if self.origin_formsemestre
|
||||
else "-"}
|
||||
le {self.date.strftime("%d/%m/%Y")} à {self.date.strftime("%Hh%M")}
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def autorise_etud(
|
||||
cls,
|
||||
|
|
|
@ -122,6 +122,7 @@ ABAN = "ABAN"
|
|||
ABL = "ABL"
|
||||
ADM = "ADM" # moyenne gen., barres UE, assiduité: sem. validé
|
||||
ADC = "ADC" # admis par compensation (eg moy(S1, S2) > 10)
|
||||
ADSUP = "ADSUP" # BUT: UE ou RCUE validé par niveau supérieur
|
||||
ADJ = "ADJ" # admis par le jury
|
||||
ADJR = "ADJR" # UE admise car son RCUE est ADJ
|
||||
ATT = "ATT" #
|
||||
|
@ -162,6 +163,7 @@ CODES_EXPL = {
|
|||
ADJ: "Validé par le Jury",
|
||||
ADJR: "UE validée car son RCUE est validé ADJ par le jury",
|
||||
ADM: "Validé",
|
||||
ADSUP: "UE ou RCUE validé car le niveau supérieur est validé",
|
||||
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)",
|
||||
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
||||
|
@ -195,17 +197,18 @@ CODES_SEM_ATTENTES = {ATT, ATB, ATJ} # semestre en attente
|
|||
CODES_SEM_REO = {NAR} # reorientation
|
||||
|
||||
CODES_UE_VALIDES_DE_DROIT = {ADM, CMP} # validation "de droit"
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR}
|
||||
CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
|
||||
"UE validée"
|
||||
CODES_UE_CAPITALISANTS = {ADM}
|
||||
"UE capitalisée"
|
||||
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
||||
"Niveau RCUE validé"
|
||||
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD}
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
|
||||
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
|
@ -229,6 +232,7 @@ BUT_CODES_ORDERED = {
|
|||
PASD: 50,
|
||||
PAS1NCI: 60,
|
||||
ADJR: 90,
|
||||
ADSUP: 90,
|
||||
ADJ: 100,
|
||||
ADM: 100,
|
||||
}
|
||||
|
@ -249,6 +253,16 @@ def code_ue_validant(code: str) -> bool:
|
|||
return code in CODES_UE_VALIDES
|
||||
|
||||
|
||||
def code_rcue_validant(code: str) -> bool:
|
||||
"Vrai si ce code d'RCUE est validant"
|
||||
return code in CODES_RCUE_VALIDES
|
||||
|
||||
|
||||
def code_annee_validant(code: str) -> bool:
|
||||
"Vrai si code d'année BUT validant"
|
||||
return code in CODES_ANNEE_BUT_VALIDES
|
||||
|
||||
|
||||
DEVENIR_EXPL = {
|
||||
NEXT: "Passage au semestre suivant",
|
||||
REDOANNEE: "Redoublement année",
|
||||
|
|
|
@ -473,7 +473,10 @@ class ApoEtud(dict):
|
|||
)
|
||||
|
||||
def _but_load_validation_annuelle(self):
|
||||
"charge la validation de jury BUT annuelle"
|
||||
"""charge la validation de jury BUT annuelle.
|
||||
Ici impose qu'elle soit issue d'un semestre de l'année en cours
|
||||
(pas forcément nécessaire, voir selon les retours des équipes ?)
|
||||
"""
|
||||
# le semestre impair de l'année scolaire
|
||||
if self.cur_res.formsemestre.semestre_id % 2:
|
||||
formsemestre = self.cur_res.formsemestre
|
||||
|
@ -490,7 +493,9 @@ class ApoEtud(dict):
|
|||
return
|
||||
self.validation_annee_but: ApcValidationAnnee = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id, etudid=self.etud["etudid"]
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=self.etud["etudid"],
|
||||
formation_id=self.cur_sem["formation_id"],
|
||||
).first()
|
||||
)
|
||||
self.is_nar = (
|
||||
|
|
|
@ -344,7 +344,7 @@ def do_formsemestre_archive(
|
|||
if data:
|
||||
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data)
|
||||
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
|
||||
table_html, _ = gen_formsemestre_recapcomplet_html_table(
|
||||
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
|
||||
formsemestre, res, include_evaluations=True
|
||||
)
|
||||
if table_html:
|
||||
|
|
|
@ -315,6 +315,19 @@ def invalidate_formsemestre( # was inval_cache(formsemestre_id=None, pdfonly=Fa
|
|||
SemBulletinsPDFCache.invalidate_sems(formsemestre_ids)
|
||||
|
||||
|
||||
def invalidate_formsemestre_etud(etud: "Identite"):
|
||||
"""Invalide tous les formsemestres auxquels l'étudiant est inscrit"""
|
||||
from app.models import FormSemestre, FormSemestreInscription
|
||||
|
||||
inscriptions = (
|
||||
FormSemestreInscription.query.filter_by(etudid=etud.id)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
for inscription in inscriptions:
|
||||
invalidate_formsemestre(inscription.formsemestre_id)
|
||||
|
||||
|
||||
class DeferredSemCacheManager:
|
||||
"""Contexte pour effectuer des opérations indépendantes dans la
|
||||
même requete qui invalident le cache. Par exemple, quand on inscrit
|
||||
|
|
|
@ -757,7 +757,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||
],
|
||||
page_title=f"Programme {formation.acronyme} v{formation.version}",
|
||||
),
|
||||
f"""<h2>{formation.to_html()} {lockicon}
|
||||
f"""<h2>{formation.html()} {lockicon}
|
||||
</h2>
|
||||
""",
|
||||
]
|
||||
|
@ -1010,12 +1010,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
|||
<p><ul>"""
|
||||
)
|
||||
for formsemestre in formsemestres:
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id
|
||||
)}">{formsemestre.titre_mois()}</a>"""
|
||||
)
|
||||
H.append(f"""<li>{formsemestre.html_link_status()}""")
|
||||
if not formsemestre.etat:
|
||||
H.append(" [verrouillé]")
|
||||
else:
|
||||
|
|
|
@ -66,6 +66,7 @@ from app.scodoc import sco_photos
|
|||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_pv_dict
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------
|
||||
def formsemestre_validation_etud_form(
|
||||
formsemestre_id=None, # required
|
||||
|
@ -1063,8 +1064,6 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
|
|||
"""Form. saisie UE validée hors ScoDoc
|
||||
(pour étudiants arrivant avec un UE antérieurement validée).
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
formation: Formation = Formation.query.get_or_404(sem["formation_id"])
|
||||
|
@ -1087,8 +1086,8 @@ def formsemestre_validate_previous_ue(formsemestre_id, etudid):
|
|||
<em>dans un semestre hors ScoDoc</em>.</p>
|
||||
<p><b>Les UE validées dans ScoDoc sont déjà
|
||||
automatiquement prises en compte</b>. Cette page n'est utile que pour les étudiants ayant
|
||||
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré <b>sans
|
||||
ScoDoc</b> et qui <b>redouble</b> ce semestre
|
||||
suivi un début de cursus dans <b>un autre établissement</b>, ou bien dans un semestre géré
|
||||
<b>sans ScoDoc</b> et qui <b>redouble</b> ce semestre
|
||||
(<em>ne pas utiliser pour les semestres précédents !</em>).
|
||||
</p>
|
||||
<p>Notez que l'UE est validée, avec enregistrement immédiat de la décision et
|
||||
|
|
|
@ -312,7 +312,13 @@ def ficheEtud(etudid=None):
|
|||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_inscription_with_modules_form",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">inscrire à un autre semestre</a></span>"""
|
||||
}">inscrire à un autre semestre</a></span>
|
||||
<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.jury_delete_manual",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">éditer toutes décisions de jury</a></span>
|
||||
"""
|
||||
|
||||
else:
|
||||
info["link_inscrire_ailleurs"] = ""
|
||||
else:
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
"""Tableau récapitulatif des notes d'un semestre
|
||||
"""
|
||||
import collections
|
||||
import datetime
|
||||
import time
|
||||
from xml.etree import ElementTree
|
||||
|
@ -109,7 +110,7 @@ def formsemestre_recapcomplet(
|
|||
force_publishing=force_publishing,
|
||||
)
|
||||
|
||||
table_html, table = _formsemestre_recapcomplet_to_html(
|
||||
table_html, table, freq_codes_annuels = _formsemestre_recapcomplet_to_html(
|
||||
formsemestre,
|
||||
filename=filename,
|
||||
mode_jury=mode_jury,
|
||||
|
@ -142,7 +143,7 @@ def formsemestre_recapcomplet(
|
|||
H.append(
|
||||
'<select name="tabformat" onchange="document.f.submit()" class="noprint">'
|
||||
)
|
||||
for (fmt, label) in (
|
||||
for fmt, label in (
|
||||
("html", "Tableau"),
|
||||
("evals", "Avec toutes les évaluations"),
|
||||
("xlsx", "Excel (non formaté)"),
|
||||
|
@ -186,7 +187,7 @@ def formsemestre_recapcomplet(
|
|||
</li>
|
||||
<li><a class="stdlink" href="{url_for('notes.formsemestre_jury_but_erase',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id, only_one_sem=1)
|
||||
}">Effacer <em>toutes</em> les décisions de jury (BUT) du semestre</a>
|
||||
}">Effacer <em>toutes</em> les décisions de jury BUT issues de ce semestre</a>
|
||||
</li>
|
||||
"""
|
||||
)
|
||||
|
@ -215,33 +216,37 @@ def formsemestre_recapcomplet(
|
|||
"""
|
||||
)
|
||||
|
||||
if mode_jury and table and sum(table.freq_codes_annuels.values()) > 0:
|
||||
if mode_jury and freq_codes_annuels and sum(freq_codes_annuels.values()) > 0:
|
||||
nb_etud_avec_decision_annuelle = (
|
||||
sum(freq_codes_annuels.values()) - freq_codes_annuels["total"]
|
||||
)
|
||||
H.append(
|
||||
f"""
|
||||
<div class="jury_stats">
|
||||
<div>Nb d'étudiants avec décision annuelle:
|
||||
{sum(table.freq_codes_annuels.values())} / {len(table)}
|
||||
<div><b>Nb d'étudiants avec décision annuelle:</b>
|
||||
{nb_etud_avec_decision_annuelle} / {freq_codes_annuels["total"]}
|
||||
</div>
|
||||
<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
for code in sorted(table.freq_codes_annuels.keys()):
|
||||
if nb_etud_avec_decision_annuelle > 0:
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{table.freq_codes_annuels[code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*table.freq_codes_annuels[code] / len(table)):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
"""<div><b>Codes annuels octroyés:</b></div>
|
||||
<table class="jury_stats_codes">
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
"""
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
for code in sorted(freq_codes_annuels.keys()):
|
||||
if code != "total":
|
||||
H.append(
|
||||
f"""<tr>
|
||||
<td>{code}</td>
|
||||
<td style="text-align:right">{freq_codes_annuels[code]}</td>
|
||||
<td style="text-align:right">{
|
||||
(100*freq_codes_annuels[code] / freq_codes_annuels["total"]):2.1f}%
|
||||
</td>
|
||||
</tr>"""
|
||||
)
|
||||
H.append("""</table>""")
|
||||
H.append("""</div>""")
|
||||
# Légende
|
||||
H.append(
|
||||
"""
|
||||
|
@ -251,6 +256,7 @@ def formsemestre_recapcomplet(
|
|||
<div><tt>~</tt></div><div>valeur manquante</div>
|
||||
<div><tt>=</tt></div><div>UE dispensée</div>
|
||||
<div><tt>nan</tt></div><div>valeur non disponible</div>
|
||||
<div>📍</div><div>code jury non enregistré</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
@ -271,12 +277,12 @@ def _formsemestre_recapcomplet_to_html(
|
|||
filename: str = "",
|
||||
mode_jury=False, # saisie décisions jury
|
||||
selected_etudid=None,
|
||||
) -> tuple[str, TableRecap]:
|
||||
) -> tuple[str, TableRecap, collections.Counter]:
|
||||
"""Le tableau recap en html"""
|
||||
if tabformat not in ("html", "evals"):
|
||||
raise ScoValueError("invalid table format")
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
table_html, table = gen_formsemestre_recapcomplet_html_table(
|
||||
table_html, table, freq_codes_annuels = gen_formsemestre_recapcomplet_html_table(
|
||||
formsemestre,
|
||||
res,
|
||||
include_evaluations=(tabformat == "evals"),
|
||||
|
@ -284,7 +290,7 @@ def _formsemestre_recapcomplet_to_html(
|
|||
filename=filename,
|
||||
selected_etudid=selected_etudid,
|
||||
)
|
||||
return table_html, table
|
||||
return table_html, table, freq_codes_annuels
|
||||
|
||||
|
||||
def _formsemestre_recapcomplet_to_file(
|
||||
|
@ -446,9 +452,9 @@ def gen_formsemestre_recapcomplet_html_table(
|
|||
mode_jury=False,
|
||||
filename="",
|
||||
selected_etudid=None,
|
||||
) -> tuple[str, TableRecap]:
|
||||
) -> tuple[str, TableRecap, collections.Counter]:
|
||||
"""Construit table recap pour le BUT
|
||||
Cache le résultat pour le semestre (sauf en mode jury).
|
||||
Cache le résultat pour le semestre.
|
||||
Note: on cache le HTML et non l'objet Table.
|
||||
|
||||
Si mode_jury, occultera colonnes modules (en js)
|
||||
|
@ -460,6 +466,7 @@ def gen_formsemestre_recapcomplet_html_table(
|
|||
"""
|
||||
table = None
|
||||
table_html = None
|
||||
table_html_cached = None
|
||||
cache_class = {
|
||||
(True, True): sco_cache.TableJuryWithEvalsCache,
|
||||
(True, False): sco_cache.TableJuryCache,
|
||||
|
@ -467,8 +474,8 @@ def gen_formsemestre_recapcomplet_html_table(
|
|||
(False, False): sco_cache.TableRecapCache,
|
||||
}[(bool(mode_jury), bool(include_evaluations))]
|
||||
if not selected_etudid:
|
||||
table_html = cache_class.get(formsemestre.id)
|
||||
if table_html is None:
|
||||
table_html_cached = cache_class.get(formsemestre.id)
|
||||
if table_html_cached is None:
|
||||
table = _gen_formsemestre_recapcomplet_table(
|
||||
res,
|
||||
include_evaluations,
|
||||
|
@ -477,9 +484,14 @@ def gen_formsemestre_recapcomplet_html_table(
|
|||
selected_etudid=selected_etudid,
|
||||
)
|
||||
table_html = table.html()
|
||||
cache_class.set(formsemestre.id, table_html)
|
||||
freq_codes_annuels = (
|
||||
table.freq_codes_annuels if hasattr(table, "freq_codes_annuels") else None
|
||||
)
|
||||
cache_class.set(formsemestre.id, (table_html, freq_codes_annuels))
|
||||
else:
|
||||
table_html, freq_codes_annuels = table_html_cached
|
||||
|
||||
return table_html, table
|
||||
return table_html, table, freq_codes_annuels
|
||||
|
||||
|
||||
def _gen_formsemestre_recapcomplet_table(
|
||||
|
|
|
@ -145,12 +145,7 @@ class SemSet(dict):
|
|||
|
||||
# Construction du ou des lien(s) vers le semestre
|
||||
self["semlinks"] = [
|
||||
f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)
|
||||
}">{formsemestre.titre_annee()}</a>
|
||||
"""
|
||||
for formsemestre in self.formsemestres
|
||||
formsemestre.html_link_status() for formsemestre in self.formsemestres
|
||||
]
|
||||
|
||||
self["semtitles_str"] = "<br>".join(self["semlinks"])
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
padding-bottom: 0px;
|
||||
padding-left: 16px;
|
||||
padding-right: 0px;
|
||||
|
||||
background: #FFF;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 8px;
|
||||
|
@ -39,4 +38,10 @@ div.code_rcue {
|
|||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.code_jury {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
width: 64px;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
|
||||
div.jury_decisions_list div {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.jury_decisions_list form {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
span.parcours {
|
||||
color:blueviolet;
|
||||
}
|
|
@ -2773,6 +2773,8 @@ table.notes_recapcomplet a:hover {
|
|||
|
||||
div.table_recap_caption {
|
||||
width: fit-content;
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
background-color: rgb(202, 255, 180);
|
||||
|
|
|
@ -1,285 +1,311 @@
|
|||
// Tableau recap notes
|
||||
$(function () {
|
||||
$(function () {
|
||||
if ($('table.table_recap').length == 0) { return; }
|
||||
$(function () {
|
||||
if ($("table.table_recap").length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hidden_colums = [
|
||||
"etud_codes", "identite_detail",
|
||||
"partition_aux", "partition_rangs", "admission",
|
||||
"col_empty"
|
||||
];
|
||||
// Etat (tri des colonnes) de la table:
|
||||
let hidden_colums = [
|
||||
"etud_codes",
|
||||
"identite_detail",
|
||||
"partition_aux",
|
||||
"partition_rangs",
|
||||
"admission",
|
||||
"col_empty",
|
||||
];
|
||||
// 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]);
|
||||
const etudids_key = JSON.stringify(["etudids", url.origin, formsemestre_id]);
|
||||
const noms_key = JSON.stringify(["noms", url.origin, 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 colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
|
||||
function update_buttons_labels(dt) {
|
||||
// chaque bouton controle une classe stockée dans le data-group du span
|
||||
document.querySelectorAll("button.dt-button").forEach(but => {
|
||||
let g_span = but.querySelector("span > span");
|
||||
if (g_span) {
|
||||
let group = g_span.dataset["group"];
|
||||
if (group) {
|
||||
// si le group (= la 1ere col.) est visible, but_on
|
||||
if (dt.columns("." + group).visible()[0]) {
|
||||
but.classList.add("but_on");
|
||||
but.classList.remove("but_off");
|
||||
} else {
|
||||
but.classList.add("but_off");
|
||||
but.classList.remove("but_on");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Changement visibilité groupes colonnes (boutons)
|
||||
function toggle_col_but_visibility(e, dt, node, config) {
|
||||
let group = node.children()[0].firstChild.dataset.group;
|
||||
toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
|
||||
}
|
||||
function toggle_col_ident_visibility(e, dt, node, config) {
|
||||
let onoff = node.hasClass("but_on");
|
||||
toggle_col_group_visibility(dt, "identite_detail", onoff);
|
||||
toggle_col_group_visibility(dt, "identite_court", !onoff);
|
||||
}
|
||||
function toggle_col_ressources_visibility(e, dt, node, config) {
|
||||
let onoff = node.hasClass("but_on");
|
||||
toggle_col_group_visibility(dt, "col_res", onoff);
|
||||
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
|
||||
toggle_col_group_visibility(dt, "col_malus", onoff);
|
||||
}
|
||||
function toggle_col_group_visibility(dt, group, onoff) {
|
||||
if (onoff) {
|
||||
dt.columns('.' + group).visible(false);
|
||||
} else {
|
||||
dt.columns('.' + group).visible(true);
|
||||
}
|
||||
update_buttons_labels(dt);
|
||||
}
|
||||
// Definition des boutons au dessus de la table:
|
||||
let buttons = [
|
||||
{
|
||||
extend: 'copyHtml5',
|
||||
text: 'Copier',
|
||||
exportOptions: { orthogonal: 'export' }
|
||||
},
|
||||
{
|
||||
extend: 'excelHtml5',
|
||||
// footer: true, // ne fonctionne pas ?
|
||||
exportOptions: { orthogonal: 'export' },
|
||||
title: document.querySelector('table.table_recap').dataset.filename
|
||||
},
|
||||
{
|
||||
// force affichage de toutes les colonnes
|
||||
text: '<a title="Afficher toutes les colonnes">✴</a>',
|
||||
action: function (e, dt, node, config) {
|
||||
dt.columns().visible(true);
|
||||
update_buttons_labels(dt);
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '<a title="Rétablir l\'affichage par défaut" class="clearreload">🔄</a>',
|
||||
action: function (e, dt, node, config) {
|
||||
localStorage.clear();
|
||||
console.log("cleared localStorage");
|
||||
location.reload();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '<span data-group="identite_detail">Civilité</span>',
|
||||
action: toggle_col_ident_visibility,
|
||||
},
|
||||
{
|
||||
text: '<span data-group="partition_aux"><a title="Affichage des groupes secondaires (la première partition est toujours affichée)">Groupes</a></span>',
|
||||
action: toggle_col_but_visibility,
|
||||
},
|
||||
{
|
||||
text: '<span data-group="partition_rangs"><a title="Rangs dans les groupes (si activés dans les partitions concernées)">Rg</a></span>',
|
||||
action: toggle_col_but_visibility,
|
||||
},
|
||||
]; // fin des boutons communs à toutes les tables recap
|
||||
|
||||
if ($('table.table_recap').hasClass("jury")) {
|
||||
// Table JURY:
|
||||
// avec ou sans codes enregistrés
|
||||
buttons.push(
|
||||
{
|
||||
text: '<span data-group="recorded_code">Codes jury</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
if ($('table.table_recap').hasClass("apc")) {
|
||||
// Boutons spécifiques à la table JURY BUT
|
||||
buttons.push(
|
||||
{
|
||||
text: '<span data-group="cursus_but">Compétences</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
buttons.push(
|
||||
{
|
||||
text: '<span data-group="col_rcue">RCUEs</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// BOUTONS SPECIFIQUES A LA TABLE RECAP NON JURY
|
||||
buttons.push(
|
||||
$('table.table_recap').hasClass("apc") ?
|
||||
{
|
||||
text: '<span data-group="col_res">Ressources</span>',
|
||||
action: toggle_col_ressources_visibility,
|
||||
} : {
|
||||
name: "toggle_mod",
|
||||
text: "Cacher les modules",
|
||||
action: function (e, dt, node, config) {
|
||||
let onoff = node.hasClass("but_on");
|
||||
toggle_col_group_visibility(dt, "col_mod:not(.col_empty)", onoff);
|
||||
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
|
||||
toggle_col_group_visibility(dt, "col_malus", onoff);
|
||||
}
|
||||
}
|
||||
);
|
||||
if ($('table.table_recap').hasClass("apc")) {
|
||||
buttons.push({
|
||||
text: '<span data-group="col_sae">SAÉs</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
// S'il y a des colonnes vides:
|
||||
if ($('table.table_recap td.col_empty').length > 0) {
|
||||
buttons.push({ // modules vides
|
||||
text: '<span data-group="col_empty">Vides</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
// Boutons admission (pas en jury)
|
||||
if (!$('table.table_recap').hasClass("jury")) {
|
||||
buttons.push(
|
||||
{
|
||||
text: '<span data-group="admission">Admission</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// Boutons évaluations (si présentes)
|
||||
if ($('table.table_recap').hasClass("with_evaluations")) {
|
||||
buttons.push(
|
||||
{
|
||||
text: '<span data-group="eval">Évaluations</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ------------- 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]);
|
||||
const etudids_key = JSON.stringify([
|
||||
"etudids",
|
||||
url.origin,
|
||||
formsemestre_id,
|
||||
]);
|
||||
const noms_key = JSON.stringify(["noms", url.origin, formsemestre_id]);
|
||||
let order_info;
|
||||
if (formsemestre_id) {
|
||||
const x = localStorage.getItem(order_info_key);
|
||||
if (x) {
|
||||
try {
|
||||
let table = $('table.table_recap').DataTable(
|
||||
{
|
||||
paging: false,
|
||||
searching: true,
|
||||
info: false,
|
||||
autoWidth: false,
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
footer: false
|
||||
},
|
||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||
aaSorting: [], // Prevent initial sorting
|
||||
colReorder: true,
|
||||
stateSave: true, // enregistre état de la table (tris, ...)
|
||||
"columnDefs": [
|
||||
{
|
||||
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
|
||||
targets: hidden_colums,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
// Elimine les 0 à gauche pour les exports excel et les "copy"
|
||||
targets: ["col_mod", "col_moy_gen", "col_moy_ue", "col_res", "col_sae", "evaluation", "col_rcue"],
|
||||
render: function (data, type, row) {
|
||||
return type === 'export' ? data.replace(/0(\d\..*)/, '$1') : data;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Elimine les "+"" pour les exports
|
||||
targets: ["col_ue_bonus", "col_malus"],
|
||||
render: function (data, type, row) {
|
||||
return type === 'export' ? data.replace(/.*\+(\d?\d?\.\d\d).*/m, '$1').replace(/0(\d\..*)/, '$1') : data;
|
||||
}
|
||||
},
|
||||
{
|
||||
// Elimine emoji warning sur UEs
|
||||
targets: ["col_ues_validables"],
|
||||
render: function (data, type, row) {
|
||||
return type === 'export' ? data.replace(/(\d+\/\d+).*/, '$1') : data;
|
||||
}
|
||||
}
|
||||
|
||||
],
|
||||
dom: 'Bfrtip',
|
||||
buttons: buttons,
|
||||
"drawCallback": function (settings) {
|
||||
// permet de conserver l'ordre de tri des colonnes
|
||||
let table = $('table.table_recap').DataTable();
|
||||
let order_info = JSON.stringify(table.order());
|
||||
if (formsemestre_id) {
|
||||
localStorage.setItem(order_info_key, order_info);
|
||||
}
|
||||
let etudids = [];
|
||||
document.querySelectorAll("td.identite_court").forEach(e => {
|
||||
etudids.push(e.dataset.etudid);
|
||||
});
|
||||
let noms = [];
|
||||
document.querySelectorAll("td.identite_court").forEach(e => {
|
||||
noms.push(e.dataset.nomprenom);
|
||||
});
|
||||
localStorage.setItem(etudids_key, JSON.stringify(etudids));
|
||||
localStorage.setItem(noms_key, JSON.stringify(noms));
|
||||
},
|
||||
"order": order_info,
|
||||
}
|
||||
);
|
||||
update_buttons_labels(table);
|
||||
order_info = JSON.parse(x);
|
||||
} catch (error) {
|
||||
// l'erreur peut etre causee par un ancien storage:
|
||||
localStorage.removeItem(etudids_key);
|
||||
localStorage.removeItem(noms_key);
|
||||
localStorage.removeItem(order_info_key);
|
||||
location.reload();
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
$('table.table_recap tbody').on('click', 'tr', function () {
|
||||
if ($(this).hasClass('selected')) {
|
||||
$(this).removeClass('selected');
|
||||
}
|
||||
}
|
||||
|
||||
// Les colonnes visibles sont mémorisées, il faut initialiser l'état des boutons
|
||||
function update_buttons_labels(dt) {
|
||||
// chaque bouton controle une classe stockée dans le data-group du span
|
||||
document.querySelectorAll("button.dt-button").forEach((but) => {
|
||||
let g_span = but.querySelector("span > span");
|
||||
if (g_span) {
|
||||
let group = g_span.dataset["group"];
|
||||
if (group) {
|
||||
// si le group (= la 1ere col.) est visible, but_on
|
||||
if (dt.columns("." + group).visible()[0]) {
|
||||
but.classList.add("but_on");
|
||||
but.classList.remove("but_off");
|
||||
} else {
|
||||
but.classList.add("but_off");
|
||||
but.classList.remove("but_on");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$('table.table_recap tr.selected').removeClass('selected');
|
||||
$(this).addClass('selected');
|
||||
}
|
||||
});
|
||||
// Pour montrer et surligner l'étudiant sélectionné:
|
||||
$(function () {
|
||||
let row_selected = document.querySelector(".row_selected");
|
||||
if (row_selected) {
|
||||
row_selected.scrollIntoView();
|
||||
window.scrollBy(0, -125);
|
||||
row_selected.classList.add("selected");
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Changement visibilité groupes colonnes (boutons)
|
||||
function toggle_col_but_visibility(e, dt, node, config) {
|
||||
let group = node.children()[0].firstChild.dataset.group;
|
||||
toggle_col_group_visibility(dt, group, node.hasClass("but_on"));
|
||||
}
|
||||
function toggle_col_ident_visibility(e, dt, node, config) {
|
||||
let onoff = node.hasClass("but_on");
|
||||
toggle_col_group_visibility(dt, "identite_detail", onoff);
|
||||
toggle_col_group_visibility(dt, "identite_court", !onoff);
|
||||
}
|
||||
function toggle_col_ressources_visibility(e, dt, node, config) {
|
||||
let onoff = node.hasClass("but_on");
|
||||
toggle_col_group_visibility(dt, "col_res", onoff);
|
||||
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
|
||||
toggle_col_group_visibility(dt, "col_malus", onoff);
|
||||
}
|
||||
function toggle_col_group_visibility(dt, group, onoff) {
|
||||
if (onoff) {
|
||||
dt.columns("." + group).visible(false);
|
||||
} else {
|
||||
dt.columns("." + group).visible(true);
|
||||
}
|
||||
update_buttons_labels(dt);
|
||||
}
|
||||
// Definition des boutons au dessus de la table:
|
||||
let buttons = [
|
||||
{
|
||||
extend: "copyHtml5",
|
||||
text: "Copier",
|
||||
exportOptions: { orthogonal: "export" },
|
||||
},
|
||||
{
|
||||
extend: "excelHtml5",
|
||||
// footer: true, // ne fonctionne pas ?
|
||||
exportOptions: { orthogonal: "export" },
|
||||
title: document.querySelector("table.table_recap").dataset.filename,
|
||||
},
|
||||
{
|
||||
// force affichage de toutes les colonnes
|
||||
text: '<a title="Afficher toutes les colonnes">✴</a>',
|
||||
action: function (e, dt, node, config) {
|
||||
dt.columns().visible(true);
|
||||
update_buttons_labels(dt);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '<a title="Rétablir l\'affichage par défaut" class="clearreload">🔄</a>',
|
||||
action: function (e, dt, node, config) {
|
||||
localStorage.clear();
|
||||
console.log("cleared localStorage");
|
||||
location.reload();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: '<span data-group="identite_detail">Civilité</span>',
|
||||
action: toggle_col_ident_visibility,
|
||||
},
|
||||
{
|
||||
text: '<span data-group="partition_aux"><a title="Affichage des groupes secondaires (la première partition est toujours affichée)">Groupes</a></span>',
|
||||
action: toggle_col_but_visibility,
|
||||
},
|
||||
{
|
||||
text: '<span data-group="partition_rangs"><a title="Rangs dans les groupes (si activés dans les partitions concernées)">Rg</a></span>',
|
||||
action: toggle_col_but_visibility,
|
||||
},
|
||||
]; // fin des boutons communs à toutes les tables recap
|
||||
|
||||
if ($("table.table_recap").hasClass("jury")) {
|
||||
// Table JURY:
|
||||
// avec ou sans codes enregistrés
|
||||
buttons.push({
|
||||
text: '<span data-group="recorded_code">Codes jury</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
if ($("table.table_recap").hasClass("apc")) {
|
||||
// Boutons spécifiques à la table JURY BUT
|
||||
buttons.push({
|
||||
text: '<span data-group="cursus_but">Compétences</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
buttons.push({
|
||||
text: '<span data-group="col_rcue">RCUEs</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// BOUTONS SPECIFIQUES A LA TABLE RECAP NON JURY
|
||||
buttons.push(
|
||||
$("table.table_recap").hasClass("apc")
|
||||
? {
|
||||
text: '<span data-group="col_res">Ressources</span>',
|
||||
action: toggle_col_ressources_visibility,
|
||||
}
|
||||
: {
|
||||
name: "toggle_mod",
|
||||
text: "Cacher les modules",
|
||||
action: function (e, dt, node, config) {
|
||||
let onoff = node.hasClass("but_on");
|
||||
toggle_col_group_visibility(
|
||||
dt,
|
||||
"col_mod:not(.col_empty)",
|
||||
onoff
|
||||
);
|
||||
toggle_col_group_visibility(dt, "col_ue_bonus", onoff);
|
||||
toggle_col_group_visibility(dt, "col_malus", onoff);
|
||||
},
|
||||
}
|
||||
);
|
||||
if ($("table.table_recap").hasClass("apc")) {
|
||||
buttons.push({
|
||||
text: '<span data-group="col_sae">SAÉs</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
// S'il y a des colonnes vides:
|
||||
if ($("table.table_recap td.col_empty").length > 0) {
|
||||
buttons.push({
|
||||
// modules vides
|
||||
text: '<span data-group="col_empty">Vides</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
// Boutons admission (pas en jury)
|
||||
if (!$("table.table_recap").hasClass("jury")) {
|
||||
buttons.push({
|
||||
text: '<span data-group="admission">Admission</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Boutons évaluations (si présentes)
|
||||
if ($("table.table_recap").hasClass("with_evaluations")) {
|
||||
buttons.push({
|
||||
text: '<span data-group="eval">Évaluations</span>',
|
||||
action: toggle_col_but_visibility,
|
||||
});
|
||||
}
|
||||
|
||||
// ------------- LA TABLE ---------
|
||||
try {
|
||||
let table = $("table.table_recap").DataTable({
|
||||
paging: false,
|
||||
searching: true,
|
||||
info: false,
|
||||
autoWidth: false,
|
||||
fixedHeader: {
|
||||
header: true,
|
||||
footer: false,
|
||||
},
|
||||
orderCellsTop: true, // cellules ligne 1 pour tri
|
||||
aaSorting: [], // Prevent initial sorting
|
||||
colReorder: true,
|
||||
stateSave: true, // enregistre état de la table (tris, ...)
|
||||
columnDefs: [
|
||||
{
|
||||
// cache les codes, le détail de l'identité, les groupes, les colonnes admission et les vides
|
||||
targets: hidden_colums,
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
// Elimine les 0 à gauche pour les exports excel et les "copy"
|
||||
targets: [
|
||||
"col_mod",
|
||||
"col_moy_gen",
|
||||
"col_moy_ue",
|
||||
"col_res",
|
||||
"col_sae",
|
||||
"evaluation",
|
||||
"col_rcue",
|
||||
],
|
||||
render: function (data, type, row) {
|
||||
return type === "export" ? data.replace(/0(\d\..*)/, "$1") : data;
|
||||
},
|
||||
},
|
||||
{
|
||||
// Elimine les "+"" pour les exports
|
||||
targets: ["col_ue_bonus", "col_malus"],
|
||||
render: function (data, type, row) {
|
||||
return type === "export"
|
||||
? data
|
||||
.replace(/.*\+(\d?\d?\.\d\d).*/m, "$1")
|
||||
.replace(/0(\d\..*)/, "$1")
|
||||
: data;
|
||||
},
|
||||
},
|
||||
{
|
||||
// Elimine emoji warning sur UEs
|
||||
targets: ["col_ues_validables"],
|
||||
render: function (data, type, row) {
|
||||
return type === "export"
|
||||
? data.replace(/(\d+\/\d+).*/, "$1")
|
||||
: data;
|
||||
},
|
||||
},
|
||||
],
|
||||
dom: "Bfrtip",
|
||||
buttons: buttons,
|
||||
drawCallback: function (settings) {
|
||||
// permet de conserver l'ordre de tri des colonnes
|
||||
let table = $("table.table_recap").DataTable();
|
||||
let order_info = JSON.stringify(table.order());
|
||||
if (formsemestre_id) {
|
||||
localStorage.setItem(order_info_key, order_info);
|
||||
}
|
||||
let etudids = [];
|
||||
document.querySelectorAll("td.identite_court").forEach((e) => {
|
||||
etudids.push(e.dataset.etudid);
|
||||
});
|
||||
let noms = [];
|
||||
document.querySelectorAll("td.identite_court").forEach((e) => {
|
||||
noms.push(e.dataset.nomprenom);
|
||||
});
|
||||
localStorage.setItem(etudids_key, JSON.stringify(etudids));
|
||||
localStorage.setItem(noms_key, JSON.stringify(noms));
|
||||
},
|
||||
order: order_info,
|
||||
});
|
||||
update_buttons_labels(table);
|
||||
} catch (error) {
|
||||
// l'erreur peut etre causee par un ancien storage:
|
||||
localStorage.removeItem(etudids_key);
|
||||
localStorage.removeItem(noms_key);
|
||||
localStorage.removeItem(order_info_key);
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
$("table.table_recap tbody").on("click", "tr", function () {
|
||||
if ($(this).hasClass("selected")) {
|
||||
$(this).removeClass("selected");
|
||||
} else {
|
||||
$("table.table_recap tr.selected").removeClass("selected");
|
||||
$(this).addClass("selected");
|
||||
}
|
||||
});
|
||||
// Pour montrer et surligner l'étudiant sélectionné:
|
||||
$(function () {
|
||||
let row_selected = document.querySelector(".row_selected");
|
||||
if (row_selected) {
|
||||
row_selected.scrollIntoView();
|
||||
window.scrollBy(0, -125);
|
||||
row_selected.classList.add("selected");
|
||||
}
|
||||
});
|
||||
// Ajoute bulle aide sur colonne RCUEs
|
||||
$(function () {
|
||||
// explication colonne RCUEs
|
||||
let th = document.querySelector(
|
||||
"table.table_recap.apc th.col_rcues_validables"
|
||||
);
|
||||
th.title = "RCUEs validables avec ces notes";
|
||||
});
|
||||
});
|
||||
|
|
|
@ -78,14 +78,18 @@ class TableJury(TableRecap):
|
|||
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_rcue_cols(dec_rcue)
|
||||
self.freq_codes_annuels["total"] = len(self.rows)
|
||||
|
||||
def add_jury(self):
|
||||
"""Ajoute la colonne code jury et le lien.
|
||||
Le code jury est celui du semestre: cette colonne n'est montrée
|
||||
- Le code jury est celui du semestre: cette colonne n'est montrée
|
||||
que pour les formations classiques, ce code n'est pas utilisé en BUT.
|
||||
- En BUT, on donne la décision de jury annuelle.
|
||||
"""
|
||||
res = self.res
|
||||
autorisations = res.get_autorisations_inscription()
|
||||
if res.is_apc:
|
||||
validations_annee = res.get_validations_annee()
|
||||
for row in self.rows:
|
||||
etud = row.etud
|
||||
if not res.is_apc:
|
||||
|
@ -115,6 +119,15 @@ class TableJury(TableRecap):
|
|||
group="jury_code_sem",
|
||||
classes=["recorded_code"],
|
||||
)
|
||||
if res.is_apc: # BUT
|
||||
validation_annee = validations_annee.get(etud.id, None)
|
||||
row.add_cell(
|
||||
"decision_annuelle",
|
||||
"Année",
|
||||
validation_annee.code if validation_annee else "",
|
||||
group="jury_code_sem",
|
||||
classes=["recorded_code"],
|
||||
)
|
||||
# Lien saisie ou visu jury
|
||||
a_saisir = (not res.validations) or (not res.validations.has_decision(etud))
|
||||
row.add_cell(
|
||||
|
|
|
@ -15,11 +15,9 @@
|
|||
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
|
||||
<input type="hidden" name="format" value="{{format}}"></input>
|
||||
Bulletin
|
||||
<span class="bull_liensemestre"><a href="{{
|
||||
url_for("notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_mois()
|
||||
}}</a></span>
|
||||
<span class="bull_liensemestre">
|
||||
{{formsemestre.html_link_status() | safe}}
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em>
|
||||
|
|
|
@ -15,14 +15,16 @@
|
|||
<div class="code_jury">{{validation.code}}</div>
|
||||
<div class="scoplement">
|
||||
<div>{{validation.ue1.acronyme}} - {{validation.ue2.acronyme}}</div>
|
||||
<div>Jury de {{validation.formsemestre.titre_annee()}}</div>
|
||||
<div>Jury de {{validation.formsemestre.titre_annee() if validation.formsemestre else "-"}}</div>
|
||||
<div>enregistré le {{
|
||||
validation.date.strftime("%d/%m/%Y à %H:%M")
|
||||
}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
-
|
||||
<div class="code_rcue">
|
||||
<div class="code_jury">-</div>
|
||||
</div>
|
||||
{%endif%}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -26,9 +26,13 @@
|
|||
En conséquence, saisir ensuite <b>manuellement les décisions manquantes</b>,
|
||||
notamment sur les UEs en dessous de 10.
|
||||
</p>
|
||||
<p class="warning">
|
||||
Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !
|
||||
</p>
|
||||
<div class="warning">
|
||||
<ul>
|
||||
<li>Ne jamais lancer ce calcul avant que toutes les notes ne soient saisies !
|
||||
(verrouiller le semestre ensuite)
|
||||
</li>
|
||||
<li>Il est nécessaire de relire soigneusement les décisions à l'issue de cette procédure !</li>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
{%- endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
<h2>{{formation.to_html()}}</h2>
|
||||
<h2>{{formation.html()}}</h2>
|
||||
|
||||
{# Liens vers les différents parcours #}
|
||||
<div class="les_parcours">
|
||||
|
@ -127,7 +127,7 @@ Choisissez un parcours...
|
|||
d'associer à chaque semestre d'un niveau de compétence une UE de la formation
|
||||
<a class="stdlink"
|
||||
href="{{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, formation_id=formation.id )
|
||||
}}">{{formation.to_html()}}
|
||||
}}">{{formation.html()}}
|
||||
</a>.</p>
|
||||
|
||||
<p>Le symbole <span class="parc">TC</span> désigne un niveau du tronc commun
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
{% extends 'base.j2' %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{% if not validations %}
|
||||
<p>Aucune validation de jury enregistrée pour <b>{{etud.html_link_fiche()|safe}}</b>
|
||||
sur <b>l'année {{annee}}</b>
|
||||
de la formation <em>{{ formation.html() }}</em>
|
||||
</p>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<a class="stdlink" href="{{ cancel_url }}">continuer</a>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<h2>Effacer les décisions de jury pour l'année {{annee}} de {{etud.html_link_fiche()|safe}} ?</h2>
|
||||
|
||||
<p class="help">Affectera toutes les décisions concernant l'année {{annee}} de la formation,
|
||||
quelle que soit leur origine.</p>
|
||||
|
||||
<p>Les décisions concernées sont:</p>
|
||||
<ul>
|
||||
{% for validation in validations %}
|
||||
<li>{{ validation.html() | safe}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<div style="margin-top: 16px;">
|
||||
<form method="post">
|
||||
<input type="submit" value="Effacer ces décisions" />
|
||||
{% if cancel_url %}
|
||||
<input type="button" value="Annuler" style="margin-left: 16px;"
|
||||
onClick="document.location='{{ cancel_url }}';" />
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -0,0 +1,135 @@
|
|||
{% extends 'base.j2' %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link href="{{scu.STATIC_DIR}}/css/jury_delete_manual.css" rel="stylesheet" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
|
||||
<h2>Décisions de jury enregistrées pour {{etud.html_link_fiche()|safe}}</h2>
|
||||
|
||||
<p class="help">
|
||||
Cette page liste toutes les décisions de jury connus de ScoDoc concernant cet étudiant
|
||||
et permet de les effacer une par une.
|
||||
</p>
|
||||
<p class="help">
|
||||
<b>Attention</b>, il vous appartient de vérifier la cohérence du résultat !
|
||||
En principe, <b>l'usage de cette page devrait rester exceptionnel</b>.
|
||||
Aucune annulation n'est ici possible (vous devrez re-saisir les décisions via les
|
||||
pages de saisie de jury habituelles).
|
||||
</p>
|
||||
{% if sem_vals.first() %}
|
||||
<div class="jury_decisions_list jury_decisions_sems">
|
||||
<div>Décisions de semestres</div>
|
||||
<ul>
|
||||
{% for v in sem_vals %}
|
||||
<li>{{v.html()|safe}}
|
||||
<form><button data-v_id="{{v.id}}" data-type="validation_formsemestre">effacer</button></form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if ue_vals.first() %}
|
||||
<div class="jury_decisions_list jury_decisions_ues">
|
||||
<div>Décisions d'UEs</div>
|
||||
<ul>
|
||||
{% for v in ue_vals %}
|
||||
<li>{{v.html(detail=True)|safe}}
|
||||
<form><button data-v_id="{{v.id}}" data-type="validation_ue">effacer</button></form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if rcue_vals.first() %}
|
||||
<div class="jury_decisions_list jury_decisions_rcues">
|
||||
<div>Décisions de RCUE (niveaux de compétences)</div>
|
||||
<ul>
|
||||
{% for v in rcue_vals %}
|
||||
<li>{{v.html()|safe}}
|
||||
<form><button data-v_id="{{v.id}}" data-type="validation_rcue">effacer</button></form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if annee_but_vals.first() %}
|
||||
<div class="jury_decisions_list jury_decisions_annees_but">
|
||||
<div>Décisions d'années BUT</div>
|
||||
<ul>
|
||||
{% for v in annee_but_vals %}
|
||||
<li>{{v.html()|safe}}
|
||||
<form><button data-v_id="{{v.id}}" data-type="validation_annee_but">effacer</button></form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if autorisations.first() %}
|
||||
<div class="jury_decisions_list jury_decisions_autorisation_inscription">
|
||||
<div>Autorisations d'inscriptions (passages)</div>
|
||||
<ul>
|
||||
{% for v in autorisations %}
|
||||
<li>{{v.html()|safe}}
|
||||
<form><button data-v_id="{{v.id}}" data-type="autorisation_inscription">effacer</button></form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not(
|
||||
sem_vals.first() or ue_vals.first() or rcue_vals.first()
|
||||
or annee_but_vals.first() or autorisations.first())
|
||||
%}
|
||||
<div>
|
||||
<p class="fontred">aucune décision enregistrée</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<p>retour à la fiche de {{etud.html_link_fiche()|safe}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{super()}}
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const buttons = document.querySelectorAll('.jury_decisions_list button');
|
||||
|
||||
buttons.forEach(button => {
|
||||
button.addEventListener('click', (event) => {
|
||||
// Handle button click event here
|
||||
event.preventDefault();
|
||||
const v_id = event.target.dataset.v_id;
|
||||
const validation_type = event.target.dataset.type;
|
||||
if (confirm("Supprimer cette validation ?")) {
|
||||
fetch(`${SCO_URL}/../api/etudiant/{{etud.id}}/jury/${validation_type}/${v_id}/delete`,
|
||||
{
|
||||
method: "POST",
|
||||
}).then(response => {
|
||||
// Handle the response
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error('Request failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -2897,7 +2897,12 @@ def formsemestre_jury_but_erase(formsemestre_id: int, etudid: int = None):
|
|||
)
|
||||
+ """
|
||||
<p>Les décisions des années scolaires précédentes ne seront pas modifiées.</p>
|
||||
<div class="warning">Cette opération est irréversible !</div>
|
||||
<p>Efface aussi toutes les validations concernant l'année BUT de ce semestre,
|
||||
même si elles ont été acquises ailleurs.
|
||||
</p>
|
||||
<div class="warning">Cette opération est irréversible !
|
||||
A n'utiliser que dans des cas exceptionnels, vérifiez bien tous les étudiants ensuite.
|
||||
</div>
|
||||
""",
|
||||
cancel_url=dest_url,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
"""validation niveaux inferieurs
|
||||
|
||||
Revision ID: c701224fa255
|
||||
Revises: d84bc592584e
|
||||
Create Date: 2023-06-11 11:08:05.553898
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import sessionmaker # added by ev
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "c701224fa255"
|
||||
down_revision = "d84bc592584e"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
Session = sessionmaker()
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Ajoute la colonne formation_id, nullable, la peuple puis la rend non nullable
|
||||
op.add_column(
|
||||
"apc_validation_annee", sa.Column("formation_id", sa.Integer(), nullable=True)
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"apc_validation_annee_formation_id_fkey",
|
||||
"apc_validation_annee",
|
||||
"notes_formations",
|
||||
["formation_id"],
|
||||
["id"],
|
||||
)
|
||||
|
||||
# Affecte la formation des anciennes validations
|
||||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
session.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE apc_validation_annee AS a
|
||||
SET formation_id = (
|
||||
SELECT f.id
|
||||
FROM notes_formations f
|
||||
JOIN notes_formsemestre s ON f.id = s.formation_id
|
||||
WHERE s.id = a.formsemestre_id
|
||||
)
|
||||
WHERE a.formsemestre_id IS NOT NULL;
|
||||
"""
|
||||
)
|
||||
)
|
||||
op.alter_column(
|
||||
"apc_validation_annee",
|
||||
"formation_id",
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
with op.batch_alter_table("apc_validation_annee", schema=None) as batch_op:
|
||||
batch_op.drop_constraint(
|
||||
"apc_validation_annee_formation_id_fkey", type_="foreignkey"
|
||||
)
|
||||
batch_op.drop_column("formation_id")
|
|
@ -25,7 +25,8 @@ def upgrade():
|
|||
bind = op.get_bind()
|
||||
session = Session(bind=bind)
|
||||
# Ajout extension pour recherches sans accents:
|
||||
session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
|
||||
# erreur: doit s'executer en superuser
|
||||
# session.execute(sa.text("""CREATE EXTENSION IF NOT EXISTS "unaccent";"""))
|
||||
|
||||
# Clé étrangère sur identite
|
||||
session.execute(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.4.83"
|
||||
SCOVERSION = "9.4.88"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ def test_permissions(api_headers):
|
|||
"role_name": "Ens",
|
||||
"start": "abc",
|
||||
"uid": 1,
|
||||
"validation_id": 1,
|
||||
"version": "long",
|
||||
}
|
||||
for rule in api_rules:
|
||||
|
|
|
@ -1,122 +1,122 @@
|
|||
# Tests unitaires jury BUT
|
||||
# Essais avec un BUT GCCD (GC-CD) et un parcours de S1 à S6
|
||||
# Le GCCD est un programme à 5 compétences, dont certaines
|
||||
# Le GCCD est un programme à 5 compétences, dont certaines
|
||||
# terminent en S4 ou en S6 selon les parcours.
|
||||
|
||||
ReferentielCompetences:
|
||||
filename: but-GCCD-05012022-081630.xml
|
||||
filename: but-GCCD-05012022-081630.xml
|
||||
specialite: GCCD
|
||||
|
||||
Formation:
|
||||
filename: scodoc_formation_BUT_GC-CD_v2.xml
|
||||
# Association des UEs aux compétences:
|
||||
ues:
|
||||
# S1 tronc commun:
|
||||
'UE1.1':
|
||||
# S1 tronc commun:
|
||||
"UE1.1":
|
||||
annee: BUT1
|
||||
competence: "Solutions Bâtiment"
|
||||
'UE1.2':
|
||||
"UE1.2":
|
||||
annee: BUT1
|
||||
competence: "Solutions TP"
|
||||
'UE1.3':
|
||||
"UE1.3":
|
||||
annee: BUT1
|
||||
competence: "Dimensionner"
|
||||
'UE1.4':
|
||||
"UE1.4":
|
||||
annee: BUT1
|
||||
competence: Organiser
|
||||
'UE1.5':
|
||||
"UE1.5":
|
||||
annee: BUT1
|
||||
competence: Piloter
|
||||
# S2 tronc commun:
|
||||
'UE2.1':
|
||||
# S2 tronc commun:
|
||||
"UE2.1":
|
||||
annee: BUT1
|
||||
competence: "Solutions Bâtiment"
|
||||
'UE2.2':
|
||||
"UE2.2":
|
||||
annee: BUT1
|
||||
competence: "Solutions TP"
|
||||
'UE2.3':
|
||||
"UE2.3":
|
||||
annee: BUT1
|
||||
competence: "Dimensionner"
|
||||
'UE2.4':
|
||||
"UE2.4":
|
||||
annee: BUT1
|
||||
competence: Organiser
|
||||
'UE2.5':
|
||||
"UE2.5":
|
||||
annee: BUT1
|
||||
competence: Piloter
|
||||
|
||||
|
||||
# S3 : Tronc commun
|
||||
'UE3.1':
|
||||
"UE3.1":
|
||||
annee: BUT2
|
||||
competence: "Solutions Bâtiment"
|
||||
'UE3.2':
|
||||
"UE3.2":
|
||||
annee: BUT2
|
||||
competence: "Solutions TP"
|
||||
'UE3.3':
|
||||
"UE3.3":
|
||||
annee: BUT2
|
||||
competence: "Dimensionner"
|
||||
'UE3.4':
|
||||
"UE3.4":
|
||||
annee: BUT2
|
||||
competence: Organiser
|
||||
'UE3.5':
|
||||
"UE3.5":
|
||||
annee: BUT2
|
||||
competence: Piloter
|
||||
# S4 Tronc commun
|
||||
'UE4.1':
|
||||
"UE4.1":
|
||||
annee: BUT2
|
||||
competence: "Solutions Bâtiment"
|
||||
'UE4.2':
|
||||
"UE4.2":
|
||||
annee: BUT2
|
||||
competence: "Solutions TP"
|
||||
'UE4.3':
|
||||
"UE4.3":
|
||||
annee: BUT2
|
||||
competence: "Dimensionner"
|
||||
'UE4.4':
|
||||
"UE4.4":
|
||||
annee: BUT2
|
||||
competence: Organiser
|
||||
'UE4.5':
|
||||
"UE4.5":
|
||||
annee: BUT2
|
||||
competence: Piloter
|
||||
# S5 Parcours BAT + TP
|
||||
'UE5.1': # Parcours BAT seulement
|
||||
"UE5.1": # Parcours BAT seulement
|
||||
annee: BUT3
|
||||
parcours: BAT # + RAPEB, BEC
|
||||
competence: "Solutions Bâtiment"
|
||||
'UE5.2': # Parcours TP seulement
|
||||
"UE5.2": # Parcours TP seulement
|
||||
annee: BUT3
|
||||
parcours: TP # + BEC
|
||||
competence: "Solutions TP"
|
||||
'UE5.3':
|
||||
"UE5.3":
|
||||
annee: BUT3
|
||||
parcours: [RAPEB, BEC]
|
||||
competence: "Dimensionner"
|
||||
'UE5.4':
|
||||
"UE5.4":
|
||||
annee: BUT3
|
||||
parcours: [BAT, TP]
|
||||
competence: Organiser
|
||||
'UE5.5':
|
||||
"UE5.5":
|
||||
annee: BUT3
|
||||
parcours: [BAT, TP]
|
||||
competence: Piloter
|
||||
# S6 Parcours BAT + TP
|
||||
'UE6.1': # Parcours BAT seulement
|
||||
"UE6.1": # Parcours BAT seulement
|
||||
annee: BUT3
|
||||
parcours: BAT # + RAPEB, BEC
|
||||
competence: "Solutions Bâtiment"
|
||||
'UE6.2': # Parcours TP seulement
|
||||
"UE6.2": # Parcours TP seulement
|
||||
annee: BUT3
|
||||
parcours: [TP,BEC]
|
||||
parcours: [TP, BEC]
|
||||
competence: "Solutions TP"
|
||||
'UE6.3':
|
||||
"UE6.3":
|
||||
annee: BUT3
|
||||
parcours: [RAPEB,BEC]
|
||||
parcours: [RAPEB, BEC]
|
||||
competence: "Dimensionner"
|
||||
'UE6.4':
|
||||
"UE6.4":
|
||||
annee: BUT3
|
||||
parcours: [BAT, TP]
|
||||
competence: Organiser
|
||||
'UE6.5':
|
||||
"UE6.5":
|
||||
annee: BUT3
|
||||
parcours: [BAT,TP]
|
||||
parcours: [BAT, TP]
|
||||
competence: Piloter
|
||||
|
||||
modules_parcours:
|
||||
|
@ -126,8 +126,8 @@ Formation:
|
|||
# - tous les module de S1 à S4 dans tous les parcours
|
||||
# - SAE communes en S1 et S2 mais différenciées par parcours ensuite
|
||||
# - en S5, ressources différenciées: on ne les mentionne pas toutes ici
|
||||
BAT: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14" ]
|
||||
TP: [ "R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]" ]
|
||||
BAT: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.BAT.*", "R5.0[1-7]", "R5.14"]
|
||||
TP: ["R[1-4].*", "SAÉ [1-2]", "SAÉ *.TP.*", "R5.0[1-4]", "R5.0[89]"]
|
||||
|
||||
FormSemestres:
|
||||
# S1 et S2 avec les parcours BAT et TP:
|
||||
|
@ -135,32 +135,32 @@ FormSemestres:
|
|||
idx: 1
|
||||
date_debut: 2021-09-01
|
||||
date_fin: 2022-01-15
|
||||
codes_parcours: ['BAT', 'TP']
|
||||
S2:
|
||||
codes_parcours: ["BAT", "TP"]
|
||||
S2:
|
||||
idx: 2
|
||||
date_debut: 2022-01-15
|
||||
date_fin: 2022-06-30
|
||||
codes_parcours: ['BAT', 'TP']
|
||||
codes_parcours: ["BAT", "TP"]
|
||||
S3:
|
||||
idx: 3
|
||||
date_debut: 2022-09-01
|
||||
date_fin: 2023-01-15
|
||||
codes_parcours: ['BAT', 'TP']
|
||||
codes_parcours: ["BAT", "TP"]
|
||||
S4:
|
||||
idx: 4
|
||||
date_debut: 2023-01-16
|
||||
date_fin: 2023-06-30
|
||||
codes_parcours: ['BAT', 'TP']
|
||||
codes_parcours: ["BAT", "TP"]
|
||||
S5:
|
||||
idx: 5
|
||||
date_debut: 2023-09-01
|
||||
date_fin: 2024-01-15
|
||||
codes_parcours: ['BAT', 'TP']
|
||||
codes_parcours: ["BAT", "TP"]
|
||||
S6:
|
||||
idx: 6
|
||||
date_debut: 2024-01-16
|
||||
date_fin: 2024-06-30
|
||||
codes_parcours: ['BAT', 'TP']
|
||||
codes_parcours: ["BAT", "TP"]
|
||||
|
||||
Etudiants:
|
||||
A_ok: # Etudiant parcours BAT qui va tout valider directement
|
||||
|
@ -171,10 +171,18 @@ Etudiants:
|
|||
parcours: BAT
|
||||
notes_modules:
|
||||
"R1.01": 11 # toutes UEs
|
||||
"SAÉ 1-2": EXC
|
||||
S2:
|
||||
parcours: BAT
|
||||
notes_modules:
|
||||
"R2.01": 12 # toutes UEs
|
||||
attendu: # les codes jury que l'on doit vérifier
|
||||
deca:
|
||||
passage_de_droit: True
|
||||
autorisations_inscription: [3]
|
||||
code_valide:
|
||||
nb_competences: 5
|
||||
nb_rcue_annee: 4
|
||||
S3:
|
||||
parcours: BAT
|
||||
notes_modules:
|
||||
|
@ -186,7 +194,7 @@ Etudiants:
|
|||
|
||||
S5:
|
||||
parcours: BAT
|
||||
dispense_ues: ['UE5.2', 'UE5.3']
|
||||
dispense_ues: ["UE5.2", "UE5.3"]
|
||||
notes_modules:
|
||||
"R5.01": 15 # toutes UE
|
||||
"SAÉ 5.BAT.01": 10 # UE5.1
|
||||
|
@ -202,6 +210,7 @@ Etudiants:
|
|||
parcours: TP
|
||||
notes_modules:
|
||||
"R1.01": 11 # toutes UEs
|
||||
"SAÉ 1-2": EXC
|
||||
S2:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
|
@ -217,10 +226,55 @@ Etudiants:
|
|||
|
||||
S5:
|
||||
parcours: TP
|
||||
dispense_ues: ['UE5.1', 'UE5.3']
|
||||
dispense_ues: ["UE5.1", "UE5.3"]
|
||||
notes_modules:
|
||||
"R5.01": 15 # toutes UE
|
||||
"SAÉ 5.BAT.01": 10 # UE5.1
|
||||
"SAÉ 5.BAT.02": 11 # UE5.4
|
||||
S6:
|
||||
parcours: TP
|
||||
|
||||
C: # Etudiant qui passe sans un RCUE et valide en BUT2
|
||||
prenom: Étudiant_TP_but2
|
||||
civilite: M
|
||||
formsemestres:
|
||||
S1:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R1.01": 11 # toutes UEs
|
||||
"SAÉ 1-2": 8 # plombe l'UE 2
|
||||
S2:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R2.01": 11 # toutes UEs
|
||||
S3:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R3.01": 12 # toutes UEs
|
||||
S4:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R4.01": 14 # toutes UE
|
||||
|
||||
D: # Etudiant arrive en S4 avec une UE manquante en S1
|
||||
prenom: Étudiant_TP_malaise
|
||||
civilite: M
|
||||
formsemestres:
|
||||
S1:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R1.01": 11 # toutes UEs
|
||||
"SAÉ 1-2": 8 # plombe l'UE 2
|
||||
S2:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R2.01": 11 # toutes UEs
|
||||
S3:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R3.01": 12 # toutes UEs
|
||||
S4:
|
||||
parcours: TP
|
||||
notes_modules:
|
||||
"R4.01": 14 # toutes UE
|
||||
"R4.04": 6 # plombe l'UE1
|
||||
|
|
|
@ -146,7 +146,7 @@ def create_formsemestre(
|
|||
return formsemestre
|
||||
|
||||
|
||||
def create_evaluations(formsemestre: FormSemestre):
|
||||
def create_evaluations(formsemestre: FormSemestre, publish_incomplete=True):
|
||||
"""Crée une évaluation dans chaque module du semestre"""
|
||||
for modimpl in formsemestre.modimpls:
|
||||
evaluation = Evaluation(
|
||||
|
@ -156,6 +156,7 @@ def create_evaluations(formsemestre: FormSemestre):
|
|||
coefficient=1.0,
|
||||
note_max=20.0,
|
||||
numero=1,
|
||||
publish_incomplete=publish_incomplete,
|
||||
)
|
||||
db.session.add(evaluation)
|
||||
|
||||
|
|
|
@ -31,4 +31,4 @@ source "$SCRIPT_DIR"/utils.sh || die "config.sh not found, exiting"
|
|||
# ---
|
||||
echo 'Creating postgresql database ' "$db_name"
|
||||
createdb -E UTF-8 -p "$POSTGRES_PORT" -O "$POSTGRES_USER" "$db_name"
|
||||
|
||||
echo 'CREATE EXTENSION IF NOT EXISTS "unaccent";' | psql -p "$POSTGRES_PORT" "$db_name" "$POSTGRES_USER"
|
||||
|
|
|
@ -97,13 +97,15 @@ fi
|
|||
init_postgres_user
|
||||
|
||||
# ------------ BASE DE DONNEES
|
||||
# gérées avec Flask-Migrate (Alembic/SQLAlchemy)
|
||||
# Si la base SCODOC existe, tente de la mettre à jour
|
||||
# (Ne gère pas les bases DEV et TEST)
|
||||
n=$(su -c "psql -l | grep -c -E '^[[:blank:]]*SCODOC[[:blank:]]*\|'" "$SCODOC_USER")
|
||||
if [ "$n" == 1 ]
|
||||
then
|
||||
echo "Upgrading existing SCODOC database..."
|
||||
# Ajout extension unaccent (postgres superuser, ajout sur base SCODOC)
|
||||
(cd /tmp; echo 'CREATE EXTENSION IF NOT EXISTS "unaccent";' | su -c "psql SCODOC" postgres)
|
||||
# Migrations gérées avec Flask-Migrate (Alembic/SQLAlchemy)
|
||||
# utilise les scripts dans migrations/version/
|
||||
# pour mettre à jour notre base (en tant qu'utilisateur scodoc)
|
||||
export FLASK_ENV="production"
|
||||
|
|
Loading…
Reference in New Issue