WIP: Affichage validation cursus BUT sur page etudiant.

This commit is contained in:
Emmanuel Viennet 2023-01-16 21:05:48 -03:00 committed by iziram
parent 0b9c9be222
commit bdf90dfd69
8 changed files with 336 additions and 48 deletions

View File

@ -13,7 +13,7 @@ Classe raccordant avec ScoDoc 7:
avec la même interface.
"""
import collections
from typing import Union
from flask import g, url_for
@ -47,12 +47,14 @@ from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoException, ScoValueError
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT
@ -65,3 +67,114 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
def parcours_validated(self):
"True si le parcours est validé"
return False # XXX TODO
class EtudCursusBUT:
"""L'état de l'étudiant dans son cursus BUT
Liste des niveaux validés/à valider
"""
def __init__(self, etud: Identite, formation: Formation):
"""formation indique la spécialité préparée"""
# Vérifie que l'étudiant est bien inscrit à un sem. de cette formation
if formation.id not in (
ins.formsemestre.formation.id for ins in etud.formsemestre_inscriptions
):
raise ScoValueError(
f"{etud.nomprenom} non inscrit dans {formation.titre} v{formation.version}"
)
if not formation.referentiel_competence:
raise ScoNoReferentielCompetences(formation=formation)
#
self.etud = etud
self.formation = formation
self.inscriptions = sorted(
[
ins
for ins in etud.formsemestre_inscriptions
if ins.formsemestre.formation.referentiel_competence.id
== formation.referentiel_competence.id
],
key=lambda s: (s.formsemestre.semestre_id, s.formsemestre.date_debut),
)
"Liste des inscriptions aux sem. de la formation, triées par indice et chronologie"
self.parcour: ApcParcours = self.inscriptions[-1].parcour
"Le parcour à valider: celui du DERNIER semestre suivi (peut être None)"
self.niveaux_by_annee = {}
"{ annee : liste des niveaux à valider }"
self.niveaux: dict[int, ApcNiveau] = {}
"cache les niveaux"
for annee in (1, 2, 3):
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
annee, self.parcour
)[1]
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
niveaux_d[self.parcour.id] if self.parcour else []
)
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, ... } }"
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
niveau = validation_rcue.niveau()
if not niveau.competence.id in self.validation_par_competence_et_annee:
self.validation_par_competence_et_annee[niveau.competence.id] = {}
previous_validation = self.validation_par_competence_et_annee.get(
niveau.competence.id
)
# 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]
):
self.validation_par_competence_et_annee[niveau.competence.id][
niveau.annee
] = validation_rcue
self.competences = {
competence.id: competence
for competence in (
self.parcour.query_competences()
if self.parcour
else self.formation.referentiel_competence.get_competences_tronc_commun()
)
}
"cache { competence_id : competence }"
def to_dict(self):
"""
{
competence_id : {
annee : meilleure_validation
}
}
"""
return {
competence.id: {
annee: {
self.validation_par_competence_et_annee.get(competence.id, {}).get(
annee
)
}
for annee in ("BUT1", "BUT2", "BUT3")
}
for competence in self.competences.values()
}

View File

@ -94,9 +94,10 @@ class ApcReferentielCompetences(db.Model, XMLModel):
return ""
return self.version_orebut.split()[0]
def to_dict(self):
def to_dict(self, parcours: list["ApcParcours"] = None, with_app_critiques=True):
"""Représentation complète du ref. de comp.
comme un dict.
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
"""
return {
"dept_id": self.dept_id,
@ -111,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
if self.scodoc_date_loaded
else "",
"scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences},
"parcours": {x.code: x.to_dict() for x in self.parcours},
"competences": {
x.titre: x.to_dict(with_app_critiques=with_app_critiques)
for x in self.competences
},
"parcours": {
x.code: x.to_dict()
for x in (self.parcours if parcours is None else parcours)
},
}
def get_niveaux_by_parcours(
@ -174,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return parcours, niveaux_by_parcours_no_tc
def get_competences_tronc_commun(self) -> list["ApcCompetence"]:
"""Liste des compétences communes à tous les parcours du référentiel."""
parcours = self.parcours.all()
if not parcours:
return []
ids = set.intersection(
*[
{competence.id for competence in parcour.query_competences()}
for parcour in parcours
]
)
return sorted(
[
competence
for competence in parcours[0].query_competences()
if competence.id in ids
],
key=lambda c: c.numero or 0,
)
class ApcCompetence(db.Model, XMLModel):
"Compétence"
@ -215,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre!r}>"
def to_dict(self):
def to_dict(self, with_app_critiques=True):
"repr dict recursive sur situations, composantes, niveaux"
return {
"id_orebut": self.id_orebut,
@ -227,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
"composantes_essentielles": [
x.to_dict() for x in self.composantes_essentielles
],
"niveaux": {x.annee: x.to_dict() for x in self.niveaux},
"niveaux": {
x.annee: x.to_dict(with_app_critiques=with_app_critiques)
for x in self.niveaux
},
}
def to_dict_bul(self) -> dict:
@ -293,13 +324,15 @@ class ApcNiveau(db.Model, XMLModel):
return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>"""
def to_dict(self):
"as a dict, recursif sur les AC"
def to_dict(self, with_app_critiques=True):
"as a dict, recursif (ou non) sur les AC"
return {
"libelle": self.libelle,
"annee": self.annee,
"ordre": self.ordre,
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques},
"app_critiques": {x.code: x.to_dict() for x in self.app_critiques}
if with_app_critiques
else {},
}
def to_dict_bul(self):
@ -471,6 +504,14 @@ class ApcParcours(db.Model, XMLModel):
d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
return d
def query_competences(self) -> flask_sqlalchemy.BaseQuery:
"Les compétences associées à ce parcours"
return (
ApcCompetence.query.join(ApcParcoursNiveauCompetence, ApcAnneeParcours)
.filter_by(parcours_id=self.id)
.order_by(ApcCompetence.numero)
)
class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True)

