Page bilan ECTS etudiant toutes formations

This commit is contained in:
Emmanuel Viennet 2024-07-10 00:53:09 +02:00
parent d4fd6527e5
commit abb6907a5d
12 changed files with 206 additions and 66 deletions

View File

@ -18,6 +18,7 @@ from collections.abc import Iterable
from operator import attrgetter
from flask import g, url_for
from flask_sqlalchemy.query import Query
from app import db, log
from app.comp.res_but import ResultatsSemestreBUT
@ -393,6 +394,26 @@ def but_ects_valides(
et ne les compte qu'une fois même en cas de redoublement avec re-validation.
Si annees_but est spécifié, un iterable "BUT1, "BUT2" par exemple, ne prend que ces années.
"""
validations = but_validations_ues(etud, referentiel_competence_id, annees_but)
ects_dict = {}
for v in validations:
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
if v.code in CODES_UE_VALIDES:
ects_dict[key] = v.ue.ects
return int(sum(ects_dict.values())) if ects_dict else 0
def but_validations_ues(
etud: Identite,
referentiel_competence_id: int,
annees_but: None | Iterable[str] = None,
) -> Query:
"""Query les validations d'UEs pour cet étudiant
dans des UEs appartenant à ce référentiel de compétence
et en option pour les années BUT indiquées.
annees_but : None (tout) ou liste [ "BUT1", ... ]
"""
validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None)
@ -403,18 +424,10 @@ def but_ects_valides(
if annees_but:
validations = validations.filter(ApcNiveau.annee.in_(annees_but))
# Et restreint au référentiel de compétence:
validations = validations.join(ApcCompetence).filter_by(
return validations.join(ApcCompetence).filter_by(
referentiel_id=referentiel_competence_id
)
ects_dict = {}
for v in validations:
key = (v.ue.semestre_idx, v.ue.niveau_competence.id)
if v.code in CODES_UE_VALIDES:
ects_dict[key] = v.ue.ects
return int(sum(ects_dict.values())) if ects_dict else 0
def etud_ues_de_but1_non_validees(
etud: Identite, formation: Formation, parcour: ApcParcours

View File

@ -845,11 +845,11 @@ class FormSemestre(models.ScoDocModel):
else:
return ", ".join([u.get_nomcomplet() for u in self.responsables])
def est_responsable(self, user: User):
def est_responsable(self, user: User) -> bool:
"True si l'user est l'un des responsables du semestre"
return user.id in [u.id for u in self.responsables]
def est_chef_or_diretud(self, user: User = None):
def est_chef_or_diretud(self, user: User | None = None) -> bool:
"Vrai si utilisateur (par def. current) est admin, chef dept ou responsable du semestre"
user = user or current_user
return user.has_permission(Permission.EditFormSemestre) or self.est_responsable(
@ -867,7 +867,7 @@ class FormSemestre(models.ScoDocModel):
return True # typiquement admin, chef dept
return self.est_responsable(user)
def can_edit_jury(self, user: User = None):
def can_edit_jury(self, user: User | None = None):
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
dans ce semestre: vérifie permission et verrouillage.
"""

View File

@ -2,12 +2,15 @@
"""Notes, décisions de jury
"""
from flask_sqlalchemy.query import Query
from app import db
from app import log
from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN
from app.models.events import Scolog
from app.models.formations import Formation
from app.models.ues import UniteEns
from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import CODES_UE_VALIDES
@ -113,6 +116,7 @@ class ScolarFormSemestreValidation(db.Model):
if self.ue.parcours else ""}
{("émise par " + link)}
: <b>{self.code}</b>{moyenne}
<b>{self.ue.ects:g} ECTS</b>
le {self.event_date.strftime(scu.DATEATIME_FMT)}
"""
else:
@ -131,6 +135,27 @@ class ScolarFormSemestreValidation(db.Model):
else 0.0
)
@classmethod
def validations_ues(
cls, etud: "Identite", formation_code: str | None = None
) -> Query:
"""Query les validations d'UE pour cet étudiant dans des UEs de formations
du code indiqué, ou toutes si le formation_code est None.
"""
from app.models.formsemestre import FormSemestre
query = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None)
.join(UniteEns)
.join(FormSemestre, ScolarFormSemestreValidation.formsemestre)
)
if formation_code is not None:
query = query.join(Formation).filter_by(formation_code=formation_code)
return query.order_by(
FormSemestre.semestre_id, UniteEns.numero, UniteEns.acronyme
)
class ScolarAutorisationInscription(db.Model):
"""Autorisation d'inscription dans un semestre"""

View File

@ -31,7 +31,7 @@ import time
import flask
from flask import url_for, flash, g, request
from flask_login import current_user
from flask.templating import render_template
import sqlalchemy as sa
from app.models import Identite, Evaluation
@ -64,7 +64,6 @@ from app.scodoc import sco_cursus_dut
from app.scodoc.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict
from app.scodoc.sco_permissions import Permission
# ------------------------------------------------------------------------------------
@ -1249,7 +1248,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
<p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
</div>
{_get_etud_ue_cap_html(etud, formsemestre)}
{_get_etud_ue_validations_html(etud, formsemestre)}
<div class="scobox">
<div class="scobox-title">
@ -1300,7 +1299,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
return flask.redirect(dest_url)
def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
def _get_etud_ue_validations_html(etud: Identite, formsemestre: FormSemestre) -> str:
"""HTML listant les validations d'UEs pour cet étudiant dans des formations de même
code que celle du formsemestre indiqué.
"""
@ -1319,39 +1318,13 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
if not validations:
return ""
H = [
f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
<div class="sco_box_title">Validations d'UEs dans cette formation</div>
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()},
sur des semestres ou déclarées comme "antérieures" (externes).
</div>
<ul class="liste_validations">"""
]
for validation in validations:
if validation.formsemestre_id is None:
origine = " enregistrée d'un parcours antérieur (hors ScoDoc)"
else:
origine = f", du semestre {formsemestre.html_link_status()}"
if validation.semestre_id is not None:
origine += f" (<b>S{validation.semestre_id}</b>)"
H.append(f"""<li>{validation.html()}""")
if (validation.formsemestre and validation.formsemestre.can_edit_jury()) or (
current_user and current_user.has_permission(Permission.EtudInscrit)
):
H.append(
f"""
<form class="inline-form">
<button
data-v_id="{validation.id}" data-type="validation_ue" data-etudid="{etud.id}"
>effacer</button>
</form>
""",
)
else:
H.append(scu.icontag("lock_img", border="0", title="Semestre verrouillé"))
H.append("</li>")
H.append("</ul></div>")
return "\n".join(H)
return render_template(
"jury/ue_list_etud_validations.j2",
edit_mode=True,
etud=etud,
titre_boite="Validations d'UEs dans cette formation",
validations=validations,
)
def do_formsemestre_validate_previous_ue(

View File

@ -121,14 +121,14 @@ def _menu_scolarite(
"enabled": def_enabled,
},
{
"title": "Inscrire à un module optionnel (ou au sport)",
"endpoint": "notes.formsemestre_inscription_option",
"title": "Désinscrire (en cas d'erreur)",
"endpoint": "notes.formsemestre_desinscription",
"args": args,
"enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
},
{
"title": "Désinscrire (en cas d'erreur)",
"endpoint": "notes.formsemestre_desinscription",
"title": "Inscrire à un module optionnel (ou au sport)",
"endpoint": "notes.formsemestre_inscription_option",
"args": args,
"enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
},
@ -138,12 +138,6 @@ def _menu_scolarite(
"args": args,
"enabled": formsemestre.can_edit_jury(),
},
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
"args": {"etudid": etudid},
"enabled": authuser.has_permission(Permission.EtudInscrit),
},
{
"title": "Enregistrer un semestre effectué ailleurs",
"endpoint": "notes.formsemestre_ext_create_form",
@ -156,6 +150,12 @@ def _menu_scolarite(
"args": args,
"enabled": authuser.has_permission(Permission.EditAllNotes),
},
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
"args": {"etudid": etudid},
"enabled": authuser.has_permission(Permission.EtudInscrit),
},
]
return htmlutils.make_menu(
@ -317,6 +317,12 @@ def fiche_etud(etudid=None):
else:
info["link_inscrire_ailleurs"] = ""
info[
"link_bilan_ects"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.etud_bilan_ects",
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">ECTS</a></span>"""
else:
# non inscrit
l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
@ -331,6 +337,7 @@ def fiche_etud(etudid=None):
info["liste_inscriptions"] = "\n".join(l)
info["link_bul_pdf"] = ""
info["link_inscrire_ailleurs"] = ""
info["link_bilan_ects"] = ""
# Liste des annotations
html_annotations_list = "\n".join(
@ -433,7 +440,9 @@ def fiche_etud(etudid=None):
"inscriptions_mkup"
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
<div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
{info["link_bul_pdf"]}
{info["link_inscrire_ailleurs"]}
{info["link_bilan_ects"]}
</div>"""
#

View File

@ -246,7 +246,7 @@ PREF_CATEGORIES = (
"bul_margins",
{
"title": "Marges additionnelles des bulletins, en millimètres",
"subtitle": """Le bulletin de notes notes est toujours redimensionné
"subtitle": """Le bulletin de notes classique (pas BUT) est toujours redimensionné
pour occuper l'espace disponible entre les marges.
""",
"related": ("bul", "bul_mail", "pdf"),

View File

@ -9,4 +9,10 @@ span.parcours {
div.ue_list_etud_validations ul.liste_validations li {
margin-bottom: 8px;
}
div.ue_list_etud_validations div.total_ects {
font-weight: bold;
margin-top: 16px;
margin-bottom: 12px;
}

View File

@ -0,0 +1,24 @@
{% extends "sco_page.j2" %}
{% block styles %}
{{super()}}
<link href="{{scu.STATIC_DIR}}/css/jury_delete_manual.css" rel="stylesheet" type="text/css" />
{% endblock %}
{% block app_content %}
<h1>Bilan des ECTS de {{etud.html_link_fiche()|safe}}</h1>
<div class="help">
Cette page donne toutes les UEs acquises par l'étudiant (codes <tt>ADM, ADJ, ADJR, ADSUP, CMP...</tt>)
dans chaque formation qu'il a suivi.
</div>
{% for diplome in formsemestre_by_diplome %}
{% set titre_boite = "Validations d'UEs dans la formation " + titre_by_diplome[diplome] %}
{% set validations = validations_by_diplome[diplome] %}
{% set total_ects = ects_by_diplome[diplome] %}
{% include "jury/ue_list_etud_validations.j2" %}
{% endfor %}
{% endblock app_content %}

View File

@ -0,0 +1,31 @@
{# Fragment de html pour cadre affichage validations d'une formation #}
<div class="sco_box sco_lightgreen_bg ue_list_etud_validations">
<div class="sco_box_title">{{titre_boite}}</div>
<div class="help">Liste de toutes les UEs validées par {{etud.html_link_fiche()|safe}},
sur des semestres ou déclarées comme "antérieures" (externes).
</div>
<ul class="liste_validations">
{% for validation in validations %}
<li>{{ validation.html() | safe }}
{% if edit_mode %}
{% if validation.formsemestre and validation.formsemestre.can_edit_jury() %}
<form class="inline-form">
<button data-v_id="{{validation.id}}" data-type="validation_ue" data-etudid="{{etud.id}}">
effacer
</button>
</form>
{% else %}
{{ scu.icontag("lock_img", border="0", title="Semestre verrouillé") }}
{% endif %}
{% endif %}
</li>
{% endfor %}
</ul>
{% if total_ects %}
<div class="total_ects">
Total ECTS: {{ "%g" % total_ects }}
</div>
{% endif %}
</div>

View File

@ -27,7 +27,7 @@ Vues sur les jurys et validations
Emmanuel Viennet, 2024
"""
from collections import defaultdict
import datetime
import flask
from flask import flash, g, redirect, render_template, request, url_for
@ -55,6 +55,7 @@ from app.models import (
FormSemestreInscription,
Identite,
ScolarAutorisationInscription,
ScolarFormSemestreValidation,
ScolarNews,
ScoDocSiteConfig,
)
@ -66,6 +67,7 @@ from app.scodoc import (
sco_formsemestre_validation,
sco_preferences,
)
from app.scodoc.codes_cursus import CODES_UE_VALIDES
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import (
ScoPermissionDenied,
@ -78,7 +80,6 @@ from app.scodoc.sco_pv_dict import descr_autorisations
from app.views import notes_bp as bp
from app.views import ScoData
# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES
@ -902,3 +903,60 @@ def jury_delete_manual(etudid: int):
"""Efface toute les décisions d'une année pour cet étudiant"""
etud = Identite.get_etud(etudid)
return jury_edit_manual.jury_delete_manual(etud)
@bp.route("/etud_bilan_ects/<int:etudid>")
@scodoc
@permission_required(Permission.ScoView)
def etud_bilan_ects(etudid: int):
"""Page bilan de tous els ECTS acquis par un étudiant.
Plusieurs formations (eg DUT, LP) peuvent être concernées.
"""
etud = Identite.get_etud(etudid)
# Cherche les formations différentes (au sens des ECTS)
# suivies par l'étudiant: regroupe ses formsemestres
# diplome est la clé: en classique le code formation, en BUT le referentiel_competence_id
formsemestre_by_diplome = defaultdict(list)
for formsemestre in etud.get_formsemestres(recent_first=True):
diplome = (
formsemestre.formation.referentiel_competence.id
if (
formsemestre.formation.is_apc()
and formsemestre.formation.referentiel_competence
)
else formsemestre.formation.formation_code
)
formsemestre_by_diplome[diplome].append(formsemestre)
# Pour chaque liste de formsemestres d'un même "diplôme"
# liste les UE validées avec leurs ECTS
ects_by_diplome = {}
titre_by_diplome = {} # { diplome : titre }
validations_by_diplome = {} # { diplome : query validations UEs }
for diplome, formsemestres in formsemestre_by_diplome.items():
formsemestre = formsemestres[0]
titre_by_diplome[diplome] = formsemestre.formation.get_titre_version()
if formsemestre.formation.is_apc():
validations = cursus_but.but_validations_ues(etud, diplome)
else:
validations = ScolarFormSemestreValidation.validations_ues(
etud, formsemestre.formation.formation_code
)
validations_by_diplome[diplome] = [
validation
for validation in validations
if validation.code in CODES_UE_VALIDES
]
ects_by_diplome[diplome] = sum(
(validation.ue.ects or 0.0)
for validation in validations_by_diplome[diplome]
)
return render_template(
"jury/etud_bilan_ects.j2",
etud=etud,
ects_by_diplome=ects_by_diplome,
formsemestre_by_diplome=formsemestre_by_diplome,
titre_by_diplome=titre_by_diplome,
validations_by_diplome=validations_by_diplome,
)

View File

@ -56,9 +56,9 @@ cli.register(app)
@app.context_processor
def inject_sco_utils():
"Make scu and sco available in all Jinja templates"
"Make Permission, sco and scu available in all Jinja templates"
# if modified, put the same in conftest.py#27
return {"scu": scu, "sco": ScoData()}
return {"Permission": Permission, "scu": scu, "sco": ScoData()}
@app.shell_context_processor

View File

@ -11,6 +11,7 @@ from app import models
from app.auth.models import User, Role
from app.auth.models import get_super_admin
from app.scodoc import notesdb as ndb
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu
from app.views import ScoData
@ -28,7 +29,7 @@ def test_client():
@apptest.context_processor
def inject_sco_utils():
"Make scu available in all Jinja templates"
return {"scu": scu, "sco": ScoData()}
return {"Permission": Permission, "scu": scu, "sco": ScoData()}
with apptest.test_request_context():
# initialize scodoc "g":