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. avec la même interface.
""" """
import collections
from typing import Union from typing import Union
from flask import g, url_for 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 import sco_codes_parcours as sco_codes
from app.scodoc.sco_codes_parcours import RED, UE_STANDARD from app.scodoc.sco_codes_parcours import RED, UE_STANDARD
from app.scodoc import sco_utils as scu 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 from app.scodoc import sco_cursus_dut
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic): class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT): def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
super().__init__(etud, formsemestre_id, res) super().__init__(etud, formsemestre_id, res)
# Ajustements pour le BUT # Ajustements pour le BUT
@ -65,3 +67,114 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
def parcours_validated(self): def parcours_validated(self):
"True si le parcours est validé" "True si le parcours est validé"
return False # XXX TODO 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 ""
return self.version_orebut.split()[0] 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. """Représentation complète du ref. de comp.
comme un dict. comme un dict.
Si parcours est une liste de parcours, restreint l'export aux parcours listés.
""" """
return { return {
"dept_id": self.dept_id, "dept_id": self.dept_id,
@ -111,8 +112,14 @@ class ApcReferentielCompetences(db.Model, XMLModel):
if self.scodoc_date_loaded if self.scodoc_date_loaded
else "", else "",
"scodoc_orig_filename": self.scodoc_orig_filename, "scodoc_orig_filename": self.scodoc_orig_filename,
"competences": {x.titre: x.to_dict() for x in self.competences}, "competences": {
"parcours": {x.code: x.to_dict() for x in self.parcours}, 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( def get_niveaux_by_parcours(
@ -174,6 +181,27 @@ class ApcReferentielCompetences(db.Model, XMLModel):
niveaux_by_parcours_no_tc["TC"] = niveaux_tc niveaux_by_parcours_no_tc["TC"] = niveaux_tc
return parcours, niveaux_by_parcours_no_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): class ApcCompetence(db.Model, XMLModel):
"Compétence" "Compétence"
@ -215,7 +243,7 @@ class ApcCompetence(db.Model, XMLModel):
def __repr__(self): def __repr__(self):
return f"<ApcCompetence {self.id} {self.titre!r}>" 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" "repr dict recursive sur situations, composantes, niveaux"
return { return {
"id_orebut": self.id_orebut, "id_orebut": self.id_orebut,
@ -227,7 +255,10 @@ class ApcCompetence(db.Model, XMLModel):
"composantes_essentielles": [ "composantes_essentielles": [
x.to_dict() for x in self.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: 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={ return f"""<{self.__class__.__name__} {self.id} ordre={self.ordre!r} annee={
self.annee!r} {self.competence!r}>""" self.annee!r} {self.competence!r}>"""
def to_dict(self): def to_dict(self, with_app_critiques=True):
"as a dict, recursif sur les AC" "as a dict, recursif (ou non) sur les AC"
return { return {
"libelle": self.libelle, "libelle": self.libelle,
"annee": self.annee, "annee": self.annee,
"ordre": self.ordre, "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): 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} d["annees"] = {x.ordre: x.to_dict() for x in self.annees}
return d 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): class ApcAnneeParcours(db.Model, XMLModel):
id = db.Column(db.Integer, primary_key=True) 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 # Par convention, il est donné par la seconde UE
return self.ue2.niveau_competence 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: def to_dict_bul(self) -> dict:
"Export dict pour bulletins: le code et le niveau de compétence" "Export dict pour bulletins: le code et le niveau de compétence"
niveau = self.niveau() niveau = self.niveau()

View File

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

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; font-family: Verdana;
background: #222; background: rgb(14, 5, 73);
display: block; display: block;
padding: 12px 32px; padding: 12px 32px;
color: #FFF; color: #FFF;
max-width: 1000px; max-width: 1000px;
margin: auto; margin: auto;
} }
h1{
h1 {
font-weight: 100; font-weight: 100;
} }
/**********************/ /**********************/
/* Zone parcours */ /* Zone parcours */
/**********************/ /**********************/
.parcours{ .parcours {
display: flex; display: flex;
gap: 4px; gap: 4px;
padding-right: 4px; padding-right: 4px;
} }
.parcours>div{
.parcours>div {
background: #09c; background: #09c;
font-size: 18px; font-size: 18px;
text-align: center; text-align: center;
@ -29,65 +32,89 @@ h1{
transition: 0.1s; transition: 0.1s;
opacity: 0.7; opacity: 0.7;
} }
.parcours>div:hover, .parcours>div:hover,
.competence>div:hover{ .competence>div:hover {
color: #ccc; color: #ccc;
} }
.parcours>.focus{
.parcours>.focus {
opacity: 1; opacity: 1;
} }
/**********************/ /**********************/
/* Zone compétences */ /* Zone compétences */
/**********************/ /**********************/
.competences{ .competences {
display: grid; display: grid;
margin-top: 8px; margin-top: 8px;
row-gap: 4px; row-gap: 4px;
} }
.competences>div{
.competences>div {
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
width: var(--competence-size); width: var(--competence-size);
margin-right: 4px; margin-right: 4px;
} }
.comp1{background:#a44} .comp1 {
.comp2{background:#84a} background: #a44
.comp3{background:#a84} }
.comp4{background:#8a4}
.comp5{background:#4a8}
.comp6{background:#48a}
.competences>.focus{ .comp2 {
background: #84a
}
.comp3 {
background: #a84
}
.comp4 {
background: #8a4
}
.comp5 {
background: #4a8
}
.comp6 {
background: #48a
}
.competences>.focus {
outline: 2px solid; outline: 2px solid;
} }
/**********************/ /**********************/
/* Zone AC */ /* Zone AC */
/**********************/ /**********************/
h2{ h2 {
display: table; display: table;
padding: 8px 16px; padding: 8px 16px;
font-size: 20px; font-size: 20px;
border-radius: 16px 0; border-radius: 16px 0;
} }
.ACs{
.ACs {
padding-right: 4px; padding-right: 4px;
} }
.AC li{
.AC li {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
align-items: start; align-items: start;
gap: 4px; gap: 4px;
margin-bottom: 4px; margin-bottom: 4px;
border-bottom: 1px solid; border-bottom: 1px solid;
} }
.AC li>div:nth-child(1){
.AC li>div:nth-child(1) {
padding: 2px 4px; padding: 2px 4px;
border-radius: 4px; border-radius: 4px;
} }
.AC li>div:nth-child(2){
.AC li>div:nth-child(2) {
padding-bottom: 2px; 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 ModuleImpl, ModuleImplInscription
from app.models import Partition from app.models import Partition
from app.models import ScolarFormSemestreValidation 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.models.evaluations import Evaluation
from app.scodoc.sco_logos import make_logo_local from app.scodoc.sco_logos import make_logo_local
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -57,6 +64,12 @@ def make_shell_context():
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
return { return {
"ApcCompetence": ApcCompetence,
"ApcNiveau": ApcNiveau,
"ApcParcours": ApcParcours,
"ApcReferentielCompetences": ApcReferentielCompetences,
"ApcValidationRCUE": ApcValidationRCUE,
"ApcValidationAnnee": ApcValidationAnnee,
"ctx": app.test_request_context(), "ctx": app.test_request_context(),
"current_app": flask.current_app, "current_app": flask.current_app,
"current_user": current_user, "current_user": current_user,
@ -71,21 +84,21 @@ def make_shell_context():
"login_user": login_user, "login_user": login_user,
"logout_user": logout_user, "logout_user": logout_user,
"mapp": mapp, "mapp": mapp,
"models": models,
"Matiere": Matiere, "Matiere": Matiere,
"models": models,
"Module": Module, "Module": Module,
"ModuleImpl": ModuleImpl, "ModuleImpl": ModuleImpl,
"ModuleImplInscription": ModuleImplInscription, "ModuleImplInscription": ModuleImplInscription,
"Partition": Partition,
"ndb": ndb, "ndb": ndb,
"notes": notes, "notes": notes,
"np": np, "np": np,
"Partition": Partition,
"pd": pd, "pd": pd,
"Permission": Permission, "Permission": Permission,
"pp": pp, "pp": pp,
"Role": Role,
"res_sem": res_sem, "res_sem": res_sem,
"ResultatsSemestreBUT": ResultatsSemestreBUT, "ResultatsSemestreBUT": ResultatsSemestreBUT,
"Role": Role,
"scolar": scolar, "scolar": scolar,
"ScolarFormSemestreValidation": ScolarFormSemestreValidation, "ScolarFormSemestreValidation": ScolarFormSemestreValidation,
"ScolarNews": models.ScolarNews, "ScolarNews": models.ScolarNews,