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 operator import attrgetter
from flask import g, url_for from flask import g, url_for
from flask_sqlalchemy.query import Query
from app import db, log from app import db, log
from app.comp.res_but import ResultatsSemestreBUT 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. 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. 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 = ( validations = (
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id) ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
.filter(ScolarFormSemestreValidation.ue_id != None) .filter(ScolarFormSemestreValidation.ue_id != None)
@ -403,18 +424,10 @@ def but_ects_valides(
if annees_but: if annees_but:
validations = validations.filter(ApcNiveau.annee.in_(annees_but)) validations = validations.filter(ApcNiveau.annee.in_(annees_but))
# Et restreint au référentiel de compétence: # 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 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( def etud_ues_de_but1_non_validees(
etud: Identite, formation: Formation, parcour: ApcParcours etud: Identite, formation: Formation, parcour: ApcParcours

View File

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

View File

@ -2,12 +2,15 @@
"""Notes, décisions de jury """Notes, décisions de jury
""" """
from flask_sqlalchemy.query import Query
from app import db from app import db
from app import log from app import log
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models.events import Scolog 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_cache
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.codes_cursus import CODES_UE_VALIDES from app.scodoc.codes_cursus import CODES_UE_VALIDES
@ -113,6 +116,7 @@ class ScolarFormSemestreValidation(db.Model):
if self.ue.parcours else ""} if self.ue.parcours else ""}
{("émise par " + link)} {("émise par " + link)}
: <b>{self.code}</b>{moyenne} : <b>{self.code}</b>{moyenne}
<b>{self.ue.ects:g} ECTS</b>
le {self.event_date.strftime(scu.DATEATIME_FMT)} le {self.event_date.strftime(scu.DATEATIME_FMT)}
""" """
else: else:
@ -131,6 +135,27 @@ class ScolarFormSemestreValidation(db.Model):
else 0.0 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): class ScolarAutorisationInscription(db.Model):
"""Autorisation d'inscription dans un semestre""" """Autorisation d'inscription dans un semestre"""

View File

@ -31,7 +31,7 @@ import time
import flask import flask
from flask import url_for, flash, g, request from flask import url_for, flash, g, request
from flask_login import current_user from flask.templating import render_template
import sqlalchemy as sa import sqlalchemy as sa
from app.models import Identite, Evaluation 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.sco_cursus_dut import etud_est_inscrit_ue
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pv_dict 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> <p>On ne peut valider ici que les UEs du cursus <b>{formation.titre}</b></p>
</div> </div>
{_get_etud_ue_cap_html(etud, formsemestre)} {_get_etud_ue_validations_html(etud, formsemestre)}
<div class="scobox"> <div class="scobox">
<div class="scobox-title"> <div class="scobox-title">
@ -1300,7 +1299,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
return flask.redirect(dest_url) 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 """HTML listant les validations d'UEs pour cet étudiant dans des formations de même
code que celle du formsemestre indiqué. code que celle du formsemestre indiqué.
""" """
@ -1319,39 +1318,13 @@ def _get_etud_ue_cap_html(etud: Identite, formsemestre: FormSemestre) -> str:
if not validations: if not validations:
return "" return ""
H = [ return render_template(
f"""<div class="sco_box sco_lightgreen_bg ue_list_etud_validations"> "jury/ue_list_etud_validations.j2",
<div class="sco_box_title">Validations d'UEs dans cette formation</div> edit_mode=True,
<div class="help">Liste de toutes les UEs validées par {etud.html_link_fiche()}, etud=etud,
sur des semestres ou déclarées comme "antérieures" (externes). titre_boite="Validations d'UEs dans cette formation",
</div> validations=validations,
<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)
def do_formsemestre_validate_previous_ue( def do_formsemestre_validate_previous_ue(

View File

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

View File

@ -246,7 +246,7 @@ PREF_CATEGORIES = (
"bul_margins", "bul_margins",
{ {
"title": "Marges additionnelles des bulletins, en millimètres", "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. pour occuper l'espace disponible entre les marges.
""", """,
"related": ("bul", "bul_mail", "pdf"), "related": ("bul", "bul_mail", "pdf"),

View File

@ -10,3 +10,9 @@ span.parcours {
div.ue_list_etud_validations ul.liste_validations li { div.ue_list_etud_validations ul.liste_validations li {
margin-bottom: 8px; 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 Emmanuel Viennet, 2024
""" """
from collections import defaultdict
import datetime import datetime
import flask import flask
from flask import flash, g, redirect, render_template, request, url_for from flask import flash, g, redirect, render_template, request, url_for
@ -55,6 +55,7 @@ from app.models import (
FormSemestreInscription, FormSemestreInscription,
Identite, Identite,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarFormSemestreValidation,
ScolarNews, ScolarNews,
ScoDocSiteConfig, ScoDocSiteConfig,
) )
@ -66,6 +67,7 @@ from app.scodoc import (
sco_formsemestre_validation, sco_formsemestre_validation,
sco_preferences, sco_preferences,
) )
from app.scodoc.codes_cursus import CODES_UE_VALIDES
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
ScoPermissionDenied, 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 notes_bp as bp
from app.views import ScoData from app.views import ScoData
# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES # --- 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""" """Efface toute les décisions d'une année pour cet étudiant"""
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)
return jury_edit_manual.jury_delete_manual(etud) 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 @app.context_processor
def inject_sco_utils(): 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 # 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 @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 User, Role
from app.auth.models import get_super_admin from app.auth.models import get_super_admin
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc.sco_permissions import Permission
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.views import ScoData from app.views import ScoData
@ -28,7 +29,7 @@ def test_client():
@apptest.context_processor @apptest.context_processor
def inject_sco_utils(): def inject_sco_utils():
"Make scu available in all Jinja templates" "Make scu available in all Jinja templates"
return {"scu": scu, "sco": ScoData()} return {"Permission": Permission, "scu": scu, "sco": ScoData()}
with apptest.test_request_context(): with apptest.test_request_context():
# initialize scodoc "g": # initialize scodoc "g":