Bul. BUT: ECTS, Absences, Appréciations.

This commit is contained in:
Emmanuel Viennet 2022-03-15 21:50:37 +01:00
parent ab87a98eda
commit 7409ccce5a
11 changed files with 124 additions and 46 deletions

View File

@ -13,6 +13,7 @@ from flask import url_for, g
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
from app.models.ues import UniteEns
from app.scodoc import sco_bulletins, sco_utils as scu from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
@ -62,18 +63,15 @@ class BulletinBUT:
# } # }
return d return d
def etud_ue_results(self, etud, ue): def etud_ue_results(self, etud: Identite, ue: UniteEns, decision_ue: dict) -> dict:
"dict synthèse résultats UE" "dict synthèse résultats UE"
res = self.res res = self.res
d = { d = {
"id": ue.id, "id": ue.id,
"titre": ue.titre, "titre": ue.titre,
"numero": ue.numero, "numero": ue.numero,
"type": ue.type, "type": ue.type,
"ECTS": {
"acquis": 0.0, # XXX TODO voir jury #sco92
"total": ue.ects or 0.0, # float même si non renseigné
},
"color": ue.color, "color": ue.color,
"competence": None, # XXX TODO lien avec référentiel "competence": None, # XXX TODO lien avec référentiel
"moyenne": None, "moyenne": None,
@ -86,6 +84,11 @@ class BulletinBUT:
"ressources": self.etud_ue_mod_results(etud, ue, res.ressources), "ressources": self.etud_ue_mod_results(etud, ue, res.ressources),
"saes": self.etud_ue_mod_results(etud, ue, res.saes), "saes": self.etud_ue_mod_results(etud, ue, res.saes),
} }
if self.prefs["bul_show_ects"]:
d["ECTS"] = {
"acquis": decision_ue.get("ects", 0.0),
"total": ue.ects or 0.0, # float même si non renseigné
}
if ue.type != UE_SPORT: if ue.type != UE_SPORT:
if self.prefs["bul_show_ue_rangs"]: if self.prefs["bul_show_ue_rangs"]:
rangs, effectif = res.ue_rangs[ue.id] rangs, effectif = res.ue_rangs[ue.id]
@ -277,11 +280,17 @@ class BulletinBUT:
"numero": formsemestre.semestre_id, "numero": formsemestre.semestre_id,
"inscription": "", # inutilisé mais nécessaire pour le js de Seb. "inscription": "", # inutilisé mais nécessaire pour le js de Seb.
"groupes": [], # XXX TODO "groupes": [], # XXX TODO
"absences": { }
if self.prefs["bul_show_abs"]:
semestre_infos["absences"] = {
"injustifie": nbabs - nbabsjust, "injustifie": nbabs - nbabsjust,
"total": nbabs, "total": nbabs,
}, }
} decisions_ues = self.res.get_etud_decision_ues(etud.id) or {}
if self.prefs["bul_show_ects"]:
ects_tot = sum([ue.ects or 0 for ue in res.ues]) if res.ues else 0.0
ects_acquis = sum([d.get("ects", 0) for d in decisions_ues.values()])
semestre_infos["ECTS"] = {"acquis": ects_acquis, "total": ects_tot}
semestre_infos.update( semestre_infos.update(
sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id) sco_bulletins_json.dict_decision_jury(etud.id, formsemestre.id)
) )
@ -307,7 +316,9 @@ class BulletinBUT:
), ),
"saes": self.etud_mods_results(etud, res.saes, version=version), "saes": self.etud_mods_results(etud, res.saes, version=version),
"ues": { "ues": {
ue.acronyme: self.etud_ue_results(etud, ue) ue.acronyme: self.etud_ue_results(
etud, ue, decision_ue=decisions_ues.get(ue.id, {})
)
for ue in res.ues for ue in res.ues
# si l'UE comporte des modules auxquels on est inscrit: # si l'UE comporte des modules auxquels on est inscrit:
if ( if (

View File

@ -65,6 +65,9 @@ class ResultatsSemestre(ResultatsCache):
self.moyennes_matieres = {} self.moyennes_matieres = {}
"""Moyennes de matières, si calculées. { matiere_id : Series, index etudid }""" """Moyennes de matières, si calculées. { matiere_id : Series, index etudid }"""
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, formsemestre='{self.formsemestre}')>"
def compute(self): def compute(self):
"Charge les notes et inscriptions et calcule toutes les moyennes" "Charge les notes et inscriptions et calcule toutes les moyennes"
# voir ce qui est chargé / calculé ici et dans les sous-classes # voir ce qui est chargé / calculé ici et dans les sous-classes
@ -177,7 +180,6 @@ class ResultatsSemestre(ResultatsCache):
if not self.validations: if not self.validations:
self.validations = res_sem.load_formsemestre_validations(self.formsemestre) self.validations = res_sem.load_formsemestre_validations(self.formsemestre)
ue_capitalisees = self.validations.ue_capitalisees ue_capitalisees = self.validations.ue_capitalisees
ue_by_code = {}
for etudid in ue_capitalisees.index: for etudid in ue_capitalisees.index:
recompute_mg = False recompute_mg = False
# ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"]) # ue_codes = set(ue_capitalisees.loc[etudid]["ue_code"])

View File

@ -161,7 +161,6 @@ class FormSemestre(db.Model):
d["periode"] = 1 # typiquement, début en septembre: S1, S3... d["periode"] = 1 # typiquement, début en septembre: S1, S3...
else: else:
d["periode"] = 2 # typiquement, début en février: S2, S4... d["periode"] = 2 # typiquement, début en février: S2, S4...
d["titre_num"] = self.titre_num()
d["titreannee"] = self.titre_annee() d["titreannee"] = self.titre_annee()
d["mois_debut"] = self.mois_debut() d["mois_debut"] = self.mois_debut()
d["mois_fin"] = self.mois_fin() d["mois_fin"] = self.mois_fin()
@ -174,7 +173,6 @@ class FormSemestre(db.Model):
d["session_id"] = self.session_id() d["session_id"] = self.session_id()
d["etapes"] = self.etapes_apo_vdi() d["etapes"] = self.etapes_apo_vdi()
d["etapes_apo_str"] = self.etapes_apo_str() d["etapes_apo_str"] = self.etapes_apo_str()
d["responsables"] = [u.id for u in self.responsables] # liste des ids
return d return d
def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery: def query_ues(self, with_sport=False) -> flask_sqlalchemy.BaseQuery:
@ -302,6 +300,10 @@ class FormSemestre(db.Model):
else: else:
return ", ".join([u.get_nomcomplet() for u in self.responsables]) return ", ".join([u.get_nomcomplet() for u in self.responsables])
def est_responsable(self, user):
"True si l'user est l'un des responsables du semestre"
return user.id in [u.id for u in self.responsables]
def annee_scolaire_str(self): def annee_scolaire_str(self):
"2021 - 2022" "2021 - 2022"
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)

View File

@ -51,10 +51,11 @@ Chaque semestre peut si nécessaire utiliser un type de bulletin différent.
""" """
import io import io
import pprint
import pydoc
import re import re
import time import time
import traceback import traceback
import pydoc
from flask import g, request from flask import g, request
@ -140,7 +141,11 @@ def process_field(field, cdict, style, suppress_empty_pars=False, format="pdf"):
cdict cdict
) # note that None values are mapped to empty strings ) # note that None values are mapped to empty strings
except: except:
log("process_field: invalid format=%s" % field) log(
f"""process_field: invalid format. field={field!r}
values={pprint.pformat(cdict)}
"""
)
text = ( text = (
"<para><i>format invalide !</i></para><para>" "<para><i>format invalide !</i></para><para>"
+ traceback.format_exc() + traceback.format_exc()

View File

@ -1172,7 +1172,7 @@ class BasePreferences(object):
"bul_show_abs", # ex "gestion_absence" "bul_show_abs", # ex "gestion_absence"
{ {
"initvalue": 1, "initvalue": 1,
"title": "Indiquer les absences sous les bulletins", "title": "Indiquer les absences dans les bulletins",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"category": "bul", "category": "bul",
"labels": ["non", "oui"], "labels": ["non", "oui"],

View File

@ -175,11 +175,24 @@ section>div:nth-child(1){
.ue .rang{ .ue .rang{
font-weight: 400; font-weight: 400;
} }
.absencesRecap {
align-items: baseline;
}
.absencesRecap > div:nth-child(2n) {
font-weight: normal;
}
.abs {
font-weight: bold;
}
.decision{ .decision{
margin: 5px 0; margin: 5px 0;
font-weight: bold; font-weight: bold;
font-size: 20px; font-size: 20px;
text-decoration: underline var(--couleurIntense); }
#ects_tot {
margin-left: 8px;
font-weight: bold;
font-size: 20px;
} }
.enteteSemestre{ .enteteSemestre{
color: black; color: black;

View File

@ -2152,6 +2152,18 @@ div.eval_description {
padding: 3px; padding: 3px;
} }
div.bul_foot {
max-width: 1000px;
background: #FFE7D5;
border-radius: 16px;
border: 1px solid #AAA;
padding: 16px 32px;
margin: auto;
}
div.bull_appreciations {
border-left: 1px solid black;
padding-left: 5px;
}
/* Saisie des notes */ /* Saisie des notes */
div.saisienote_etape1 { div.saisienote_etape1 {

View File

@ -83,7 +83,7 @@ class releveBUT extends HTMLElement {
<div> <div>
<div class=infoSemestre></div> <div class=infoSemestre></div>
<div> <div>
<div class=decision></div> <div><span class=decision></span><span class="ects" id="ects_tot"></span></div>
<div class=dateInscription>Inscrit le </div> <div class=dateInscription>Inscrit le </div>
<em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em> <em>Les moyennes ci-dessus servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
</div> </div>
@ -204,9 +204,10 @@ class releveBUT extends HTMLElement {
<div>Min. promo. :</div><div>${data.semestre.notes.min}</div> <div>Min. promo. :</div><div>${data.semestre.notes.min}</div>
</div> </div>
<div class=absencesRecap> <div class=absencesRecap>
<div class=enteteSemestre>Absences</div> <div class=enteteSemestre>Absences</div><div class=enteteSemestre>1/2 jour.</div>
<div class=enteteSemestre>N.J. ${data.semestre.absences?.injustifie ?? "-"}</div> <div class=abs>Non justifiées</div>
<div style="grid-column: 2">Total ${data.semestre.absences?.total ?? "-"}</div> <div>${data.semestre.absences?.injustifie ?? "-"}</div>
<div class=abs>Total</div><div>${data.semestre.absences?.total ?? "-"}</div>
</div> </div>
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a> <a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a>
`; `;
@ -223,7 +224,8 @@ class releveBUT extends HTMLElement {
}).join("") }).join("")
}*/ }*/
this.shadow.querySelector(".infoSemestre").innerHTML = output; this.shadow.querySelector(".infoSemestre").innerHTML = output;
this.shadow.querySelector(".decision").innerHTML = data.semestre.decision?.code || ""; this.shadow.querySelector(".decision").innerHTML = "Décision jury: " + (data.semestre.decision?.code || "");
this.shadow.querySelector("#ects_tot").innerHTML = "ECTS&nbsp;:&nbsp;" + (data.semestre.ECTS?.acquis || "-") + "&nbsp;/&nbsp;" + (data.semestre.ECTS?.total || "-");
} }
/*******************************/ /*******************************/
@ -256,7 +258,7 @@ class releveBUT extends HTMLElement {
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;- Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0} Malus&nbsp;:&nbsp;${dataUE.malus || 0}
<span class=ects>&nbsp;- <span class=ects>&nbsp;-
ECTS&nbsp;:&nbsp;${dataUE.ECTS.acquis}&nbsp;/&nbsp;${dataUE.ECTS.total} ECTS&nbsp;:&nbsp;${dataUE.ECTS?.acquis || "-"}&nbsp;/&nbsp;${dataUE.ECTS?.total || "-"}
</span> </span>
</div> </div>
</div>`; </div>`;
@ -377,9 +379,9 @@ class releveBUT extends HTMLElement {
setOptions(options) { setOptions(options) {
Object.entries(options).forEach(([option, value]) => { Object.entries(options).forEach(([option, value]) => {
if (value === false) { if (value === false) {
document.body.classList.add(option.replace("show", "hide")) this.shadow.children[0].classList.add(option.replace("show", "hide"));
} }
}) });
} }

View File

@ -1,28 +1,54 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
{# Pied des bulletins HTML #} {# Pied des bulletins HTML #}
<p>Situation actuelle: <div class="bul_foot">
{% if inscription_courante %} <div>
<a class="stdlink" href="{{url_for( <p>Situation actuelle:
"notes.formsemestre_status", {% if inscription_courante %}
scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{{url_for(
formsemestre_id=inscription_courante.formsemestre_id) "notes.formsemestre_status",
}}">{{inscription_str}}</a> scodoc_dept=g.scodoc_dept,
{% else %} formsemestre_id=inscription_courante.formsemestre_id)
{{inscription_str}} }}">{{inscription_str}}</a>
{% endif %} {% else %}
</p> {{inscription_str}}
{% endif %}
</p>
{% if formsemestre.modalite == "EXT" %} <div class="bull_appreciations">
<p><a href="{{ <h3>Appréciations</h3>
url_for('notes.formsemestre_ext_edit_ue_validations', {% for app in appreciations %}
scodoc_dept=g.scodoc_dept, <p><span class="bull_appreciations_date">{{app.date}}</span>{{
formsemestre_id=formsemestre.id, app.comment}}<span
etudid=etud.id)}}" class="bull_appreciations_link">{% if can_edit_appreciations %}<a
class="stdlink"> class="stdlink" href="{{url_for('notes.appreciation_add_form',
Éditer les validations d'UE dans ce semestre extérieur scodoc_dept=g.scodoc_dept, id=app.id)}}">modifier</a>
</a></p> <a class="stdlink" href="{{url_for('notes.appreciation_add_form',
{% endif %} scodoc_dept=g.scodoc_dept, id=app.id, suppress=1)}}">supprimer</a>{% endif %}
</span>
</p>
{% endfor %}
{% if can_edit_appreciations %}
<p><a class="stdlink" href="{{url_for(
'notes.appreciation_add_form', scodoc_dept=g.scodoc_dept,
etudid=etud.id, formsemestre_id=formsemestre_id)
}}">Ajouter une appréciation</a>
</p>
{% endif %}
</div>
{% if formsemestre.modalite == "EXT" %}
<p><a href="{{
url_for('notes.formsemestre_ext_edit_ue_validations',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id)}}"
class="stdlink">
Éditer les validations d'UE dans ce semestre extérieur
</a></p>
{% endif %}
</div>
</div>
{# Place du diagramme radar #} {# Place du diagramme radar #}
<form id="params"> <form id="params">

View File

@ -5,7 +5,7 @@
<div class="formsemestre_page_title"> <div class="formsemestre_page_title">
<div class="infos"> <div class="infos">
<span class="semtitle"><a class="stdlink" <span class="semtitle"><a class="stdlink"
title="{{formsemestre.session_id}}" title="{{formsemestre.session_id()}}"
href="{{url_for('notes.formsemestre_status', href="{{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}" scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}"
>{{formsemestre.titre}}</a> >{{formsemestre.titre}}</a>

View File

@ -323,6 +323,9 @@ def formsemestre_bulletinetud(
elif format == "html": elif format == "html":
return render_template( return render_template(
"but/bulletin.html", "but/bulletin.html",
appreciations=models.BulAppreciations.query.filter_by(
etudid=etudid, formsemestre_id=formsemestre.id
).order_by(models.BulAppreciations.date),
bul_url=url_for( bul_url=url_for(
"notes.formsemestre_bulletinetud", "notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
@ -332,6 +335,8 @@ def formsemestre_bulletinetud(
force_publishing=1, # pour ScoDoc lui même force_publishing=1, # pour ScoDoc lui même
version=version, version=version,
), ),
can_edit_appreciations=formsemestre.est_responsable(current_user)
or (current_user.has_permission(Permission.ScoEtudInscrit)),
etud=etud, etud=etud,
formsemestre=formsemestre, formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(), inscription_courante=etud.inscription_courante(),