View File

@ -76,6 +76,12 @@ class ApcValidationRCUE(db.Model):
# Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence
def to_dict(self):
"as a dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def to_dict_bul(self) -> dict:
"Export dict pour bulletins: le code et le niveau de compétence"
niveau = self.niveau()

View File

@ -30,14 +30,15 @@
Fiche description d'un étudiant et de son parcours
"""
from flask import abort, url_for, g, request
from flask import abort, url_for, g, render_template, request
from flask_login import current_user
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
from app.but import jury_but_view
from app.models.etudiants import make_etud_args
from app.but import cursus_but, jury_but_view
from app.models.etudiants import Identite, make_etud_args
from app.models.formsemestre import FormSemestre
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_archives_etud
@ -169,11 +170,12 @@ def ficheEtud(etudid=None):
if not etuds:
log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}")
raise ScoValueError("Étudiant inexistant !")
etud = etuds[0]
etudid = etud["etudid"]
sco_etud.fill_etuds_info([etud])
etud_ = etuds[0] # transition: etud_ à éliminer et remplacer par etud
etudid = etud_["etudid"]
etud = Identite.query.get(etudid)
sco_etud.fill_etuds_info([etud_])
#
info = etud
info = etud_
info["ScoURL"] = scu.ScoURL()
info["authuser"] = authuser
info["info_naissance"] = info["date_naissance"]
@ -181,7 +183,7 @@ def ficheEtud(etudid=None):
info["info_naissance"] += " à " + info["lieu_naissance"]
if info["dept_naissance"]:
info["info_naissance"] += f" ({info['dept_naissance']})"
info["etudfoto"] = sco_photos.etud_photo_html(etud)
info["etudfoto"] = sco_photos.etud_photo_html(etud_)
if (
(not info["domicile"])
and (not info["codepostaldomicile"])
@ -206,7 +208,7 @@ def ficheEtud(etudid=None):
info["emaillink"] = ", ".join(
[
'<a class="stdlink" href="mailto:%s">%s</a>' % (m, m)
for m in [etud["email"], etud["emailperso"]]
for m in [etud_["email"], etud_["emailperso"]]
if m
]
)
@ -277,7 +279,7 @@ def ficheEtud(etudid=None):
sem_info[sem["formsemestre_id"]] = grlink
if info["sems"]:
Se = sco_cursus.get_situation_etud_cursus(etud, info["last_formsemestre_id"])
Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"])
info["liste_inscriptions"] = formsemestre_recap_parcours_table(
Se,
etudid,
@ -454,6 +456,18 @@ def ficheEtud(etudid=None):
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche...
info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
# XXX dev
info["but_cursus_mkup"] = ""
if info["sems"]:
last_sem = FormSemestre.query.get_or_404(info["sems"][-1]["formsemestre_id"])
if last_sem.formation.is_apc():
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
info["but_cursus_mkup"] = render_template(
"but/cursus_etud.j2",
cursus=but_cursus,
scu=scu,
)
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
<div class="ficheEtud" id="ficheEtud"><table>
<tr><td>
@ -488,6 +502,8 @@ def ficheEtud(etudid=None):
%(but_infos_mkup)s
%(but_cursus_mkup)s
<div class="ficheadmission">
%(adm_data)s
@ -524,7 +540,11 @@ def ficheEtud(etudid=None):
"""
header = html_sco_header.sco_header(
page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
cssstyles=["libjs/jQuery-tagEditor/jquery.tag-editor.css", "css/jury_but.css"],
cssstyles=[
"libjs/jQuery-tagEditor/jquery.tag-editor.css",
"css/jury_but.css",
"css/cursus_but.css",
],
javascripts=[
"libjs/jinplace-1.2.1.min.js",
"js/ue_list.js",

View File

@ -0,0 +1,42 @@
/* Affichage cursus BUT étudiant (sur sa fiche) */
.cursus_but {
margin-left: 32px;
display: inline-grid;
grid-template-columns: repeat(4, auto);
gap: 8px;
}
.cursus_but>* {
display: flex;
align-items: center;
padding-top: 0px;
padding-bottom: 0px;
padding-left: 16px;
padding-right: 0px;
background: #FFF;
border: 1px solid #aaa;
border-radius: 8px;
}
.cursus_but>div.cb_head {
background: rgb(242, 242, 238);
border: none;
border-radius: 0px;
border-bottom: 1px solid gray;
font-weight: bold;
}
div.cb_titre_competence {
background: #09c !important;
color: #FFF;
padding: 8px !important;
}
div.code_rcue {
padding-top: 8px;
padding-bottom: 8px;
position: relative;
}

View File

@ -1,24 +1,27 @@
:host{
:host {
font-family: Verdana;
background: #222;
background: rgb(14, 5, 73);
display: block;
padding: 12px 32px;
color: #FFF;
max-width: 1000px;
margin: auto;
}
h1{
h1 {
font-weight: 100;
}
/**********************/
/* Zone parcours */
/**********************/
.parcours{
.parcours {
display: flex;
gap: 4px;
padding-right: 4px;
}
.parcours>div{
.parcours>div {
background: #09c;
font-size: 18px;
text-align: center;
@ -29,23 +32,26 @@ h1{
transition: 0.1s;
opacity: 0.7;
}
.parcours>div:hover,
.competence>div:hover{
.competence>div:hover {
color: #ccc;
}
.parcours>.focus{
.parcours>.focus {
opacity: 1;
}
/**********************/
/* Zone compétences */
/**********************/
.competences{
.competences {
display: grid;
margin-top: 8px;
row-gap: 4px;
}
.competences>div{
.competences>div {
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
@ -53,30 +59,49 @@ h1{
margin-right: 4px;
}
.comp1{background:#a44}
.comp2{background:#84a}
.comp3{background:#a84}
.comp4{background:#8a4}
.comp5{background:#4a8}
.comp6{background:#48a}
.comp1 {
background: #a44
}
.competences>.focus{
.comp2 {
background: #84a
}
.comp3 {
background: #a84
}
.comp4 {
background: #8a4
}
.comp5 {
background: #4a8
}
.comp6 {
background: #48a
}
.competences>.focus {
outline: 2px solid;
}
/**********************/
/* Zone AC */
/**********************/
h2{
h2 {
display: table;
padding: 8px 16px;
font-size: 20px;
border-radius: 16px 0;
}
.ACs{
.ACs {
padding-right: 4px;
}
.AC li{
.AC li {
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
@ -84,10 +109,12 @@ h2{
margin-bottom: 4px;
border-bottom: 1px solid;
}
.AC li>div:nth-child(1){
.AC li>div:nth-child(1) {
padding: 2px 4px;
border-radius: 4px;
}
.AC li>div:nth-child(2){
.AC li>div:nth-child(2) {
padding-bottom: 2px;
}

View File

@ -0,0 +1,26 @@
{# Affichage cursus BUT fiche étudiant #}
<div class="cursus_but">
<div class="cb_head"></div>
<div class="cb_head">BUT 1</div>
<div class="cb_head">BUT 2</div>
<div class="cb_head">BUT 3</div>
{% for competence_id in cursus.to_dict() %}
<div class="cb_titre_competence">{{ cursus.competences[competence_id].titre }}</div>
{% for annee in ('BUT1', 'BUT2', 'BUT3') %}
{% set validation = cursus.validation_par_competence_et_annee.get(competence_id, {}).get(annee) %}
<div>
{% if validation %}
<div class="code_rcue with_scoplement">
<div>{{validation.code}}</div>
<div class="scoplement">Validé le {{
validation.date.strftime("%d/%m/%Y à %H:%M")
}}</div>
</div>
{% else %}
-
{%endif%}
</div>
{% endfor %}
{% endfor %}
</div>

View File

@ -33,6 +33,13 @@ from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models import Partition
from app.models import ScolarFormSemestreValidation
from app.models.but_refcomp import (
ApcCompetence,
ApcNiveau,
ApcParcours,
ApcReferentielCompetences,
)
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.evaluations import Evaluation
from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission
@ -57,6 +64,12 @@ def make_shell_context():
from app.scodoc import sco_utils as scu
return {
"ApcCompetence": ApcCompetence,
"ApcNiveau": ApcNiveau,
"ApcParcours": ApcParcours,
"ApcReferentielCompetences": ApcReferentielCompetences,
"ApcValidationRCUE": ApcValidationRCUE,
"ApcValidationAnnee": ApcValidationAnnee,
"ctx": app.test_request_context(),
"current_app": flask.current_app,
"current_user": current_user,
@ -71,21 +84,21 @@ def make_shell_context():
"login_user": login_user,
"logout_user": logout_user,
"mapp": mapp,
"models": models,
"Matiere": Matiere,
"models": models,
"Module": Module,
"ModuleImpl": ModuleImpl,
"ModuleImplInscription": ModuleImplInscription,
"Partition": Partition,
"ndb": ndb,
"notes": notes,
"np": np,
"Partition": Partition,
"pd": pd,
"Permission": Permission,
"pp": pp,
"Role": Role,
"res_sem": res_sem,
"ResultatsSemestreBUT": ResultatsSemestreBUT,
"Role": Role,
"scolar": scolar,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews,