forked from ScoDoc/ScoDoc
Nouvelle page de visu/saisie des décisions RCUEs: validation_rcues
This commit is contained in:
parent
24bb78bdfd
commit
f08a4130dd
141
app/api/jury.py
141
app/api/jury.py
@ -8,25 +8,32 @@
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||
"""
|
||||
|
||||
from flask import g, url_for
|
||||
import datetime
|
||||
|
||||
from flask import flash, g, request, url_for
|
||||
from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, tools
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.but import jury_but_results
|
||||
from app.models import (
|
||||
ApcParcours,
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
@ -161,6 +168,136 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/record",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_record(etudid: int):
|
||||
"""Enregistre une validation de RCUE.
|
||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"code" : str,
|
||||
"ue1_id" : int,
|
||||
"ue2_id" : int,
|
||||
// Optionnel:
|
||||
"formsemestre_id" : int,
|
||||
"date" : date_iso, // si non spécifié, now()
|
||||
"parcours_id" :int,
|
||||
}
|
||||
"""
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return json_error(404, "étudiant inconnu")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
code = data.get("code")
|
||||
if code is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: code")
|
||||
if code not in codes_cursus.CODES_JURY_RCUE:
|
||||
return json_error(API_CLIENT_ERROR, "invalid code value")
|
||||
ue1_id = data.get("ue1_id")
|
||||
if ue1_id is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: ue1_id")
|
||||
try:
|
||||
ue1_id = int(ue1_id)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid value for ue1_id")
|
||||
ue2_id = data.get("ue2_id")
|
||||
if ue2_id is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing argument: ue2_id")
|
||||
try:
|
||||
ue2_id = int(ue2_id)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid value for ue2_id")
|
||||
formsemestre_id = data.get("formsemestre_id")
|
||||
date_validation_str = data.get("date", datetime.datetime.now().isoformat())
|
||||
parcours_id = data.get("parcours_id")
|
||||
#
|
||||
query = UniteEns.query.filter_by(id=ue1_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue1: UniteEns = query.first_or_404()
|
||||
query = UniteEns.query.filter_by(id=ue2_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue2: UniteEns = query.first_or_404()
|
||||
if ue1.niveau_competence_id != ue2.niveau_competence_id:
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "UEs non associees au meme niveau de competence"
|
||||
)
|
||||
if formsemestre_id is not None:
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
formsemestre: FormSemestre = query.first_or_404()
|
||||
if (formsemestre.formation_id != ue1.formation_id) or (
|
||||
formsemestre.formation_id != ue2.formation_id
|
||||
):
|
||||
return json_error(
|
||||
API_CLIENT_ERROR, "ues et semestre ne sont pas de la meme formation"
|
||||
)
|
||||
else:
|
||||
formsemestre = None
|
||||
try:
|
||||
date_validation = datetime.datetime.fromisoformat(date_validation_str)
|
||||
except ValueError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid date string")
|
||||
if parcours_id is not None:
|
||||
parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id)
|
||||
if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id:
|
||||
return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles")
|
||||
|
||||
# Une validation pour ce niveau de compétence existe-elle ?
|
||||
validation = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etudid)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.filter_by(niveau_competence_id=ue2.niveau_competence_id)
|
||||
.first()
|
||||
)
|
||||
if validation:
|
||||
validation.code = code
|
||||
validation.date = date_validation
|
||||
validation.formsemestre_id = formsemestre_id
|
||||
validation.parcours_id = parcours_id
|
||||
validation.ue1_id = ue1_id
|
||||
validation.ue2_id = ue2_id
|
||||
log(f"updating {validation}")
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Mise à jour {validation}",
|
||||
commit=False,
|
||||
)
|
||||
else:
|
||||
validation = ApcValidationRCUE(
|
||||
code=code,
|
||||
date=date_validation,
|
||||
etudid=etudid,
|
||||
formsemestre_id=formsemestre_id,
|
||||
parcours_id=parcours_id,
|
||||
ue1_id=ue1_id,
|
||||
ue2_id=ue2_id,
|
||||
)
|
||||
log(f"recording {validation}")
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Enregistrement {validation}",
|
||||
commit=False,
|
||||
)
|
||||
db.session.add(validation)
|
||||
db.session.commit()
|
||||
return validation.to_dict()
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
|
@ -282,7 +282,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoEtudChangeGroups)
|
||||
@as_json
|
||||
def group_create(partition_id: int):
|
||||
def group_create(partition_id: int): # partition-group-create
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
The request content type should be "application/json":
|
||||
|
@ -18,7 +18,7 @@ from operator import attrgetter
|
||||
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
|
||||
@ -41,7 +41,8 @@ from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES
|
||||
from app.scodoc.codes_cursus import code_ue_validant, CODES_UE_VALIDES, UE_STANDARD
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
|
||||
@ -479,3 +480,122 @@ def formsemestre_warning_apc_setup(
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def ue_associee_au_niveau_du_parcours(
|
||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||
) -> UniteEns:
|
||||
"L'UE associée à ce niveau, ou None"
|
||||
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
||||
if len(ues) > 1:
|
||||
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
||||
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
|
||||
if ues_pair_avec_parcours:
|
||||
ues = ues_pair_avec_parcours
|
||||
if len(ues) > 1:
|
||||
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
|
||||
return ues[0] if ues else None
|
||||
|
||||
|
||||
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
|
||||
"""
|
||||
[
|
||||
{
|
||||
'competence' : ApcCompetence,
|
||||
'niveaux' : {
|
||||
1 : { ... },
|
||||
2 : { ... },
|
||||
3 : {
|
||||
'niveau' : ApcNiveau,
|
||||
'ue_impair' : UniteEns, # actuellement associée
|
||||
'ues_impair' : list[UniteEns], # choix possibles
|
||||
'ue_pair' : UniteEns,
|
||||
'ues_pair' : list[UniteEns],
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
refcomp: ApcReferentielCompetences = formation.referentiel_competence
|
||||
|
||||
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
|
||||
"""niveau et ues pour cette compétence de cette année du parcours.
|
||||
Si parcour est None, les niveaux du tronc commun
|
||||
"""
|
||||
if parcour is not None:
|
||||
# L'étudiant est inscrit à un parcours: cherche les niveaux
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(
|
||||
parcour, annee, competence=competence
|
||||
)
|
||||
else:
|
||||
# sans parcours, on cherche les niveaux du Tronc Commun de cette année
|
||||
niveaux = [
|
||||
niveau
|
||||
for niveau in refcomp.get_niveaux_by_parcours(annee)[1]["TC"]
|
||||
if niveau.competence_id == competence.id
|
||||
]
|
||||
|
||||
if len(niveaux) > 0:
|
||||
if len(niveaux) > 1:
|
||||
log(
|
||||
f"""_niveau_ues: plus d'un niveau pour {competence}
|
||||
annee {annee} {("parcours " + parcour.code) if parcour else ""}"""
|
||||
)
|
||||
niveau = niveaux[0]
|
||||
elif len(niveaux) == 0:
|
||||
return {
|
||||
"niveau": None,
|
||||
"ue_pair": None,
|
||||
"ue_impair": None,
|
||||
"ues_pair": [],
|
||||
"ues_impair": [],
|
||||
}
|
||||
# Toutes les UEs de la formation dans ce parcours ou tronc commun
|
||||
ues = [
|
||||
ue
|
||||
for ue in formation.ues
|
||||
if (
|
||||
(not ue.parcours)
|
||||
or (parcour is not None and (parcour.id in (p.id for p in ue.parcours)))
|
||||
)
|
||||
and ue.type == UE_STANDARD
|
||||
]
|
||||
ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)]
|
||||
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
|
||||
|
||||
# UE associée au niveau dans ce parcours
|
||||
ue_pair = ue_associee_au_niveau_du_parcours(
|
||||
ues_pair_possibles, niveau, f"S{2*annee}"
|
||||
)
|
||||
ue_impair = ue_associee_au_niveau_du_parcours(
|
||||
ues_impair_possibles, niveau, f"S{2*annee-1}"
|
||||
)
|
||||
|
||||
return {
|
||||
"niveau": niveau,
|
||||
"ue_pair": ue_pair,
|
||||
"ues_pair": [
|
||||
ue
|
||||
for ue in ues_pair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
"ue_impair": ue_impair,
|
||||
"ues_impair": [
|
||||
ue
|
||||
for ue in ues_impair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
}
|
||||
|
||||
competences = [
|
||||
{
|
||||
"competence": competence,
|
||||
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
|
||||
}
|
||||
for competence in (
|
||||
parcour.query_competences()
|
||||
if parcour
|
||||
else refcomp.competences.order_by(ApcCompetence.numero)
|
||||
)
|
||||
]
|
||||
return competences
|
||||
|
115
app/but/validations_view.py
Normal file
115
app/but/validations_view.py
Normal file
@ -0,0 +1,115 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions (correction d'erreurs, parcours hors normes)
|
||||
|
||||
Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import log
|
||||
from app.but import cursus_but
|
||||
from app.models import (
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcReferentielCompetences,
|
||||
# ApcValidationAnnee, # TODO
|
||||
ApcValidationRCUE,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
UniteEns,
|
||||
# ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def validation_rcues(etud: Identite, formsemestre: FormSemestre, edit: bool = False):
|
||||
"""Page de saisie des décisions de RCUEs "antérieures"
|
||||
On peut l'utiliser pour saisir la validation de n'importe quel RCUE
|
||||
d'une année antérieure et de la formation du formsemestre indiqué.
|
||||
"""
|
||||
formation: Formation = formsemestre.formation
|
||||
refcomp = formation.referentiel_competence
|
||||
if refcomp is None:
|
||||
raise ScoNoReferentielCompetences(formation=formation)
|
||||
parcour = formsemestre.etuds_inscriptions[etud.id].parcour
|
||||
# Si non inscrit à un parcours, prend toutes les compétences
|
||||
competences_parcour = cursus_but.parcour_formation_competences(parcour, formation)
|
||||
|
||||
ue_validation_by_niveau = get_ue_validation_by_niveau(refcomp, etud)
|
||||
rcue_validation_by_niveau = get_rcue_validation_by_niveau(refcomp, etud)
|
||||
return render_template(
|
||||
"but/validation_rcues.j2",
|
||||
competences_parcour=competences_parcour,
|
||||
edit=edit,
|
||||
formation=formation,
|
||||
parcour=parcour,
|
||||
rcue_validation_by_niveau=rcue_validation_by_niveau,
|
||||
rcue_codes=sorted(codes_cursus.CODES_JURY_RCUE),
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
title=f"{formation.acronyme} - Niveaux et UEs",
|
||||
ue_validation_by_niveau=ue_validation_by_niveau,
|
||||
)
|
||||
|
||||
|
||||
def get_ue_validation_by_niveau(
|
||||
refcomp: ApcReferentielCompetences, etud: Identite
|
||||
) -> dict[tuple[int, str], ScolarFormSemestreValidation]:
|
||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
||||
"""
|
||||
validations: list[ScolarFormSemestreValidation] = (
|
||||
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=refcomp.id)
|
||||
.all()
|
||||
)
|
||||
# La meilleure validation pour chaque UE
|
||||
ue_validation_by_niveau = {} # { (niveau_id, pair|impair) : validation }
|
||||
for validation in validations:
|
||||
if validation.ue.niveau_competence is None:
|
||||
log(
|
||||
f"""validation_rcues: ignore validation d'UE {
|
||||
validation.ue.id} pas de niveau de competence"""
|
||||
)
|
||||
key = (
|
||||
validation.ue.niveau_competence.id,
|
||||
"impair" if validation.ue.semestre_idx % 2 else "pair",
|
||||
)
|
||||
existing = ue_validation_by_niveau.get(key, None)
|
||||
if (not existing) or (
|
||||
codes_cursus.BUT_CODES_ORDER[existing.code]
|
||||
< codes_cursus.BUT_CODES_ORDER[validation.code]
|
||||
):
|
||||
ue_validation_by_niveau[key] = validation
|
||||
return ue_validation_by_niveau
|
||||
|
||||
|
||||
def get_rcue_validation_by_niveau(
|
||||
refcomp: ApcReferentielCompetences, etud: Identite
|
||||
) -> dict[int, ApcValidationRCUE]:
|
||||
"""Les validations d'UEs de cet étudiant liées à ce référentiel de compétences.
|
||||
Pour chaque niveau / pair ou impair, choisi la "meilleure" validation
|
||||
"""
|
||||
validations: list[ApcValidationRCUE] = (
|
||||
ApcValidationRCUE.query.filter_by(etudid=etud.id)
|
||||
.join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id)
|
||||
.join(ApcNiveau, UniteEns.niveau_competence_id == ApcNiveau.id)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=refcomp.id)
|
||||
.all()
|
||||
)
|
||||
return {
|
||||
validation.ue2.niveau_competence.id: validation for validation in validations
|
||||
}
|
@ -204,6 +204,7 @@ CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
|
||||
CODES_UE_CAPITALISANTS = {ADM}
|
||||
"UE capitalisée"
|
||||
|
||||
CODES_JURY_RCUE = CODES_JURY_UE # tous les codes d'UEs sont utilisables pour les RCUEs
|
||||
CODES_RCUE_VALIDES_DE_DROIT = {ADM, CMP}
|
||||
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
||||
"Niveau RCUE validé"
|
||||
|
@ -313,9 +313,26 @@ def ficheEtud(etudid=None):
|
||||
)
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">tous les bulletins</a></span>"""
|
||||
] = f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">tous les bulletins</a>
|
||||
</span>
|
||||
"""
|
||||
last_formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, info["sems"][0]["formsemestre_id"]
|
||||
)
|
||||
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] += f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
|
||||
}">Visualiser les compétences BUT</a>
|
||||
</span>
|
||||
"""
|
||||
if authuser.has_permission(Permission.ScoEtudInscrit):
|
||||
info[
|
||||
"link_inscrire_ailleurs"
|
||||
|
@ -144,6 +144,10 @@ div.ue.pair {
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.rcue {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
/* ne fonctionne pas
|
||||
option.non_associe {
|
||||
background-color: yellow;
|
||||
@ -154,3 +158,8 @@ option.non_associe {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
select.validation_rcue {
|
||||
display: inline-block;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
218
app/templates/but/validation_rcues.j2
Normal file
218
app/templates/but/validation_rcues.j2
Normal file
@ -0,0 +1,218 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link href="{{scu.STATIC_DIR}}/css/refcomp_parcours_niveaux.css" rel="stylesheet" type="text/css" />
|
||||
<link href="{{scu.STATIC_DIR}}/css/parcour_formation.css" rel="stylesheet" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
{% macro show_ue(niv, sem="pair", sem_idx=0) -%}
|
||||
{% if niv['niveau'] %}
|
||||
{# Affiche l'UE et sa décision de jury #}
|
||||
{% if niv['ue_'+sem] %}
|
||||
{{ niv['ue_'+sem].acronyme }}
|
||||
{% set validation = ue_validation_by_niveau.get((niv['niveau'].id, sem)) %}
|
||||
<div class="ue_validation_code with_scoplement">
|
||||
<div>
|
||||
{% if validation %}
|
||||
<b>{{validation.code}}</b>
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="scoplement">
|
||||
{% if validation %}
|
||||
<div>Validation de {{niv['ue_'+sem].acronyme}}</div>
|
||||
<div>Jury de {{validation.formsemestre.titre_annee()
|
||||
if validation.formsemestre else "-"}}</div>
|
||||
<div>enregistrée le {{
|
||||
validation.event_date.strftime("%d/%m/%Y à %H:%M")
|
||||
if validation.event_date else "-"
|
||||
}}</div>
|
||||
{% else %}
|
||||
pas de décision de jury enregistrée pour cette UE
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<span class="fontred" title="Pas d'UE associée à ce niveau !">{{scu.EMO_WARNING|safe}} non associé</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{# Résultats dans le parcours #}
|
||||
<div class="parcour_formation">
|
||||
|
||||
<div class="titre_parcours">
|
||||
Validations de {{sco.etud.html_link_fiche()|safe}}
|
||||
{% if parcour %}
|
||||
parcours {{parcour.code}} « {{parcour.libelle}} »
|
||||
{% else %}
|
||||
non inscrit à un parcours de la spécialité
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% for comp in competences_parcour %}
|
||||
{% set color_idx = 1 + loop.index0 % 6 %}
|
||||
<div class="competence comp-c{{color_idx}}">
|
||||
<div class="titre_competence tc">
|
||||
Compétence {{comp['competence'].numero}} : {{comp['competence'].titre}}
|
||||
</div>
|
||||
<div class="niveaux">
|
||||
{% for annee, niv in comp['niveaux'].items() %}
|
||||
<div class="niveau comp-c{{color_idx}}-{{annee}}"
|
||||
style="--color: var(--col-c{{color_idx}}-{{annee}});">
|
||||
<div class="titre_niveau n{{annee}}">
|
||||
<span class="parcs">
|
||||
{% if niv['niveau'].is_tronc_commun %}
|
||||
<span class="parc">TC</span>
|
||||
{% elif niv['niveau'].parcours|length > 1 %}
|
||||
<span class="parc">
|
||||
{% set virg = joiner(", ") %}
|
||||
{% for p in niv['niveau'].parcours %}
|
||||
{{ virg() }}{{p.code}}
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{{niv['niveau'].libelle if niv['niveau'] else ''}}
|
||||
</div>
|
||||
<div class="rcue">
|
||||
<div class="rcue_validation_code with_scoplement">
|
||||
{% set validation = rcue_validation_by_niveau.get(niv['niveau'].id) %}
|
||||
{% if validation %}
|
||||
<div>
|
||||
RCUE enregistré <b>{{validation.code}}</b>
|
||||
{% if niv['niveau'] and edit %}
|
||||
{% if not (niv['ue_pair'] and niv['ue_impair']) %}
|
||||
<span title="UEs manquantes">⛔</span>
|
||||
{% else %}
|
||||
<select class="validation_rcue" name="ue_niv_{{niv['niveau'].id}}" id="ue_niv_{{niv['niveau'].id}}"
|
||||
onchange="record_rcue_validation(event,
|
||||
{{niv['niveau'].id}},
|
||||
);"
|
||||
|
||||
data-ue1_id="{{niv['ue_impair'].id}}"
|
||||
data-ue2_id="{{niv['ue_pair'].id}}"
|
||||
>
|
||||
{% for code in rcue_codes %}
|
||||
<option value="{{code}}"
|
||||
{% if validation and validation.code == code -%}
|
||||
selected
|
||||
{%- endif %}
|
||||
>{{code}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="scoplement">
|
||||
<div>Validation du RCUE</div>
|
||||
<div>enregistrée le {{
|
||||
validation.date.strftime("%d/%m/%Y à %H:%M")
|
||||
if validation.date else "-"
|
||||
}}
|
||||
</div>
|
||||
<div>par le jury de {{validation.formsemestre.titre_annee()
|
||||
if validation.formsemestre else "-"}}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ue impair u{{annee}}1">
|
||||
{{ show_ue(niv, "impair", 2*annee-1) }}
|
||||
</div>
|
||||
<div class="ue pair u{{annee}}1">
|
||||
{{ show_ue(niv, "pair", 2*annee) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if sco.formsemestre.can_edit_jury() %}
|
||||
<div style="padding-bottom: 16px;">
|
||||
{% if edit %}
|
||||
<a class="stdlink" href="{{url_for('notes.validation_rcues',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id, etudid=sco.etud.id
|
||||
)}}">quitter le mode édition des RCUEs</a>
|
||||
{% else %}
|
||||
<a class="stdlink" href="{{url_for('notes.validation_rcues_edit',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sco.formsemestre.id, etudid=sco.etud.id
|
||||
)}}">éditer les décisions d'RCUE antérieurs</a>
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="help">
|
||||
|
||||
<p>Cette page montre les validations d'UEs et de niveaux de compétences (RCUEs)
|
||||
de {{sco.etud.html_link_fiche()|safe}}
|
||||
dans le
|
||||
{%if parcour %}
|
||||
parcours <span class="parc">{{parcour.code}}</span>
|
||||
{% else %}
|
||||
tronc commun
|
||||
{% endif %}
|
||||
du référentiel de compétence {{formation.referentiel_competence.specialite}}
|
||||
</p>
|
||||
|
||||
<p>Seuls les UEs et niveaux de ce référentiel sont montrés. Si le référentiel a
|
||||
changé, enregistrer des validations "antérieures".
|
||||
</p>
|
||||
|
||||
<p>Le symbole <span class="parc">TC</span> désigne un niveau du tronc commun
|
||||
(c'est à dire présent dans tous les parcours de la spécialité). </p>
|
||||
|
||||
{% if edit %}
|
||||
<p>Les validations sont enregistrées au fur et à mesure.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function record_rcue_validation(event, niveau_id) {
|
||||
let code = event.target.value;
|
||||
let ue1_id = event.target.dataset.ue1_id;
|
||||
let ue2_id = event.target.dataset.ue2_id;
|
||||
const record_url = '{{
|
||||
url_for(
|
||||
"apiweb.validation_rcue_record",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=sco.etud.id
|
||||
)
|
||||
}}';
|
||||
|
||||
fetch(record_url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify( {
|
||||
code : code,
|
||||
ue1_id : ue1_id,
|
||||
ue2_id : ue2_id,
|
||||
// Optionnel:
|
||||
formsemestre_id : {{sco.formsemestre.id}},
|
||||
parcours_id : {{parcour.id if parcour else "null"}},
|
||||
} )
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status) {
|
||||
/* sco_message(data.message); */
|
||||
/* revert menu to initial state */
|
||||
event.target.value = event.target.dataset.ue_id;
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
@ -50,7 +50,7 @@ def close_dept_db_connection(arg):
|
||||
class ScoData:
|
||||
"""Classe utilisée pour passer des valeurs aux vues (templates)"""
|
||||
|
||||
def __init__(self, etud=None, formsemestre=None):
|
||||
def __init__(self, etud: Identite = None, formsemestre: FormSemestre = None):
|
||||
# Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête)
|
||||
self.Permission = Permission
|
||||
self.scu = scu
|
||||
@ -96,6 +96,7 @@ class ScoData:
|
||||
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
|
||||
self.sem
|
||||
)
|
||||
self.formsemestre = formsemestre
|
||||
# --- Préférences
|
||||
# prefs fallback to global pref if sem is None:
|
||||
if formsemestre:
|
||||
|
@ -30,7 +30,8 @@ Emmanuel Viennet, 2023
|
||||
|
||||
from flask import flash, g, redirect, render_template, request, url_for
|
||||
|
||||
from app import db, log
|
||||
from app import db
|
||||
from app.but import cursus_but, validations_view
|
||||
from app.decorators import (
|
||||
scodoc,
|
||||
permission_required,
|
||||
@ -39,16 +40,16 @@ from app.decorators import (
|
||||
from app.forms.formation.ue_parcours_ects import UEParcoursECTSForm
|
||||
|
||||
from app.models import (
|
||||
ApcCompetence,
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
ApcReferentielCompetences,
|
||||
Formation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoPermissionDenied, ScoValueError
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
|
||||
@ -74,7 +75,9 @@ def parcour_formation(formation_id: int, parcour_id: int = None) -> str:
|
||||
raise ScoValueError("parcours invalide ou hors référentiel de formation")
|
||||
|
||||
competences_parcour = (
|
||||
parcour_formation_competences(parcour, formation) if parcour else None
|
||||
cursus_but.parcour_formation_competences(parcour, formation)
|
||||
if parcour
|
||||
else None
|
||||
)
|
||||
|
||||
return render_template(
|
||||
@ -87,103 +90,32 @@ def parcour_formation(formation_id: int, parcour_id: int = None) -> str:
|
||||
)
|
||||
|
||||
|
||||
def ue_associee_au_niveau_du_parcours(
|
||||
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S"
|
||||
) -> UniteEns:
|
||||
"L'UE associée à ce niveau, ou None"
|
||||
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id]
|
||||
if len(ues) > 1:
|
||||
# plusieurs UEs associées à ce niveau: élimine celles sans parcours
|
||||
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours]
|
||||
if ues_pair_avec_parcours:
|
||||
ues = ues_pair_avec_parcours
|
||||
if len(ues) > 1:
|
||||
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}")
|
||||
return ues[0] if ues else None
|
||||
|
||||
|
||||
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
|
||||
@bp.route(
|
||||
"/validation_rcues/<int:formsemestre_id>/<int:etudid>/edit",
|
||||
defaults={"edit": True},
|
||||
endpoint="validation_rcues_edit",
|
||||
)
|
||||
@bp.route("/validation_rcues/<int:formsemestre_id>/<int:etudid>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def validation_rcues(
|
||||
formsemestre_id: int, etudid: int = None, edit: bool = False
|
||||
) -> str:
|
||||
"""Visualisation des résultats UEs et RCUEs d'un étudiant
|
||||
et saisie des validation de RCUE antérieures.
|
||||
"""
|
||||
[
|
||||
{
|
||||
'competence' : ApcCompetence,
|
||||
'niveaux' : {
|
||||
1 : { ... },
|
||||
2 : { ... },
|
||||
3 : {
|
||||
'niveau' : ApcNiveau,
|
||||
'ue_impair' : UniteEns, # actuellement associée
|
||||
'ues_impair' : list[UniteEns], # choix possibles
|
||||
'ue_pair' : UniteEns,
|
||||
'ues_pair' : list[UniteEns],
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
def _niveau_ues(competence: ApcCompetence, annee: int) -> dict:
|
||||
"niveau et ues pour cette compétence de cette année du parcours"
|
||||
niveaux = ApcNiveau.niveaux_annee_de_parcours(
|
||||
parcour, annee, competence=competence
|
||||
)
|
||||
if len(niveaux) > 0:
|
||||
if len(niveaux) > 1:
|
||||
log(
|
||||
f"""_niveau_ues: plus d'un niveau pour {competence}
|
||||
annee {annee} parcours {parcour.code}"""
|
||||
etud: Identite = Identite.query.get_or_404(etudid)
|
||||
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if edit: # check permission
|
||||
if not formsemestre.can_edit_jury():
|
||||
raise ScoPermissionDenied(
|
||||
dest_url=url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
niveau = niveaux[0]
|
||||
elif len(niveaux) == 0:
|
||||
return {
|
||||
"niveau": None,
|
||||
"ue_pair": None,
|
||||
"ue_impair": None,
|
||||
"ues_pair": [],
|
||||
"ues_impair": [],
|
||||
}
|
||||
# Toutes les UEs de la formation dans ce parcours ou tronc commun
|
||||
ues = [
|
||||
ue
|
||||
for ue in formation.ues
|
||||
if ((not ue.parcours) or (parcour.id in (p.id for p in ue.parcours)))
|
||||
and ue.type == UE_STANDARD
|
||||
]
|
||||
ues_pair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee)]
|
||||
ues_impair_possibles = [ue for ue in ues if ue.semestre_idx == (2 * annee - 1)]
|
||||
|
||||
# UE associée au niveau dans ce parcours
|
||||
ue_pair = ue_associee_au_niveau_du_parcours(
|
||||
ues_pair_possibles, niveau, f"S{2*annee}"
|
||||
)
|
||||
ue_impair = ue_associee_au_niveau_du_parcours(
|
||||
ues_impair_possibles, niveau, f"S{2*annee-1}"
|
||||
)
|
||||
|
||||
return {
|
||||
"niveau": niveau,
|
||||
"ue_pair": ue_pair,
|
||||
"ues_pair": [
|
||||
ue
|
||||
for ue in ues_pair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
"ue_impair": ue_impair,
|
||||
"ues_impair": [
|
||||
ue
|
||||
for ue in ues_impair_possibles
|
||||
if (not ue.niveau_competence) or ue.niveau_competence.id == niveau.id
|
||||
],
|
||||
}
|
||||
|
||||
competences = [
|
||||
{
|
||||
"competence": competence,
|
||||
"niveaux": {annee: _niveau_ues(competence, annee) for annee in (1, 2, 3)},
|
||||
}
|
||||
for competence in parcour.query_competences()
|
||||
]
|
||||
return competences
|
||||
)
|
||||
return validations_view.validation_rcues(etud, formsemestre, edit)
|
||||
|
||||
|
||||
@bp.route("/ue_parcours_ects/<int:ue_id>", methods=["GET", "POST"])
|
||||
|
@ -16,8 +16,9 @@ Utilisation :
|
||||
Lancer :
|
||||
pytest tests/api/test_api_jury.py
|
||||
"""
|
||||
import requests
|
||||
|
||||
|
||||
from app.scodoc import sco_utils as scu
|
||||
from tests.api.setup_test_api import (
|
||||
API_URL,
|
||||
CHECK_CERTIFICATE,
|
||||
@ -37,4 +38,29 @@ def test_jury_decisions(api_headers):
|
||||
decisions_jury = GET(
|
||||
f"/formsemestre/{formsemestre_id}/decisions_jury", headers=api_headers
|
||||
)
|
||||
assert len(etudiants) > 0
|
||||
assert len(etudiants) == len(decisions_jury)
|
||||
# TODO La suite de ce test est a compléter: il faut modifier le formation test RT
|
||||
# pour avoir au moins le S2 et le S2: actuellement seulement le S1
|
||||
# # Récupère la formation de ce semestre pour avoir les UEs
|
||||
# r = requests.get(
|
||||
# API_URL + "/formation/1/export",
|
||||
# headers=api_headers,
|
||||
# verify=CHECK_CERTIFICATE,
|
||||
# timeout=scu.SCO_TEST_API_TIMEOUT,
|
||||
# )
|
||||
# assert r.status_code == 200
|
||||
# export_formation = r.json()
|
||||
# ues = export_formation["ue"]
|
||||
# # Enregistre une validation d'RCUE
|
||||
# etudid = etudiants[0]["id"]
|
||||
# validation = POST_JSON(
|
||||
# f"/etudiant/{etudid}/jury/validation_rcue/record",
|
||||
# data={
|
||||
# "code": "ADM",
|
||||
# "ue1_id": XXX,
|
||||
# "ue2_id": XXX,
|
||||
# },
|
||||
# headers=api_headers,
|
||||
# )
|
||||
# assert validation
|
||||
|
Loading…
Reference in New Issue
Block a user