Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
11 changed files with 687 additions and 111 deletions
Showing only changes of commit f08a4130dd - Show all commits

View File

@ -8,25 +8,32 @@
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions 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_json import as_json
from flask_login import current_user, login_required from flask_login import current_user, login_required
import app import app
from app import db, log 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.decorators import scodoc, permission_required
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import ScoException
from app.but import jury_but_results from app.but import jury_but_results
from app.models import ( from app.models import (
ApcParcours,
ApcValidationAnnee, ApcValidationAnnee,
ApcValidationRCUE, ApcValidationRCUE,
Formation,
FormSemestre, FormSemestre,
Identite, Identite,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
ScolarNews, ScolarNews,
Scolog,
UniteEns,
) )
from app.scodoc import codes_cursus
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
@ -161,6 +168,136 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
return "ok" 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( @bp.route(
"/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete", "/etudiant/<int:etudid>/jury/validation_rcue/<int:validation_id>/delete",
methods=["POST"], methods=["POST"],

View File

@ -282,7 +282,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
@scodoc @scodoc
@permission_required(Permission.ScoEtudChangeGroups) @permission_required(Permission.ScoEtudChangeGroups)
@as_json @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 """Création d'un groupe dans une partition
The request content type should be "application/json": The request content type should be "application/json":

View File

@ -18,7 +18,7 @@ from operator import attrgetter
from flask import g, url_for 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_but import ResultatsSemestreBUT
from app.comp.res_compat import NotesTableCompat 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.ues import UniteEns
from app.models.validations import ScolarFormSemestreValidation from app.models.validations import ScolarFormSemestreValidation
from app.scodoc import codes_cursus as sco_codes 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 import sco_utils as scu
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
@ -479,3 +480,122 @@ def formsemestre_warning_apc_setup(
</p> </p>
</div> </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
View 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
}

View File

@ -204,6 +204,7 @@ CODES_UE_VALIDES = CODES_UE_VALIDES_DE_DROIT | {ADJ, ADJR, ADSUP}
CODES_UE_CAPITALISANTS = {ADM} CODES_UE_CAPITALISANTS = {ADM}
"UE capitalisée" "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_DE_DROIT = {ADM, CMP}
CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP} CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
"Niveau RCUE validé" "Niveau RCUE validé"

View File

@ -313,9 +313,26 @@ def ficheEtud(etudid=None):
) )
info[ info[
"link_bul_pdf" "link_bul_pdf"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{ ] = f"""
url_for("notes.etud_bulletins_pdf", scodoc_dept=g.scodoc_dept, etudid=etudid) <span class="link_bul_pdf">
}">tous les bulletins</a></span>""" <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): if authuser.has_permission(Permission.ScoEtudInscrit):
info[ info[
"link_inscrire_ailleurs" "link_inscrire_ailleurs"

View File

@ -144,6 +144,10 @@ div.ue.pair {
color: black; color: black;
} }
div.rcue {
grid-column: 1 / span 2;
}
/* ne fonctionne pas /* ne fonctionne pas
option.non_associe { option.non_associe {
background-color: yellow; background-color: yellow;
@ -153,4 +157,9 @@ option.non_associe {
.links { .links {
margin-top: 16px; margin-top: 16px;
margin-bottom: 8px; margin-bottom: 8px;
} }
select.validation_rcue {
display: inline-block;
margin-left: 32px;
}

View 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}}&nbsp;: {{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 %}

View File

@ -50,7 +50,7 @@ def close_dept_db_connection(arg):
class ScoData: class ScoData:
"""Classe utilisée pour passer des valeurs aux vues (templates)""" """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) # Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête)
self.Permission = Permission self.Permission = Permission
self.scu = scu self.scu = scu
@ -96,6 +96,7 @@ class ScoData:
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar( self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
self.sem self.sem
) )
self.formsemestre = formsemestre
# --- Préférences # --- Préférences
# prefs fallback to global pref if sem is None: # prefs fallback to global pref if sem is None:
if formsemestre: if formsemestre:

View File

@ -30,7 +30,8 @@ Emmanuel Viennet, 2023
from flask import flash, g, redirect, render_template, request, url_for 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 ( from app.decorators import (
scodoc, scodoc,
permission_required, permission_required,
@ -39,16 +40,16 @@ from app.decorators import (
from app.forms.formation.ue_parcours_ects import UEParcoursECTSForm from app.forms.formation.ue_parcours_ects import UEParcoursECTSForm
from app.models import ( from app.models import (
ApcCompetence,
ApcNiveau,
ApcParcours, ApcParcours,
ApcReferentielCompetences, ApcReferentielCompetences,
Formation, Formation,
FormSemestre,
Identite,
UniteEns, UniteEns,
) )
from app.scodoc.codes_cursus import UE_STANDARD from app.scodoc.codes_cursus import UE_STANDARD
from app.scodoc.sco_permissions import Permission 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 notes_bp as bp
from app.views import ScoData 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") raise ScoValueError("parcours invalide ou hors référentiel de formation")
competences_parcour = ( 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( 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( @bp.route(
ues_possibles: list[UniteEns], niveau: ApcNiveau, sem_name: str = "S" "/validation_rcues/<int:formsemestre_id>/<int:etudid>/edit",
) -> UniteEns: defaults={"edit": True},
"L'UE associée à ce niveau, ou None" endpoint="validation_rcues_edit",
ues = [ue for ue in ues_possibles if ue.niveau_competence_id == niveau.id] )
if len(ues) > 1: @bp.route("/validation_rcues/<int:formsemestre_id>/<int:etudid>")
# plusieurs UEs associées à ce niveau: élimine celles sans parcours @scodoc
ues_pair_avec_parcours = [ue for ue in ues if ue.parcours] @permission_required(Permission.ScoView)
if ues_pair_avec_parcours: def validation_rcues(
ues = ues_pair_avec_parcours formsemestre_id: int, etudid: int = None, edit: bool = False
if len(ues) > 1: ) -> str:
log(f"_niveau_ues: {len(ues)} associées au niveau {niveau} / {sem_name}") """Visualisation des résultats UEs et RCUEs d'un étudiant
return ues[0] if ues else None et saisie des validation de RCUE antérieures.
def parcour_formation_competences(parcour: ApcParcours, formation: Formation) -> list:
""" """
[ etud: Identite = Identite.query.get_or_404(etudid)
{ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
'competence' : ApcCompetence, if edit: # check permission
'niveaux' : { if not formsemestre.can_edit_jury():
1 : { ... }, raise ScoPermissionDenied(
2 : { ... }, dest_url=url_for(
3 : { "notes.formsemestre_status",
'niveau' : ApcNiveau, scodoc_dept=g.scodoc_dept,
'ue_impair' : UniteEns, # actuellement associée formsemestre_id=formsemestre_id,
'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}"""
) )
niveau = niveaux[0] )
elif len(niveaux) == 0: return validations_view.validation_rcues(etud, formsemestre, edit)
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
@bp.route("/ue_parcours_ects/<int:ue_id>", methods=["GET", "POST"]) @bp.route("/ue_parcours_ects/<int:ue_id>", methods=["GET", "POST"])

View File

@ -16,8 +16,9 @@ Utilisation :
Lancer : Lancer :
pytest tests/api/test_api_jury.py pytest tests/api/test_api_jury.py
""" """
import requests
from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import ( from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
@ -37,4 +38,29 @@ def test_jury_decisions(api_headers):
decisions_jury = GET( decisions_jury = GET(
f"/formsemestre/{formsemestre_id}/decisions_jury", headers=api_headers f"/formsemestre/{formsemestre_id}/decisions_jury", headers=api_headers
) )
assert len(etudiants) > 0
assert len(etudiants) == len(decisions_jury) 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