forked from ScoDoc/ScoDoc
Compare commits
1 Commits
master
...
assi_excel
Author | SHA1 | Date | |
---|---|---|---|
|
af45a3ad03 |
@ -1,14 +1,10 @@
|
||||
"""api.__init__
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from flask_json import as_json
|
||||
from flask import Blueprint
|
||||
from flask import current_app, g, request
|
||||
from flask import request, g
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
from app.decorators import permission_required
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -20,28 +16,6 @@ api_web_bp = Blueprint("apiweb", __name__)
|
||||
API_CLIENT_ERROR = 400 # erreur dans les paramètres fournis par le client
|
||||
|
||||
|
||||
def api_permission_required(permission):
|
||||
"""Ce décorateur fait la même chose que @permission_required
|
||||
mais enregistre dans l'attribut .scodoc_permission
|
||||
de la fonction la valeur de la permission.
|
||||
Cette valeur n'est utilisée que pour la génération automatique de la documentation.
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
f.scodoc_permission = permission
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
scodoc_dept = getattr(g, "scodoc_dept", None)
|
||||
if not current_user.has_permission(permission, scodoc_dept):
|
||||
return current_app.login_manager.unauthorized()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@api_bp.errorhandler(ScoException)
|
||||
@api_web_bp.errorhandler(ScoException)
|
||||
@api_bp.errorhandler(404)
|
||||
|
@ -12,18 +12,15 @@ from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.orm.exc import ObjectDeletedError
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from app import db, log, set_sco_dept
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp, get_model_api_object, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import (
|
||||
Assiduite,
|
||||
Evaluation,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
@ -48,8 +45,6 @@ def assiduite(assiduite_id: int = None):
|
||||
"""Retourne un objet assiduité à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"assiduite_id": 1,
|
||||
"etudid": 2,
|
||||
@ -58,17 +53,11 @@ def assiduite(assiduite_id: int = None):
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
"user_id": 1 or null,
|
||||
"user_name" : login scodoc or null,
|
||||
"user_nom_complet": "Marie Dupont",
|
||||
"user_id: 1 or null,
|
||||
"user_name" : login scodoc or null
|
||||
"user_nom_complet": "Marie Dupont"
|
||||
"est_just": False or True,
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1;
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Assiduite, assiduite_id, Identite)
|
||||
@ -86,23 +75,15 @@ def assiduite(assiduite_id: int = None):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
||||
"""Retourne la liste des justificatifs qui justifient cette assiduité.
|
||||
"""Retourne la liste des justificatifs qui justifie cette assiduitée
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
...
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/justificatifs;
|
||||
/assiduite/1/justificatifs/long;
|
||||
|
||||
"""
|
||||
|
||||
return get_assiduites_justif(assiduite_id, long)
|
||||
@ -136,42 +117,52 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_count(
|
||||
def count_assiduites(
|
||||
etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False
|
||||
):
|
||||
"""
|
||||
Retourne le nombre d'assiduités d'un étudiant.
|
||||
Retourne le nombre d'assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>/count
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/count/query?
|
||||
|
||||
Les différents filtres :
|
||||
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
|
||||
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
|
||||
ex: .../query?type=heure
|
||||
Comportement par défaut : compte le nombre d'assiduité enregistrée
|
||||
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemestre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
metric:<array[string]:metric>
|
||||
split:<bool:split>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
metric: la/les métriques de comptage (journee, demi, heure, compte)
|
||||
split: divise le comptage par état
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1/count;
|
||||
/assiduites/1/count/query?etat=retard;
|
||||
/assiduites/1/count/query?split;
|
||||
/assiduites/1/count/query?etat=present,retard&metric=compte,heure;
|
||||
|
||||
"""
|
||||
|
||||
@ -228,35 +219,40 @@ def assiduites_count(
|
||||
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /assiduites/<int:etudid>
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
with_justifs:<bool:with_justifs>
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/<int:etudid>/query?
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
with_justif:ajoute les justificatifs liés à l'assiduité
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1;
|
||||
/assiduites/1/query?etat=retard;
|
||||
/assiduites/1/query?moduleimpl_id=1;
|
||||
/assiduites/1/query?with_justifs=;
|
||||
|
||||
"""
|
||||
|
||||
@ -268,7 +264,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
# Récupération des assiduités de l'étudiant
|
||||
assiduites_query: Query = etud.assiduites
|
||||
|
||||
@ -291,108 +286,6 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
|
||||
return data_set
|
||||
|
||||
|
||||
@bp.route("/assiduites/<int:etudid>/evaluations")
|
||||
@api_web_bp.route("/assiduites/<int:etudid>/evaluations")
|
||||
# etudid
|
||||
@bp.route("/assiduites/etudid/<int:etudid>/evaluations")
|
||||
@api_web_bp.route("/assiduites/etudid/<int:etudid>/evaluations")
|
||||
# ine
|
||||
@bp.route("/assiduites/ine/<ine>/evaluations")
|
||||
@api_web_bp.route("/assiduites/ine/<ine>/evaluations")
|
||||
# nip
|
||||
@bp.route("/assiduites/nip/<nip>/evaluations")
|
||||
@api_web_bp.route("/assiduites/nip/<nip>/evaluations")
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Retourne la liste de toutes les évaluations de l'étudiant
|
||||
Pour chaque évaluation, retourne la liste des objets assiduités
|
||||
sur la plage de l'évaluation
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"evaluation_id": 1234,
|
||||
"assiduites": [
|
||||
{
|
||||
"assiduite_id":1234,
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/1/evaluations;
|
||||
|
||||
```
|
||||
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
if etud is None:
|
||||
return json_error(
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
# Récupération des évaluations et des assidiutés
|
||||
etud_evaluations_assiduites: list[dict] = scass.get_etud_evaluations_assiduites(
|
||||
etud
|
||||
)
|
||||
|
||||
return etud_evaluations_assiduites
|
||||
|
||||
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/assiduites")
|
||||
@bp.route("/evaluation/<int:evaluation_id>/assiduites")
|
||||
@login_required
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation_assiduites(evaluation_id):
|
||||
"""
|
||||
Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"<etudid>" : [
|
||||
{
|
||||
"assiduite_id":1234,
|
||||
...
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Évaluations
|
||||
"""
|
||||
# Récupération de l'évaluation
|
||||
try:
|
||||
evaluation: Evaluation = Evaluation.get_evaluation(evaluation_id)
|
||||
except HTTPException:
|
||||
return json_error(404, "L'évaluation n'existe pas")
|
||||
|
||||
evaluation_assiduites_par_etudid: dict[int, list[Assiduite]] = {}
|
||||
for assi in scass.get_evaluation_assiduites(evaluation):
|
||||
etudid: str = str(assi.etudid)
|
||||
etud_assiduites = evaluation_assiduites_par_etudid.get(etudid, [])
|
||||
etud_assiduites.append(assi.to_dict(format_api=True))
|
||||
evaluation_assiduites_par_etudid[etudid] = etud_assiduites
|
||||
|
||||
return evaluation_assiduites_par_etudid
|
||||
|
||||
|
||||
@bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@api_web_bp.route("/assiduites/group/query", defaults={"with_query": True})
|
||||
@login_required
|
||||
@ -404,34 +297,38 @@ def assiduites_group(with_query: bool = False):
|
||||
Retourne toutes les assiduités d'un groupe d'étudiants
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /assiduites/group/query?etudids=1,2,3
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
etudids:<array[int]:etudids>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
with_justif:<bool:with_justif>
|
||||
Les différents filtres :
|
||||
Etat (etat de l'étudiant -> absent, present ou retard):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=present,retard
|
||||
Date debut
|
||||
(date de début de l'assiduité, sont affichés les assiduités
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin de l'assiduité, sont affichés les assiduités
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
Moduleimpl_id (l'id du module concerné par l'assiduité):
|
||||
query?moduleimpl_id=[- int ou vide -]
|
||||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
etudids:liste des ids des étudiants concernés par la recherche
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
with_justifs:ajoute les justificatifs liés à l'assiduité
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/group/query?etudids=1,2,3;
|
||||
|
||||
"""
|
||||
|
||||
@ -491,34 +388,7 @@ def assiduites_group(with_query: bool = False):
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne toutes les assiduités du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/formsemestre/1;
|
||||
/assiduites/formsemestre/1/query?etat=retard;
|
||||
/assiduites/formsemestre/1/query?moduleimpl_id=1;
|
||||
|
||||
"""
|
||||
"""Retourne toutes les assiduités du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre à partir du formsemestre_id
|
||||
formsemestre: FormSemestre = None
|
||||
@ -565,42 +435,10 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
@scodoc
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def assiduites_formsemestre_count(
|
||||
def count_assiduites_formsemestre(
|
||||
formsemestre_id: int = None, with_query: bool = False
|
||||
):
|
||||
"""Comptage des assiduités du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
moduleimpl_id:<int:moduleimpl_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
formsemestre_id:<int:formsemestre_id>
|
||||
metric:<array[string]:metric>
|
||||
split:<bool:split>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur de l'assiduité
|
||||
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
|
||||
moduleimpl_id:l'id du module concerné par l'assiduité
|
||||
date_debut:date de début de l'assiduité (supérieur ou égal)
|
||||
date_fin:date de fin de l'assiduité (inférieur ou égal)
|
||||
etat:etat de l'étudiant → absent, present ou retard
|
||||
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
|
||||
metric: la/les métriques de comptage (journee, demi, heure, compte)
|
||||
split: divise le comptage par état
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/formsemestre/1/count;
|
||||
/assiduites/formsemestre/1/count/query?etat=retard;
|
||||
/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure;
|
||||
|
||||
"""
|
||||
"""Comptage des assiduités du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre à partir du formsemestre_id
|
||||
formsemestre: FormSemestre = None
|
||||
@ -651,10 +489,7 @@ def assiduites_formsemestre_count(
|
||||
def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Enregistrement d'assiduités pour un étudiant (etudid)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -670,12 +505,6 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]
|
||||
/assiduite/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]
|
||||
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
@ -729,10 +558,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
|
||||
def assiduites_create():
|
||||
"""
|
||||
Création d'une assiduité ou plusieurs assiduites
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -745,17 +571,12 @@ def assiduites_create():
|
||||
"date_fin": str,
|
||||
"etat": str,
|
||||
"etudid":int,
|
||||
|
||||
"moduleimpl_id": int,
|
||||
"desc":str,
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/create;[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]
|
||||
/assiduites/create;[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]
|
||||
|
||||
"""
|
||||
|
||||
@ -926,18 +747,13 @@ def assiduite_delete():
|
||||
"""
|
||||
Suppression d'une assiduité à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<assiduite_id:int>,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/delete;[2,2,3]
|
||||
|
||||
"""
|
||||
# Récupération des ids envoyés dans la liste
|
||||
@ -1012,24 +828,13 @@ def _delete_one(assiduite_id: int) -> tuple[int, str]:
|
||||
def assiduite_edit(assiduite_id: int):
|
||||
"""
|
||||
Edition d'une assiduité à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
{
|
||||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduite/1/edit;{""etat"":""absent""}
|
||||
/assiduite/1/edit;{""moduleimpl_id"":2}
|
||||
/assiduite/1/edit;{""etat"": ""retard"",""moduleimpl_id"":3}
|
||||
|
||||
"""
|
||||
|
||||
# Récupération de l'assiduité à modifier
|
||||
@ -1071,10 +876,7 @@ def assiduite_edit(assiduite_id: int):
|
||||
def assiduites_edit():
|
||||
"""
|
||||
Edition de plusieurs assiduités
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"assiduite_id" : int,
|
||||
@ -1084,13 +886,6 @@ def assiduites_edit():
|
||||
"est_just"?: bool
|
||||
}
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/assiduites/edit;[{""etat"":""absent"",""assiduite_id"":1}]
|
||||
/assiduites/edit;[{""moduleimpl_id"":2,""assiduite_id"":1}]
|
||||
/assiduites/edit;[{""etat"": ""retard"",""moduleimpl_id"":3,""assiduite_id"":1}]
|
||||
|
||||
"""
|
||||
edit_list: list[object] = request.get_json(force=True)
|
||||
|
||||
|
@ -6,11 +6,6 @@
|
||||
|
||||
"""
|
||||
API : billets d'absences
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Billets d'absence
|
||||
|
||||
"""
|
||||
|
||||
from flask import g, request
|
||||
@ -34,7 +29,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def billets_absence_etudiant(etudid: int):
|
||||
"""Liste des billets d'absence pour cet étudiant."""
|
||||
"""Liste des billets d'absence pour cet étudiant"""
|
||||
billets = sco_abs_billets.query_billets_etud(etudid)
|
||||
return [billet.to_dict() for billet in billets]
|
||||
|
||||
@ -46,20 +41,7 @@ def billets_absence_etudiant(etudid: int):
|
||||
@permission_required(Permission.AbsAddBillet)
|
||||
@as_json
|
||||
def billets_absence_create():
|
||||
"""Ajout d'un billet d'absence. Renvoie le billet créé en json.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"etudid" : int,
|
||||
"abs_begin" : date_iso,
|
||||
"abs_end" : date_iso,
|
||||
"description" : string,
|
||||
"justified" : bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Ajout d'un billet d'absence"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
etudid = data.get("etudid")
|
||||
abs_begin = data.get("abs_begin")
|
||||
|
@ -9,11 +9,6 @@
|
||||
|
||||
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
|
||||
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Département
|
||||
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
@ -21,15 +16,26 @@ from flask import request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
from app import db, log
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.models import departements
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
def get_departement(dept_ident: str) -> Departement:
|
||||
"Le departement, par id ou acronyme. Erreur 404 si pas trouvé."
|
||||
try:
|
||||
dept_id = int(dept_ident)
|
||||
except ValueError:
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return Departement.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return Departement.query.get_or_404(dept_id)
|
||||
|
||||
|
||||
@bp.route("/departements")
|
||||
@ -38,7 +44,7 @@ from app.scodoc.sco_utils import json_error
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_list():
|
||||
"""Liste tous les départements."""
|
||||
"""Liste les départements"""
|
||||
return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
|
||||
|
||||
|
||||
@ -48,7 +54,7 @@ def departements_list():
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departements_ids():
|
||||
"""Liste des ids de tous les départements."""
|
||||
"""Liste des ids de départements"""
|
||||
return [dept.id for dept in Departement.query]
|
||||
|
||||
|
||||
@ -57,12 +63,11 @@ def departements_ids():
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_by_acronym(acronym: str):
|
||||
def departement(acronym: str):
|
||||
"""
|
||||
Info sur un département. Accès par acronyme.
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronym": "TAPI",
|
||||
@ -71,7 +76,6 @@ def departement_by_acronym(acronym: str):
|
||||
"visible": true,
|
||||
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
|
||||
}
|
||||
```
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return dept.to_dict(with_dept_name=True)
|
||||
@ -98,15 +102,11 @@ def departement_by_id(dept_id: int):
|
||||
def departement_create():
|
||||
"""
|
||||
Création d'un département.
|
||||
Le content type doit être `application/json`.
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"acronym": str,
|
||||
"visible": bool,
|
||||
"visible":bool,
|
||||
}
|
||||
```
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
acronym = str(data.get("acronym", ""))
|
||||
@ -117,9 +117,6 @@ def departement_create():
|
||||
dept = departements.create_dept(acronym, visible=visible)
|
||||
except ScoValueError as exc:
|
||||
return json_error(500, exc.args[0] if exc.args else "")
|
||||
|
||||
log(f"departement_create {dept.acronym}")
|
||||
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -130,12 +127,10 @@ def departement_create():
|
||||
@as_json
|
||||
def departement_edit(acronym):
|
||||
"""
|
||||
Édition d'un département: seul le champ `visible` peut être modifié.
|
||||
|
||||
DATA
|
||||
----
|
||||
Edition d'un département: seul visible peut être modifié
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"visible": bool,
|
||||
"visible":bool,
|
||||
}
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
@ -147,7 +142,6 @@ def departement_edit(acronym):
|
||||
dept.visible = visible
|
||||
db.session.add(dept)
|
||||
db.session.commit()
|
||||
log(f"departement_edit {dept.acronym}")
|
||||
return dept.to_dict()
|
||||
|
||||
|
||||
@ -157,13 +151,11 @@ def departement_edit(acronym):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def departement_delete(acronym):
|
||||
"""
|
||||
Suppression d'un département identifié par son acronyme.
|
||||
Suppression d'un département.
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
acronym = dept.acronym
|
||||
db.session.delete(dept)
|
||||
db.session.commit()
|
||||
log(f"departement_delete {acronym}")
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
@ -172,16 +164,13 @@ def departement_delete(acronym):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_etudiants(acronym: str):
|
||||
def dept_etudiants(acronym: str):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département.
|
||||
Retourne la liste des étudiants d'un département
|
||||
|
||||
PARAMS
|
||||
------
|
||||
acronym : l'acronyme d'un département
|
||||
acronym: l'acronyme d'un département
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"civilite": "M",
|
||||
@ -196,7 +185,6 @@ def departement_etudiants(acronym: str):
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [etud.to_dict_short() for etud in dept.etudiants]
|
||||
@ -207,7 +195,7 @@ def departement_etudiants(acronym: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_etudiants_by_id(dept_id: int):
|
||||
def dept_etudiants_by_id(dept_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un département d'id donné.
|
||||
"""
|
||||
@ -220,8 +208,8 @@ def departement_etudiants_by_id(dept_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_ids(acronym: str):
|
||||
"""Liste des ids de tous les formsemestres du département."""
|
||||
def dept_formsemestres_ids(acronym: str):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
@ -231,34 +219,57 @@ def departement_formsemestres_ids(acronym: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_ids_by_id(dept_id: int):
|
||||
"""Liste des ids de tous les formsemestres du département."""
|
||||
def dept_formsemestres_ids_by_id(dept_id: int):
|
||||
"""liste des ids formsemestre du département"""
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
return [formsemestre.id for formsemestre in dept.formsemestres]
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>/formsemestres_courants")
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None):
|
||||
def dept_formsemestres_courants(acronym: str):
|
||||
"""
|
||||
Liste les formsemestres du département indiqué (par son acronyme ou son id)
|
||||
contenant la date courante, ou à défaut celle indiquée en argument
|
||||
(au format ISO).
|
||||
|
||||
QUERY
|
||||
-----
|
||||
date_courante:<string:date_courante>
|
||||
Liste des semestres actifs d'un département d'acronyme donné
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"titre": "master machine info",
|
||||
"gestion_semestrielle": false,
|
||||
"scodoc7_id": null,
|
||||
"date_debut": "01/09/2021",
|
||||
"bul_bgcolor": null,
|
||||
"date_fin": "15/12/2022",
|
||||
"resp_can_edit": false,
|
||||
"dept_id": 1,
|
||||
"etat": true,
|
||||
"resp_can_change_ens": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"ens_can_edit_eval": false,
|
||||
"formation_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"elt_sem_apo": null,
|
||||
"semestre_id": 1,
|
||||
"bul_hide_xml": false,
|
||||
"elt_annee_apo": null,
|
||||
"block_moyennes": false,
|
||||
"formsemestre_id": 1,
|
||||
"titre_num": "master machine info semestre 1",
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_fin_iso": "2022-12-15",
|
||||
"responsables": [
|
||||
3,
|
||||
2
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
dept = (
|
||||
Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
if acronym
|
||||
else Departement.query.get_or_404(dept_id)
|
||||
)
|
||||
dept = Departement.query.filter_by(acronym=acronym).first_or_404()
|
||||
date_courante = request.args.get("date_courante")
|
||||
date_courante = datetime.fromisoformat(date_courante) if date_courante else None
|
||||
return [
|
||||
@ -267,3 +278,29 @@ def departement_formsemestres_courants(acronym: str = "", dept_id: int | None =
|
||||
dept, date_courante
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
"""
|
||||
Liste des semestres actifs d'un département d'id donné
|
||||
"""
|
||||
# Le département, spécifié par un id ou un acronyme
|
||||
dept = Departement.query.get_or_404(dept_id)
|
||||
date_courante = request.args.get("date_courante")
|
||||
if date_courante:
|
||||
test_date = datetime.fromisoformat(date_courante)
|
||||
else:
|
||||
test_date = db.func.current_date()
|
||||
# Les semestres en cours de ce département
|
||||
formsemestres = FormSemestre.query.filter(
|
||||
FormSemestre.dept_id == dept.id,
|
||||
FormSemestre.date_debut <= test_date,
|
||||
FormSemestre.date_fin >= test_date,
|
||||
)
|
||||
|
||||
return [d.to_dict_api() for d in formsemestres]
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
API : accès aux étudiants
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Étudiants
|
||||
"""
|
||||
from datetime import datetime
|
||||
from operator import attrgetter
|
||||
@ -25,9 +21,8 @@ import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.but import bulletin_but_court
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
Admission,
|
||||
Departement,
|
||||
@ -42,8 +37,9 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
import app.scodoc.sco_photos as sco_photos
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
# Un exemple:
|
||||
@ -93,20 +89,14 @@ def _get_etud_by_code(
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_courants(long: bool = False):
|
||||
def etudiants_courants(long=False):
|
||||
"""
|
||||
La liste des étudiants des semestres "courants".
|
||||
Considère tous les départements dans lesquels l'utilisateur a la
|
||||
permission `ScoView` (donc tous si le dépt. du rôle est `None`),
|
||||
et les formsemestres contenant la date courante,
|
||||
ou à défaut celle indiquée en argument (au format ISO).
|
||||
|
||||
QUERY
|
||||
-----
|
||||
date_courante:<string:date_courante>
|
||||
La liste des étudiants des semestres "courants" (tous départements)
|
||||
(date du jour comprise dans la période couverte par le sem.)
|
||||
dans lesquels l'utilisateur a la permission ScoView
|
||||
(donc tous si le dept du rôle est None).
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1234,
|
||||
@ -119,7 +109,6 @@ def etudiants_courants(long: bool = False):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
En format "long": voir documentation.
|
||||
|
||||
@ -165,13 +154,10 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
|
||||
`etudid` est unique dans la base (tous départements).
|
||||
Les codes INE et NIP sont uniques au sein d'un département.
|
||||
Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
|
||||
"""
|
||||
@ -195,18 +181,11 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Retourne la photo de l'étudiant ou un placeholder si non existant.
|
||||
Le paramètre `size` peut prendre la valeur `small` (taille réduite, hauteur
|
||||
environ 90 pixels) ou `orig` (défaut, image de la taille originale).
|
||||
Retourne la photo de l'étudiant
|
||||
correspondant ou un placeholder si non existant.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
size:<string:size>
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etudid : l'etudid de l'étudiant
|
||||
nip : le code nip de l'étudiant
|
||||
ine : le code ine de l'étudiant
|
||||
@ -236,7 +215,7 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudChangeAdr)
|
||||
@as_json
|
||||
def etudiant_set_photo_image(etudid: int = None):
|
||||
def set_photo_image(etudid: int = None):
|
||||
"""Enregistre la photo de l'étudiant."""
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
@ -279,12 +258,9 @@ def etudiant_set_photo_image(etudid: int = None):
|
||||
@as_json
|
||||
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
"""
|
||||
Info sur le ou les étudiants correspondants.
|
||||
|
||||
Comme `/etudiant` mais renvoie toujours une liste.
|
||||
|
||||
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
|
||||
toujours une liste.
|
||||
Si non trouvé, liste vide, pas d'erreur.
|
||||
|
||||
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
|
||||
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
|
||||
"""
|
||||
@ -317,9 +293,8 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
"""Liste des étudiants dont le nom débute par `start`.
|
||||
|
||||
Si `start` fait moins de `min_len=3` caractères, liste vide.
|
||||
"""Liste des étudiants dont le nom débute par start.
|
||||
Si start fait moins de min_len=3 caractères, liste vide.
|
||||
La casse et les accents sont ignorés.
|
||||
"""
|
||||
if len(start) < min_len:
|
||||
@ -354,13 +329,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
@as_json
|
||||
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
|
||||
"""
|
||||
Liste des formsemestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
|
||||
Accès par etudid, nip ou ine.
|
||||
|
||||
Attention, si accès via NIP ou INE, les formsemestres peuvent être de départements
|
||||
Attention, si accès via NIP ou INE, les semestres peuvent être de départements
|
||||
différents (si l'étudiant a changé de département). L'id du département est `dept_id`.
|
||||
|
||||
Si accès par département, ne retourne que les formsemestres suivis dans le département.
|
||||
Si accès par département, ne retourne que les formsemestre suivis dans le département.
|
||||
"""
|
||||
if etudid is not None:
|
||||
q_etud = Identite.query.filter_by(id=etudid)
|
||||
@ -428,14 +403,13 @@ def bulletin(
|
||||
"""
|
||||
Retourne le bulletin d'un étudiant dans un formsemestre.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
code_type : "etudid", "nip" ou "ine"
|
||||
code : valeur du code INE, NIP ou etudid, selon code_type.
|
||||
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
|
||||
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
||||
|
||||
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
if version == "pdf":
|
||||
version = "long"
|
||||
@ -489,13 +463,10 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"""
|
||||
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
[
|
||||
{
|
||||
"partition_id": 1,
|
||||
@ -520,7 +491,6 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"group_name": "A"
|
||||
}
|
||||
]
|
||||
```
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -548,12 +518,9 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def etudiant_create(force=False):
|
||||
"""Création d'un nouvel étudiant.
|
||||
|
||||
"""Création d'un nouvel étudiant
|
||||
Si force, crée même si homonymie détectée.
|
||||
|
||||
L'étudiant créé n'est pas inscrit à un semestre.
|
||||
|
||||
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -621,13 +588,7 @@ def etudiant_edit(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Édition des données étudiant (identité, admission, adresses).
|
||||
|
||||
PARAMS
|
||||
------
|
||||
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
|
||||
`code`: la valeur du code
|
||||
"""
|
||||
"""Edition des données étudiant (identité, admission, adresses)"""
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
return etud # json error
|
||||
@ -666,23 +627,7 @@ def etudiant_annotation(
|
||||
code_type: str = "etudid",
|
||||
code: str = None,
|
||||
):
|
||||
"""Ajout d'une annotation sur un étudiant.
|
||||
|
||||
Renvoie l'annotation créée.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
|
||||
`code`: la valeur du code
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"comment" : string
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Ajout d'une annotation sur un étudiant"""
|
||||
if not current_user.has_permission(Permission.ViewEtudData):
|
||||
return json_error(403, "non autorisé (manque ViewEtudData)")
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
@ -719,13 +664,7 @@ def etudiant_annotation_delete(
|
||||
code_type: str = "etudid", code: str = None, annotation_id: int = None
|
||||
):
|
||||
"""
|
||||
Suppression d'une annotation. On spécifie l'étudiant et l'id de l'annotation.
|
||||
|
||||
PARAMS
|
||||
------
|
||||
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
|
||||
`code`: la valeur du code
|
||||
`annotation_id` : id de l'annotation
|
||||
Suppression d'une annotation
|
||||
"""
|
||||
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
||||
if not ok:
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Évaluations
|
||||
"""
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -18,8 +14,7 @@ from flask_login import current_user, login_required
|
||||
import app
|
||||
from app import log, db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Evaluation, ModuleImpl, FormSemestre
|
||||
from app.scodoc import sco_evaluation_db, sco_saisie_notes
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
@ -36,28 +31,24 @@ import app.scodoc.sco_utils as scu
|
||||
def get_evaluation(evaluation_id: int):
|
||||
"""Description d'une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visibulletin': True
|
||||
}
|
||||
```
|
||||
'coefficient': 1.0,
|
||||
'date_debut': '2016-01-04T08:30:00',
|
||||
'date_fin': '2016-01-04T12:30:00',
|
||||
'description': 'TP NI9219 Température',
|
||||
'evaluation_type': 0,
|
||||
'id': 15797,
|
||||
'moduleimpl_id': 1234,
|
||||
'note_max': 20.0,
|
||||
'numero': 3,
|
||||
'poids': {
|
||||
'UE1.1': 1.0,
|
||||
'UE1.2': 1.0,
|
||||
'UE1.3': 1.0
|
||||
},
|
||||
'publish_incomplete': False,
|
||||
'visibulletin': True
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -78,13 +69,11 @@ def get_evaluation(evaluation_id: int):
|
||||
@as_json
|
||||
def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations d'un moduleimpl.
|
||||
Retourne la liste des évaluations d'un moduleimpl
|
||||
|
||||
PARAMS
|
||||
------
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat : voir `/evaluation`.
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
||||
@ -98,36 +87,30 @@ def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
Retourne la liste des notes de l'évaluation.
|
||||
Retourne la liste des notes de l'évaluation
|
||||
|
||||
PARAMS
|
||||
------
|
||||
evaluation_id : l'id de l'évaluation
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"11": {
|
||||
{
|
||||
"11": {
|
||||
"etudid": 11,
|
||||
"evaluation_id": 1,
|
||||
"value": 15.0,
|
||||
"note_max" : 20.0,
|
||||
"comment": "",
|
||||
"date": "2024-07-19T19:08:44+02:00",
|
||||
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
"12": {
|
||||
},
|
||||
"12": {
|
||||
"etudid": 12,
|
||||
"evaluation_id": 1,
|
||||
"value": "ABS",
|
||||
"note_max" : 20.0,
|
||||
"value": 12.0,
|
||||
"comment": "",
|
||||
"date": "2024-07-19T19:08:44+02:00",
|
||||
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
|
||||
"uid": 2
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -161,18 +144,13 @@ def evaluation_notes(evaluation_id: int):
|
||||
@as_json
|
||||
def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
||||
"""Écriture de notes dans une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
'notes' : [ [etudid, value], ... ],
|
||||
'comment' : optional string
|
||||
}
|
||||
```
|
||||
|
||||
Résultat:
|
||||
|
||||
Result:
|
||||
- nb_changed: nombre de notes changées
|
||||
- nb_suppress: nombre de notes effacées
|
||||
- etudids_with_decision: liste des etudiants dont la note a changé
|
||||
@ -207,9 +185,8 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
|
||||
@as_json
|
||||
def evaluation_create(moduleimpl_id: int):
|
||||
"""Création d'une évaluation.
|
||||
|
||||
DATA
|
||||
----
|
||||
The request content type should be "application/json",
|
||||
and contains:
|
||||
{
|
||||
"description" : str,
|
||||
"evaluation_type" : int, // {0,1,2} default 0 (normale)
|
||||
@ -222,8 +199,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
"coefficient" : float, // si non spécifié, 1.0
|
||||
"poids" : { ue_id : poids } // optionnel
|
||||
}
|
||||
|
||||
Résultat: l'évaluation créée.
|
||||
Result: l'évaluation créée.
|
||||
"""
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
if not moduleimpl.can_edit_evaluation(current_user):
|
||||
@ -273,7 +249,7 @@ def evaluation_create(moduleimpl_id: int):
|
||||
@as_json
|
||||
def evaluation_delete(evaluation_id: int):
|
||||
"""Suppression d'une évaluation.
|
||||
Efface aussi toutes ses notes.
|
||||
Efface aussi toutes ses notes
|
||||
"""
|
||||
query = Evaluation.query.filter_by(id=evaluation_id)
|
||||
if g.scodoc_dept:
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formations
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Formations
|
||||
"""
|
||||
|
||||
from flask import flash, g, request
|
||||
@ -19,15 +15,12 @@ from flask_login import login_required
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
ApcNiveau,
|
||||
ApcParcours,
|
||||
Formation,
|
||||
Module,
|
||||
UniteEns,
|
||||
)
|
||||
from app.scodoc import sco_formations
|
||||
@ -42,8 +35,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
@as_json
|
||||
def formations():
|
||||
"""
|
||||
Retourne la liste de toutes les formations (tous départements,
|
||||
sauf si route départementale).
|
||||
Retourne la liste de toutes les formations (tous départements)
|
||||
"""
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
@ -63,7 +55,7 @@ def formations_ids():
|
||||
Retourne la liste de toutes les id de formations
|
||||
(tous départements, ou du département indiqué dans la route)
|
||||
|
||||
Exemple de résultat : `[ 17, 99, 32 ]`.
|
||||
Exemple de résultat : [ 17, 99, 32 ]
|
||||
"""
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
@ -79,26 +71,24 @@ def formations_ids():
|
||||
@as_json
|
||||
def formation_by_id(formation_id: int):
|
||||
"""
|
||||
La formation d'id donné.
|
||||
La formation d'id donné
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
```
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1
|
||||
}
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -130,102 +120,97 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
"""
|
||||
Retourne la formation, avec UE, matières, modules
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formation_id : l'id d'une formation
|
||||
export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
|
||||
export_ids : True ou False, si l'on veut ou non exporter les ids
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
"id": 1,
|
||||
"acronyme": "BUT R&T",
|
||||
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
|
||||
"formation_code": "V1RET",
|
||||
"code_specialite": null,
|
||||
"dept_id": 1,
|
||||
"titre": "BUT R&T",
|
||||
"version": 1,
|
||||
"type_parcours": 700,
|
||||
"referentiel_competence_id": null,
|
||||
"formation_id": 1,
|
||||
"ue": [
|
||||
{
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
"acronyme": "RT1.1",
|
||||
"numero": 1,
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"type": 0,
|
||||
"ue_code": "UCOD11",
|
||||
"ects": 12.0,
|
||||
"is_external": false,
|
||||
"code_apogee": "",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"reference": 1,
|
||||
"matiere": [
|
||||
{
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"numero": 1,
|
||||
"module": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
"titre": "Initiation aux r\u00e9seaux informatiques",
|
||||
"abbrev": "Init aux r\u00e9seaux informatiques",
|
||||
"code": "R101",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 2,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "12.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ue_reference": "2",
|
||||
"coef": "4.0"
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"ue_reference": "3",
|
||||
"coef": "4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...",
|
||||
"abbrev": "Hygi\u00e8ne informatique",
|
||||
"code": "SAE11",
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"coefficient": 1.0,
|
||||
"ects": "",
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"code_apogee": "",
|
||||
"module_type": 3,
|
||||
"coefficients": [
|
||||
{
|
||||
"ue_reference": "1",
|
||||
"coef": "16.0"
|
||||
}
|
||||
]
|
||||
...
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
]
|
||||
},
|
||||
...
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
]
|
||||
}
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -248,8 +233,11 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
|
||||
@as_json
|
||||
def referentiel_competences(formation_id: int):
|
||||
"""
|
||||
Retourne le référentiel de compétences de la formation
|
||||
ou null si pas de référentiel associé.
|
||||
Retourne le référentiel de compétences
|
||||
|
||||
formation_id : l'id d'une formation
|
||||
|
||||
return null si pas de référentiel associé.
|
||||
"""
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
@ -260,22 +248,16 @@ def referentiel_competences(formation_id: int):
|
||||
return formation.referentiel_competence.to_dict()
|
||||
|
||||
|
||||
@bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
||||
@api_web_bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
|
||||
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_set_parcours(ue_id: int):
|
||||
def set_ue_parcours(ue_id: int):
|
||||
"""Associe UE et parcours BUT.
|
||||
|
||||
La liste des ids de parcours est passée en argument JSON.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ parcour_id1, parcour_id2, ... ]
|
||||
```
|
||||
JSON arg: [parcour_id1, parcour_id2, ...]
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
@ -288,7 +270,7 @@ def ue_set_parcours(ue_id: int):
|
||||
parcours = [
|
||||
ApcParcours.query.get_or_404(int(parcour_id)) for parcour_id in parcours_ids
|
||||
]
|
||||
log(f"ue_set_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
log(f"set_ue_parcours: ue_id={ue.id} parcours_ids={parcours_ids}")
|
||||
ok, error_message = ue.set_parcours(parcours)
|
||||
if not ok:
|
||||
return json_error(404, error_message)
|
||||
@ -296,19 +278,19 @@ def ue_set_parcours(ue_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
|
||||
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence."""
|
||||
def assoc_ue_niveau(ue_id: int, niveau_id: int):
|
||||
"""Associe l'UE au niveau de compétence"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -325,278 +307,32 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/desassoc_niveau",
|
||||
"/desassoc_ue_niveau/<int:ue_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_desassoc_niveau(ue_id: int):
|
||||
def desassoc_ue_niveau(ue_id: int):
|
||||
"""Désassocie cette UE de son niveau de compétence
|
||||
(si elle n'est pas associée, ne fait rien).
|
||||
(si elle n'est pas associée, ne fait rien)
|
||||
"""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
ok, error_message = ue.set_niveau_competence(None)
|
||||
if not ok:
|
||||
if g.scodoc_dept: # "usage web"
|
||||
flash(error_message, "error")
|
||||
return json_error(404, error_message)
|
||||
if g.scodoc_dept: # "usage web"
|
||||
ue.niveau_competence = None
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
# Invalidation du cache
|
||||
ue.formation.invalidate_cached_sems()
|
||||
log(f"desassoc_ue_niveau: {ue}")
|
||||
if g.scodoc_dept:
|
||||
# "usage web"
|
||||
flash(f"UE {ue.acronyme} dé-associée")
|
||||
return {"status": 0}
|
||||
|
||||
|
||||
@bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
|
||||
@api_web_bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def get_ue(ue_id: int):
|
||||
"""Renvoie l'UE."""
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
return ue.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/formation/module/<int:module_id>", methods=["GET"])
|
||||
@api_web_bp.route("/formation/module/<int:module_id>", methods=["GET"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formation_module_get(module_id: int):
|
||||
"""Renvoie le module."""
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
return module.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@bp.route("/formation/ue/set_code_apogee", methods=["POST"])
|
||||
@api_web_bp.route("/formation/ue/set_code_apogee", methods=["POST"])
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
|
||||
"""Change le code Apogée de l'UE.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si `ue_id` n'est pas spécifié, utilise l'argument oid du POST.
|
||||
Si `code_apogee` n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST.
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if ue_id is None:
|
||||
ue_id = request.form.get("oid")
|
||||
if ue_id is None:
|
||||
return json_error(404, "argument oid manquant")
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(f"API ue_set_code_apogee: ue_id={ue.id} code_apogee={code_apogee}")
|
||||
|
||||
ue.code_apogee = code_apogee
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/set_code_apogee_rcue",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
|
||||
"""Change le code Apogée du RCUE de l'UE.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si code_apogee n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST (utilisé par `jinplace.js`)
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = UniteEns.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
ue: UniteEns = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(f"API ue_set_code_apogee_rcue: ue_id={ue.id} code_apogee={code_apogee}")
|
||||
|
||||
ue.code_apogee_rcue = code_apogee
|
||||
db.session.add(ue)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route("/formation/module/set_code_apogee", methods=["POST"])
|
||||
@api_web_bp.route("/formation/module/set_code_apogee", methods=["POST"])
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/set_code_apogee",
|
||||
defaults={"code_apogee": ""},
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
def formation_module_set_code_apogee(
|
||||
module_id: int | None = None, code_apogee: str = ""
|
||||
):
|
||||
"""Change le code Apogée du module.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur formation verrouillée.
|
||||
|
||||
Si `module_id` n'est pas spécifié, utilise l'argument `oid` du POST.
|
||||
Si `code_apogee` n'est pas spécifié ou vide,
|
||||
utilise l'argument value du POST (utilisé par jinplace.js)
|
||||
|
||||
Le retour est une chaîne (le code enregistré), pas du json.
|
||||
"""
|
||||
if module_id is None:
|
||||
module_id = request.form.get("oid")
|
||||
if module_id is None:
|
||||
return json_error(404, "argument oid manquant")
|
||||
if not code_apogee:
|
||||
code_apogee = request.form.get("value", "")
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
|
||||
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log(
|
||||
f"API formation_module_set_code_apogee: module_id={module.id} code_apogee={code_apogee}"
|
||||
)
|
||||
|
||||
module.code_apogee = code_apogee
|
||||
db.session.add(module)
|
||||
db.session.commit()
|
||||
return code_apogee or ""
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/module/<int:module_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/module/<int:module_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def formation_module_edit(module_id: int):
|
||||
"""Édition d'un module. Renvoie le module en json."""
|
||||
query = Module.query.filter_by(id=module_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
module: Module = query.first_or_404()
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
module.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(module)
|
||||
log(f"API module_edit: module_id={module.id} args={args}")
|
||||
r = module.to_dict(convert_objects=True, with_parcours_ids=True)
|
||||
return r
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/ue/<int:ue_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formation/ue/<int:ue_id>/edit",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EditFormation)
|
||||
@as_json
|
||||
def ue_edit(ue_id: int):
|
||||
"""Édition d'une UE. Renvoie l'UE en json."""
|
||||
ue = UniteEns.get_ue(ue_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
ue.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(ue)
|
||||
log(f"API ue_edit: ue_id={ue.id} args={args}")
|
||||
r = ue.to_dict(convert_objects=True)
|
||||
return r
|
||||
|
@ -6,12 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux formsemestres
|
||||
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
FormSemestre
|
||||
|
||||
"""
|
||||
from operator import attrgetter, itemgetter
|
||||
|
||||
@ -20,10 +14,9 @@ from flask_json import as_json
|
||||
from flask_login import current_user, login_required
|
||||
import sqlalchemy as sa
|
||||
import app
|
||||
from app import db, log
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.comp import res_sem
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
@ -61,37 +54,35 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
formsemestre_id : l'id du formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"block_moyennes": false,
|
||||
"bul_bgcolor": "white",
|
||||
"bul_hide_xml": false,
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_debut": "01/09/2021",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"date_fin": "31/08/2022",
|
||||
"dept_id": 1,
|
||||
"elt_annee_apo": null,
|
||||
"elt_passage_apo" : null,
|
||||
"elt_sem_apo": null,
|
||||
"ens_can_edit_eval": false,
|
||||
"etat": true,
|
||||
"formation_id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"gestion_semestrielle": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"resp_can_change_ens": true,
|
||||
"resp_can_edit": false,
|
||||
"responsables": [1, 99], // uids
|
||||
"scodoc7_id": null,
|
||||
"semestre_id": 1,
|
||||
"titre_formation" : "BUT GEA",
|
||||
"titre_num": "BUT GEA semestre 1",
|
||||
"titre": "BUT GEA",
|
||||
}
|
||||
```
|
||||
{
|
||||
"block_moyennes": false,
|
||||
"bul_bgcolor": "white",
|
||||
"bul_hide_xml": false,
|
||||
"date_debut_iso": "2021-09-01",
|
||||
"date_debut": "01/09/2021",
|
||||
"date_fin_iso": "2022-08-31",
|
||||
"date_fin": "31/08/2022",
|
||||
"dept_id": 1,
|
||||
"elt_annee_apo": null,
|
||||
"elt_sem_apo": null,
|
||||
"ens_can_edit_eval": false,
|
||||
"etat": true,
|
||||
"formation_id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"gestion_compensation": false,
|
||||
"gestion_semestrielle": false,
|
||||
"id": 1,
|
||||
"modalite": "FI",
|
||||
"resp_can_change_ens": true,
|
||||
"resp_can_edit": false,
|
||||
"responsables": [1, 99], // uids
|
||||
"scodoc7_id": null,
|
||||
"semestre_id": 1,
|
||||
"titre_formation" : "BUT GEA",
|
||||
"titre_num": "BUT GEA semestre 1",
|
||||
"titre": "BUT GEA",
|
||||
}
|
||||
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -108,28 +99,15 @@ def formsemestre_infos(formsemestre_id: int):
|
||||
@as_json
|
||||
def formsemestres_query():
|
||||
"""
|
||||
Retourne les formsemestres filtrés par étape Apogée ou année scolaire
|
||||
ou département (acronyme ou id) ou état ou code étudiant.
|
||||
Retourne les formsemestres filtrés par
|
||||
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
|
||||
|
||||
PARAMS
|
||||
------
|
||||
etape_apo : un code étape apogée
|
||||
annee_scolaire : année de début de l'année scolaire
|
||||
dept_acronym : acronyme du département (eg "RT")
|
||||
dept_id : id du département
|
||||
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
||||
etat: 0 si verrouillé, 1 sinon
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etape_apo:<string:etape_apo>
|
||||
annee_scolaire:<string:annee_scolaire>
|
||||
dept_acronym:<string:dept_acronym>
|
||||
dept_id:<int:dept_id>
|
||||
etat:<int:etat>
|
||||
nip:<string:nip>
|
||||
ine:<string:ine>
|
||||
|
||||
"""
|
||||
etape_apo = request.args.get("etape_apo")
|
||||
annee_scolaire = request.args.get("annee_scolaire")
|
||||
@ -199,36 +177,7 @@ def formsemestres_query():
|
||||
@permission_required(Permission.EditFormSemestre)
|
||||
@as_json
|
||||
def formsemestre_edit(formsemestre_id: int):
|
||||
"""Modifie les champs d'un formsemestre.
|
||||
|
||||
On peut spécifier un ou plusieurs champs.
|
||||
|
||||
DATA
|
||||
---
|
||||
```json
|
||||
{
|
||||
"semestre_id" : string,
|
||||
"titre" : string,
|
||||
"date_debut" : date iso,
|
||||
"date_fin" : date iso,
|
||||
"edt_id" : string,
|
||||
"etat" : string,
|
||||
"modalite" : string,
|
||||
"gestion_compensation" : bool,
|
||||
"bul_hide_xml" : bool,
|
||||
"block_moyennes" : bool,
|
||||
"block_moyenne_generale" : bool,
|
||||
"mode_calcul_moyennes" : string,
|
||||
"gestion_semestrielle" : string,
|
||||
"bul_bgcolor" : string,
|
||||
"resp_can_edit" : bool,
|
||||
"resp_can_change_ens" : bool,
|
||||
"ens_can_edit_eval" : bool,
|
||||
"elt_sem_apo" : string,
|
||||
"elt_annee_apo : string,
|
||||
}
|
||||
```
|
||||
"""
|
||||
"""Modifie les champs d'un formsemestre."""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
editable_keys = {
|
||||
@ -260,154 +209,6 @@ def formsemestre_edit(formsemestre_id: int):
|
||||
return formsemestre.to_dict_api()
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_etapes", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_etapes", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_apo_etapes():
|
||||
"""Change les codes étapes du semestre indiqué.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V1RT, V1RT2", codes séparés par des virgules
|
||||
}
|
||||
"""
|
||||
formsemestre_id = int(request.form.get("oid"))
|
||||
etapes_apo_str = request.form.get("value")
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
||||
current_etapes = {e.etape_apo for e in formsemestre.etapes}
|
||||
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
|
||||
|
||||
if new_etapes != current_etapes:
|
||||
formsemestre.etapes = []
|
||||
for etape_apo in new_etapes:
|
||||
etape = FormSemestreEtape(
|
||||
formsemestre_id=formsemestre_id, etape_apo=etape_apo
|
||||
)
|
||||
formsemestre.etapes.append(etape)
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_apo_etapes: formsemestre_id={
|
||||
formsemestre.id} code_apogee={etapes_apo_str}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_elt_sem", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_elt_sem", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_elt_sem_apo():
|
||||
"""Change les codes étapes du semestre indiqué.
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
||||
}
|
||||
```
|
||||
"""
|
||||
oid = int(request.form.get("oid"))
|
||||
value = (request.form.get("value") or "").strip()
|
||||
formsemestre = FormSemestre.get_formsemestre(oid)
|
||||
if value != formsemestre.elt_sem_apo:
|
||||
formsemestre.elt_sem_apo = value
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_elt_sem_apo: formsemestre_id={
|
||||
formsemestre.id} code_apogee={value}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_elt_annee", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_elt_annee", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_elt_annee_apo():
|
||||
"""Change les codes étapes du semestre indiqué (par le champ oid).
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
||||
}
|
||||
```
|
||||
"""
|
||||
oid = int(request.form.get("oid"))
|
||||
value = (request.form.get("value") or "").strip()
|
||||
formsemestre = FormSemestre.get_formsemestre(oid)
|
||||
if value != formsemestre.elt_annee_apo:
|
||||
formsemestre.elt_annee_apo = value
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_elt_annee_apo: formsemestre_id={
|
||||
formsemestre.id} code_apogee={value}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/apo/set_elt_passage", methods=["POST"])
|
||||
@api_web_bp.route("/formsemestre/apo/set_elt_passage", methods=["POST"])
|
||||
@scodoc
|
||||
@permission_required(Permission.EditApogee)
|
||||
def formsemestre_set_elt_passage_apo():
|
||||
"""Change les codes apogée de passage du semestre indiqué (par le champ oid).
|
||||
|
||||
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
||||
par des virgules.
|
||||
|
||||
Ce changement peut être fait sur un semestre verrouillé.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
oid : int, le formsemestre_id
|
||||
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
||||
}
|
||||
```
|
||||
"""
|
||||
oid = int(request.form.get("oid"))
|
||||
value = (request.form.get("value") or "").strip()
|
||||
formsemestre = FormSemestre.get_formsemestre(oid)
|
||||
if value != formsemestre.elt_annee_apo:
|
||||
formsemestre.elt_passage_apo = value
|
||||
db.session.add(formsemestre)
|
||||
db.session.commit()
|
||||
log(
|
||||
f"""API formsemestre_set_elt_passage_apo: formsemestre_id={
|
||||
formsemestre.id} code_apogee={value}"""
|
||||
)
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@ -418,12 +219,9 @@ def formsemestre_set_elt_passage_apo():
|
||||
@as_json
|
||||
def bulletins(formsemestre_id: int, version: str = "long"):
|
||||
"""
|
||||
Retourne les bulletins d'un formsemestre.
|
||||
Retourne les bulletins d'un formsemestre donné
|
||||
|
||||
PARAMS
|
||||
------
|
||||
formsemestre_id : int
|
||||
version : string ("long", "short", "selectedevals")
|
||||
formsemestre_id : l'id d'un formesemestre
|
||||
|
||||
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
|
||||
"""
|
||||
@ -455,67 +253,66 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
```json
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"ens": [ 10, 18 ],
|
||||
"formsemestre_id": 1,
|
||||
"id": 15,
|
||||
"module": {
|
||||
"abbrev": "Programmer",
|
||||
"code": "SAE15",
|
||||
"code_apogee": "V7GOP",
|
||||
"coefficient": 1.0,
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"id": 15,
|
||||
"matiere_id": 3,
|
||||
"module_id": 15,
|
||||
"module_type": 3,
|
||||
"numero": 50,
|
||||
"semestre_id": 1,
|
||||
"titre": "Programmer en Python",
|
||||
"ue_id": 3
|
||||
},
|
||||
"module_id": 15,
|
||||
"moduleimpl_id": 15,
|
||||
"responsable_id": 2
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
"ues": [
|
||||
{
|
||||
"type": 0,
|
||||
"formation_id": 1,
|
||||
"ue_code": "UCOD11",
|
||||
"id": 1,
|
||||
"ects": 12.0,
|
||||
"acronyme": "RT1.1",
|
||||
"is_external": false,
|
||||
"numero": 1,
|
||||
"code_apogee": "",
|
||||
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
||||
"coefficient": 0.0,
|
||||
"semestre_idx": 1,
|
||||
"color": "#B80004",
|
||||
"ue_id": 1
|
||||
},
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
||||
}
|
||||
```
|
||||
],
|
||||
"ressources": [
|
||||
{
|
||||
"ens": [ 10, 18 ],
|
||||
"formsemestre_id": 1,
|
||||
"id": 15,
|
||||
"module": {
|
||||
"abbrev": "Programmer",
|
||||
"code": "SAE15",
|
||||
"code_apogee": "V7GOP",
|
||||
"coefficient": 1.0,
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0.0,
|
||||
"heures_td": 0.0,
|
||||
"heures_tp": 0.0,
|
||||
"id": 15,
|
||||
"matiere_id": 3,
|
||||
"module_id": 15,
|
||||
"module_type": 3,
|
||||
"numero": 50,
|
||||
"semestre_id": 1,
|
||||
"titre": "Programmer en Python",
|
||||
"ue_id": 3
|
||||
},
|
||||
"module_id": 15,
|
||||
"moduleimpl_id": 15,
|
||||
"responsable_id": 2
|
||||
},
|
||||
...
|
||||
],
|
||||
"saes": [
|
||||
{
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
||||
}
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -579,16 +376,7 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
def formsemestre_etudiants(
|
||||
formsemestre_id: int, with_query: bool = False, long: bool = False
|
||||
):
|
||||
"""Étudiants d'un formsemestre.
|
||||
|
||||
Si l'état est spécifié, ne renvoie que les inscrits (`I`), les
|
||||
démissionnaires (`D`) ou les défaillants (`DEF`)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etat:<string:etat>
|
||||
|
||||
"""
|
||||
"""Étudiants d'un formsemestre."""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -630,13 +418,13 @@ def formsemestre_etudiants(
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
def etat_evals(formsemestre_id: int):
|
||||
"""
|
||||
Informations sur l'état des évaluations d'un formsemestre.
|
||||
|
||||
Exemple de résultat :
|
||||
formsemestre_id : l'id d'un semestre
|
||||
|
||||
```json
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"id": 1, // moduleimpl_id
|
||||
@ -664,9 +452,11 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
]
|
||||
},
|
||||
]
|
||||
```
|
||||
"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
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(formsemestre_id)
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
@ -739,16 +529,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_resultat(formsemestre_id: int):
|
||||
"""Tableau récapitulatif des résultats.
|
||||
|
||||
"""Tableau récapitulatif des résultats
|
||||
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
|
||||
|
||||
Si `format=raw`, ne converti pas les valeurs.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
format:<string:format>
|
||||
|
||||
"""
|
||||
format_spec = request.args.get("format", None)
|
||||
if format_spec is not None and format_spec != "raw":
|
||||
@ -788,14 +570,14 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||
return rows
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/groups_get_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/groups_get_auto_assignment")
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def groups_get_auto_assignment(formsemestre_id: int):
|
||||
"""Rend les données stockées par `groups_save_auto_assignment`."""
|
||||
def get_groups_auto_assignment(formsemestre_id: int):
|
||||
"""rend les données"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
@ -806,27 +588,22 @@ def groups_get_auto_assignment(formsemestre_id: int):
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/groups_save_auto_assignment", methods=["POST"]
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/groups_save_auto_assignment", methods=["POST"]
|
||||
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def groups_save_auto_assignment(formsemestre_id: int):
|
||||
"""Enregistre les données, associées à ce formsemestre.
|
||||
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
|
||||
"""
|
||||
def save_groups_auto_assignment(formsemestre_id: int):
|
||||
"""enregistre les données"""
|
||||
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(formsemestre_id)
|
||||
|
||||
if not formsemestre.can_change_groups():
|
||||
return json_error(403, "non autorisé (can_change_groups)")
|
||||
|
||||
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
||||
return json_error(413, "data too large")
|
||||
formsemestre.groups_auto_assignment_data = request.data
|
||||
@ -841,16 +618,11 @@ def groups_save_auto_assignment(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_edt(formsemestre_id: int):
|
||||
"""L'emploi du temps du semestre.
|
||||
|
||||
"""l'emploi du temps du semestre.
|
||||
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
||||
|
||||
Expérimental, ne pas utiliser hors ScoDoc.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
group_ids : string (optionnel) filtre sur les groupes ScoDoc.
|
||||
show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
||||
group_ids permet de filtrer sur les groupes ScoDoc.
|
||||
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
|
@ -5,12 +5,7 @@
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions.
|
||||
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Jury
|
||||
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
|
||||
"""
|
||||
|
||||
import datetime
|
||||
@ -22,8 +17,7 @@ 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, API_CLIENT_ERROR, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
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 (
|
||||
@ -38,7 +32,6 @@ from app.models import (
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cache
|
||||
@ -69,7 +62,7 @@ def decisions_jury(formsemestre_id: int):
|
||||
raise ScoException("non implemente")
|
||||
|
||||
|
||||
def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
@ -78,7 +71,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
obj=etud.id,
|
||||
text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
|
||||
url=url,
|
||||
)
|
||||
|
||||
@ -96,7 +89,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_ue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation d'UE."
|
||||
"Efface cette validation"
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
|
||||
@ -113,7 +106,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def validation_formsemestre_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de semestre."
|
||||
"Efface cette validation"
|
||||
# c'est la même chose (formations classiques)
|
||||
return _validation_ue_delete(etudid, validation_id)
|
||||
|
||||
@ -165,7 +158,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
"Efface cette autorisation d'inscription."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
@ -194,12 +187,8 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
|
||||
@as_json
|
||||
def validation_rcue_record(etudid: int):
|
||||
"""Enregistre une validation de RCUE.
|
||||
|
||||
Si une validation existe déjà pour ce RCUE, la remplace.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"code" : str,
|
||||
"ue1_id" : int,
|
||||
@ -209,7 +198,6 @@ def validation_rcue_record(etudid: int):
|
||||
"date" : date_iso, // si non spécifié, now()
|
||||
"parcours_id" :int,
|
||||
}
|
||||
```
|
||||
"""
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
@ -301,12 +289,13 @@ def validation_rcue_record(etudid: int):
|
||||
db.session.add(validation)
|
||||
# invalider bulletins (les autres résultats ne dépendent pas des RCUEs):
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="validation_rcue_record",
|
||||
etudid=etudid,
|
||||
msg=f"Enregistrement {validation}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"{operation} {validation}")
|
||||
return validation.to_dict()
|
||||
|
||||
@ -324,18 +313,18 @@ def validation_rcue_record(etudid: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de RCUE."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationRCUE.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"delete validation_ue_delete: etuid={etudid} {validation}")
|
||||
log(f"validation_ue_delete: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail="UE")
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
||||
|
||||
@ -352,45 +341,16 @@ def validation_rcue_delete(etudid: int, validation_id: int):
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_annee_but_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation d'année BUT."
|
||||
"Efface cette validation"
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ApcValidationAnnee.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
ordre = validation.ordre
|
||||
log(f"delete validation_annee_but: etuid={etudid} {validation}")
|
||||
log(f"validation_annee_but: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail=f"année BUT{ordre}")
|
||||
return "ok"
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@api_web_bp.route(
|
||||
"/etudiant/<int:etudid>/jury/validation_dut120/<int:validation_id>/delete",
|
||||
methods=["POST"],
|
||||
)
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def validation_dut120_delete(etudid: int, validation_id: int):
|
||||
"Efface cette validation de DUT120."
|
||||
etud = tools.get_etud(etudid)
|
||||
if etud is None:
|
||||
return "étudiant inconnu", 404
|
||||
validation = ValidationDUT120.query.filter_by(
|
||||
id=validation_id, etudid=etudid
|
||||
).first_or_404()
|
||||
log(f"delete validation_dut120: etuid={etudid} {validation}")
|
||||
db.session.delete(validation)
|
||||
sco_cache.invalidate_formsemestre_etud(etud)
|
||||
db.session.commit()
|
||||
_news_delete_jury_etud(etud, detail="diplôme DUT120")
|
||||
_news_delete_jury_etud(etud)
|
||||
return "ok"
|
||||
|
@ -3,8 +3,8 @@
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
"""ScoDoc 9 API : Justificatifs"""
|
||||
|
||||
"""ScoDoc 9 API : Justificatifs
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_json import as_json
|
||||
@ -19,8 +19,7 @@ from app import db, set_sco_dept
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object, tools
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Identite, Justificatif, Departement, FormSemestre, Scolog
|
||||
from app.models.assiduites import (
|
||||
get_formsemestre_from_data,
|
||||
@ -38,11 +37,9 @@ from app.scodoc.sco_groups import get_group_members
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatif(justif_id: int = None):
|
||||
"""Retourne un objet justificatif à partir de son id.
|
||||
"""Retourne un objet justificatif à partir de son id
|
||||
|
||||
Exemple de résultat:
|
||||
|
||||
```json
|
||||
{
|
||||
"justif_id": 1,
|
||||
"etudid": 2,
|
||||
@ -54,11 +51,6 @@ def justificatif(justif_id: int = None):
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1;
|
||||
|
||||
"""
|
||||
|
||||
@ -99,32 +91,28 @@ def justificatif(justif_id: int = None):
|
||||
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
|
||||
"""
|
||||
Retourne toutes les assiduités d'un étudiant
|
||||
chemin : /justificatifs/<int:etudid>
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/1;
|
||||
/justificatifs/1/query?etat=attente;
|
||||
Un filtrage peut être donné avec une query
|
||||
chemin : /justificatifs/<int:etudid>/query?
|
||||
|
||||
Les différents filtres :
|
||||
Etat (etat du justificatif -> validé, non validé, modifé, en attente):
|
||||
query?etat=[- liste des états séparé par une virgule -]
|
||||
ex: .../query?etat=validé,modifié
|
||||
Date debut
|
||||
(date de début du justificatif, sont affichés les justificatifs
|
||||
dont la date de début est supérieur ou égale à la valeur donnée):
|
||||
query?date_debut=[- date au format iso -]
|
||||
ex: query?date_debut=2022-11-03T08:00+01:00
|
||||
Date fin
|
||||
(date de fin du justificatif, sont affichés les justificatifs
|
||||
dont la date de fin est inférieure ou égale à la valeur donnée):
|
||||
query?date_fin=[- date au format iso -]
|
||||
ex: query?date_fin=2022-11-03T10:00+01:00
|
||||
user_id (l'id de l'auteur du justificatif)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
"""
|
||||
# Récupération de l'étudiant
|
||||
etud: Identite = tools.get_etud(etudid, nip, ine)
|
||||
@ -166,32 +154,6 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||
"""
|
||||
Renvoie tous les justificatifs d'un département
|
||||
(en ajoutant un champ "formsemestre" si possible)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/dept/1;
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du département et des étudiants du département
|
||||
@ -263,34 +225,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
"""Retourne tous les justificatifs du formsemestre
|
||||
|
||||
QUERY
|
||||
-----
|
||||
user_id:<int:user_id>
|
||||
est_just:<bool:est_just>
|
||||
date_debut:<string:date_debut_iso>
|
||||
date_fin:<string:date_fin_iso>
|
||||
etat:<array[string]:etat>
|
||||
order:<bool:order>
|
||||
courant:<bool:courant>
|
||||
group_id:<int:group_id>
|
||||
|
||||
PARAMS
|
||||
-----
|
||||
user_id:l'id de l'auteur du justificatif
|
||||
date_debut:date de début du justificatif (supérieur ou égal)
|
||||
date_fin:date de fin du justificatif (inférieur ou égal)
|
||||
etat:etat du justificatif → valide, non_valide, attente, modifie
|
||||
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
|
||||
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
|
||||
group_id:<int:group_id>
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatifs/formsemestre/1;
|
||||
|
||||
"""
|
||||
"""Retourne tous les justificatifs du formsemestre"""
|
||||
|
||||
# Récupération du formsemestre
|
||||
formsemestre: FormSemestre = None
|
||||
@ -338,10 +273,7 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
"""
|
||||
Création d'un justificatif pour l'étudiant (etudid)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
La requête doit avoir un content type "application/json":
|
||||
[
|
||||
{
|
||||
"date_debut": str,
|
||||
@ -356,10 +288,6 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
}
|
||||
...
|
||||
]
|
||||
```
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/create;[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""attente""}]
|
||||
|
||||
"""
|
||||
|
||||
@ -413,10 +341,6 @@ def _create_one(
|
||||
errors.append("param 'etat': invalide")
|
||||
|
||||
etat: scu.EtatJustificatif = scu.EtatJustificatif.get(etat)
|
||||
if etat != scu.EtatJustificatif.ATTENTE and not current_user.has_permission(
|
||||
Permission.JustifValidate
|
||||
):
|
||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
||||
|
||||
# cas 2 : date_debut
|
||||
date_debut: str = data.get("date_debut", None)
|
||||
@ -490,23 +414,14 @@ def _create_one(
|
||||
def justif_edit(justif_id: int):
|
||||
"""
|
||||
Edition d'un justificatif à partir de son id
|
||||
La requête doit avoir un content type "application/json":
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"etat"?: str,
|
||||
"raison"?: str
|
||||
"date_debut"?: str
|
||||
"date_fin"?: str
|
||||
}
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/edit;{""etat"":""valide""}
|
||||
/justificatif/1/edit;{""raison"":""MEDIC""}
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du justificatif à modifier
|
||||
@ -525,10 +440,7 @@ def justif_edit(justif_id: int):
|
||||
if etat is None:
|
||||
errors.append("param 'etat': invalide")
|
||||
else:
|
||||
if current_user.has_permission(Permission.JustifValidate):
|
||||
justificatif_unique.etat = etat
|
||||
else:
|
||||
errors.append("param 'etat': non autorisé (Permission.JustifValidate)")
|
||||
justificatif_unique.etat = etat
|
||||
|
||||
# Cas 2 : raison
|
||||
raison: str = data.get("raison", False)
|
||||
@ -581,12 +493,13 @@ def justif_edit(justif_id: int):
|
||||
# Mise à jour du justificatif
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
|
||||
Scolog.logdb(
|
||||
method="edit_justificatif",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg=f"justificatif modif: {justificatif_unique}",
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
# Génération du dictionnaire de retour
|
||||
# La couverture correspond
|
||||
@ -613,18 +526,13 @@ def justif_delete():
|
||||
"""
|
||||
Suppression d'un justificatif à partir de son id
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
Forme des données envoyées :
|
||||
|
||||
[
|
||||
<justif_id:int>,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/delete;[2, 2, 3]
|
||||
|
||||
"""
|
||||
|
||||
@ -679,11 +587,6 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
scass.simple_invalidate_cache(justificatif_unique.to_dict())
|
||||
# On actualise les assiduités justifiées de l'étudiant concerné
|
||||
justificatif_unique.dejustifier_assiduites()
|
||||
Scolog.logdb(
|
||||
method="justificatif/delete",
|
||||
etudid=justificatif_unique.etudiant.id,
|
||||
msg="suppression justificatif",
|
||||
)
|
||||
# On supprime le justificatif
|
||||
db.session.delete(justificatif_unique)
|
||||
|
||||
@ -700,8 +603,6 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
|
||||
def justif_import(justif_id: int = None):
|
||||
"""
|
||||
Importation d'un fichier (création d'archive)
|
||||
|
||||
> Procédure d'importation de fichier : [importer un justificatif](FichiersJustificatifs.md#importer-un-fichier)
|
||||
"""
|
||||
|
||||
# On vérifie qu'un fichier a bien été envoyé
|
||||
@ -753,8 +654,6 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
||||
"""
|
||||
Retourne un fichier d'une archive d'un justificatif.
|
||||
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
|
||||
|
||||
> Procédure de téléchargement de fichier : [télécharger un justificatif](FichiersJustificatifs.md#télécharger-un-fichier)
|
||||
"""
|
||||
# On récupère le justificatif concerné
|
||||
justificatif_unique = Justificatif.get_justificatif(justif_id)
|
||||
@ -792,20 +691,14 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
|
||||
def justif_remove(justif_id: int = None):
|
||||
"""
|
||||
Supression d'un fichier ou d'une archive
|
||||
|
||||
> Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier)
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
{
|
||||
"remove": <"all"/"list">,
|
||||
"remove": <"all"/"list">
|
||||
|
||||
"filenames"?: [
|
||||
<filename:str>,
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
# On récupère le dictionnaire
|
||||
@ -871,11 +764,6 @@ def justif_remove(justif_id: int = None):
|
||||
def justif_list(justif_id: int = None):
|
||||
"""
|
||||
Liste les fichiers du justificatif
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/list;
|
||||
|
||||
"""
|
||||
|
||||
# Récupération du justificatif concerné
|
||||
@ -918,11 +806,6 @@ def justif_list(justif_id: int = None):
|
||||
def justif_justifies(justif_id: int = None):
|
||||
"""
|
||||
Liste assiduite_id justifiées par le justificatif
|
||||
|
||||
SAMPLES
|
||||
-------
|
||||
/justificatif/1/justifies;
|
||||
|
||||
"""
|
||||
|
||||
# On récupère le justificatif concerné
|
||||
|
@ -34,13 +34,11 @@ from flask import Response, send_file
|
||||
from flask_json import as_json
|
||||
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import Departement
|
||||
from app.scodoc.sco_logos import list_logos, find_logo
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
# Note: l'API logos n'est accessible qu'en mode global (avec jeton, sans dept)
|
||||
|
||||
@ -49,8 +47,8 @@ from app.scodoc.sco_utils import json_error
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_list_globals():
|
||||
"""Liste des noms des logos définis pour le site ScoDoc."""
|
||||
def api_get_glob_logos():
|
||||
"""Liste tous les logos"""
|
||||
logos = list_logos()[None]
|
||||
return list(logos.keys())
|
||||
|
||||
@ -58,12 +56,7 @@ def logo_list_globals():
|
||||
@bp.route("/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_global(logoname):
|
||||
"""Renvoie le logo global de nom donné.
|
||||
|
||||
L'image est au format png ou jpg; le format retourné dépend du format sous lequel
|
||||
l'image a été initialement enregistrée.
|
||||
"""
|
||||
def api_get_glob_logo(logoname):
|
||||
logo = find_logo(logoname=logoname)
|
||||
if logo is None:
|
||||
return json_error(404, message="logo not found")
|
||||
@ -84,10 +77,7 @@ def _core_get_logos(dept_id) -> list:
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_get_local_by_acronym(departement):
|
||||
"""Liste des noms des logos définis pour le département
|
||||
désigné par son acronyme.
|
||||
"""
|
||||
def api_get_local_logos_by_acronym(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
@ -96,10 +86,7 @@ def logo_get_local_by_acronym(departement):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def logo_get_local_by_id(dept_id):
|
||||
"""Liste des noms des logos définis pour le département
|
||||
désigné par son id.
|
||||
"""
|
||||
def api_get_local_logos_by_id(dept_id):
|
||||
return _core_get_logos(dept_id)
|
||||
|
||||
|
||||
@ -118,13 +105,7 @@ def _core_get_logo(dept_id, logoname) -> Response:
|
||||
@bp.route("/departement/<string:departement>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_local_dept_by_acronym(departement, logoname):
|
||||
"""Le logo: image (format png ou jpg).
|
||||
|
||||
**Exemple d'utilisation:**
|
||||
|
||||
* `/ScoDoc/api/departement/MMI/logo/header`
|
||||
"""
|
||||
def api_get_local_logo_dept_by_acronym(departement, logoname):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
||||
@ -132,11 +113,5 @@ def logo_get_local_dept_by_acronym(departement, logoname):
|
||||
@bp.route("/departement/id/<int:dept_id>/logo/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
def logo_get_local_dept_by_id(dept_id, logoname):
|
||||
"""Le logo: image (format png ou jpg).
|
||||
|
||||
**Exemple d'utilisation:**
|
||||
|
||||
* `/ScoDoc/api/departement/id/3/logo/header`
|
||||
"""
|
||||
def api_get_local_logo_dept_by_id(dept_id, logoname):
|
||||
return _core_get_logo(dept_id, logoname)
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux moduleimpl
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
ModuleImpl
|
||||
"""
|
||||
|
||||
from flask_json import as_json
|
||||
@ -17,8 +13,7 @@ from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_liste_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
@ -32,43 +27,38 @@ from app.scodoc.sco_permissions import Permission
|
||||
@as_json
|
||||
def moduleimpl(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne le moduleimpl.
|
||||
Retourne un moduleimpl en fonction de son id
|
||||
|
||||
PARAMS
|
||||
------
|
||||
moduleimpl_id : l'id d'un moduleimpl
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"formsemestre_id": 1,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"moduleimpl_id": 1,
|
||||
"ens": [],
|
||||
"module": {
|
||||
"heures_tp": 0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux réseaux informatiques",
|
||||
"coefficient": 1,
|
||||
"module_type": 2,
|
||||
{
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux réseaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
"formsemestre_id": 1,
|
||||
"module_id": 1,
|
||||
"responsable_id": 2,
|
||||
"moduleimpl_id": 1,
|
||||
"ens": [],
|
||||
"module": {
|
||||
"heures_tp": 0,
|
||||
"code_apogee": "",
|
||||
"titre": "Initiation aux réseaux informatiques",
|
||||
"coefficient": 1,
|
||||
"module_type": 2,
|
||||
"id": 1,
|
||||
"ects": null,
|
||||
"abbrev": "Init aux réseaux informatiques",
|
||||
"ue_id": 1,
|
||||
"code": "R101",
|
||||
"formation_id": 1,
|
||||
"heures_cours": 0,
|
||||
"matiere_id": 1,
|
||||
"heures_td": 0,
|
||||
"semestre_id": 1,
|
||||
"numero": 10,
|
||||
"module_id": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return modimpl.to_dict(convert_objects=True)
|
||||
@ -81,20 +71,16 @@ def moduleimpl(moduleimpl_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
"""Liste des inscriptions à ce moduleimpl.
|
||||
|
||||
"""Liste des inscriptions à ce moduleimpl
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"etudid": 666,
|
||||
"moduleimpl_id": 1234,
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"etudid": 666,
|
||||
"moduleimpl_id": 1234,
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [i.to_dict() for i in modimpl.inscriptions]
|
||||
@ -106,26 +92,22 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def moduleimpl_notes(moduleimpl_id: int):
|
||||
"""Liste des notes dans ce moduleimpl.
|
||||
|
||||
"""Liste des notes dans ce moduleimpl
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"etudid": 17776, // code de l'étudiant
|
||||
"nom": "DUPONT",
|
||||
"prenom": "Luz",
|
||||
"38411": 16.0, // Note dans l'évaluation d'id 38411
|
||||
"38410": 15.0,
|
||||
"moymod": 15.5, // Moyenne INDICATIVE module
|
||||
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
|
||||
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
|
||||
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
[
|
||||
{
|
||||
"etudid": 17776, // code de l'étudiant
|
||||
"nom": "DUPONT",
|
||||
"prenom": "Luz",
|
||||
"38411": 16.0, // Note dans l'évaluation d'id 38411
|
||||
"38410": 15.0,
|
||||
"moymod": 15.5, // Moyenne INDICATIVE module
|
||||
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
|
||||
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
|
||||
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
app.set_sco_dept(modimpl.formsemestre.departement.acronym)
|
||||
|
@ -6,11 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : partitions
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Groupes et Partitions
|
||||
|
||||
"""
|
||||
from operator import attrgetter
|
||||
|
||||
@ -23,8 +18,7 @@ from sqlalchemy.exc import IntegrityError
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.decorators import scodoc
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.models import GroupDescr, Partition, Scolog
|
||||
@ -46,8 +40,7 @@ def partition_info(partition_id: int):
|
||||
"""Info sur une partition.
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
```
|
||||
{
|
||||
'bul_show_rank': False,
|
||||
'formsemestre_id': 39,
|
||||
@ -77,11 +70,10 @@ def partition_info(partition_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_partitions(formsemestre_id: int):
|
||||
"""Liste de toutes les partitions d'un formsemestre.
|
||||
"""Liste de toutes les partitions d'un formsemestre
|
||||
|
||||
Exemple de résultat :
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
|
||||
```json
|
||||
{
|
||||
partition_id : {
|
||||
"bul_show_rank": False,
|
||||
@ -95,7 +87,7 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -115,18 +107,13 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_etudiants(group_id: int):
|
||||
def etud_in_group(group_id: int):
|
||||
"""
|
||||
Retourne la liste des étudiants dans un groupe
|
||||
(inscrits au groupe et inscrits au semestre).
|
||||
|
||||
PARAMS
|
||||
------
|
||||
group_id : l'id d'un groupe
|
||||
|
||||
Exemple de résultat :
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
'civilite': 'M',
|
||||
@ -139,7 +126,6 @@ def group_etudiants(group_id: int):
|
||||
},
|
||||
...
|
||||
]
|
||||
```
|
||||
"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -164,14 +150,8 @@ def group_etudiants(group_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_etudiants_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état (aucun, `I`, `D`, `DEF`)
|
||||
|
||||
QUERY
|
||||
-----
|
||||
etat : string
|
||||
|
||||
"""
|
||||
def etud_in_group_query(group_id: int):
|
||||
"""Étudiants du groupe, filtrés par état"""
|
||||
etat = request.args.get("etat")
|
||||
if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}:
|
||||
return json_error(API_CLIENT_ERROR, "etat: valeur invalide")
|
||||
@ -198,8 +178,8 @@ def group_etudiants_query(group_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_set_etudiant(group_id: int, etudid: int):
|
||||
"""Affecte l'étudiant au groupe indiqué."""
|
||||
def set_etud_group(etudid: int, group_id: int):
|
||||
"""Affecte l'étudiant au groupe indiqué"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -261,8 +241,7 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_remove_etud(partition_id: int, etudid: int):
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition.
|
||||
|
||||
"""Enlève l'étudiant de tous les groupes de cette partition
|
||||
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
|
||||
"""
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
@ -307,15 +286,12 @@ def partition_remove_etud(partition_id: int, etudid: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
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
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"group_name" : nom_du_groupe,
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -362,7 +338,7 @@ def group_create(partition_id: int): # partition-group-create
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_delete(group_id: int):
|
||||
"""Suppression d'un groupe."""
|
||||
"""Suppression d'un groupe"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -391,7 +367,7 @@ def group_delete(group_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_edit(group_id: int):
|
||||
"""Édition d'un groupe."""
|
||||
"""Edit a group"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
@ -432,10 +408,9 @@ def group_edit(group_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_set_edt_id(group_id: int, edt_id: str):
|
||||
"""Set edt_id du groupe.
|
||||
|
||||
Contrairement à `/edit`, peut-être changé pour toute partition
|
||||
d'un formsemestre non verrouillé.
|
||||
"""Set edt_id for this group.
|
||||
Contrairement à /edit, peut-être changé pour toute partition
|
||||
ou formsemestre non verrouillé.
|
||||
"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
@ -461,19 +436,16 @@ def group_set_edt_id(group_id: int, edt_id: str):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_create(formsemestre_id: int):
|
||||
"""Création d'une partition dans un semestre.
|
||||
"""Création d'une partition dans un semestre
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"partition_name": str,
|
||||
"numero": int,
|
||||
"bul_show_rank": bool,
|
||||
"show_in_lists": bool,
|
||||
"groups_editable": bool
|
||||
"numero":int,
|
||||
"bul_show_rank":bool,
|
||||
"show_in_lists":bool,
|
||||
"groups_editable":bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -528,14 +500,9 @@ def partition_create(formsemestre_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ partition_id1, partition_id2, ... ]
|
||||
```
|
||||
def formsemestre_order_partitions(formsemestre_id: int):
|
||||
"""Modifie l'ordre des partitions du formsemestre
|
||||
JSON args: [partition_id1, partition_id2, ...]
|
||||
"""
|
||||
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||
if g.scodoc_dept:
|
||||
@ -546,7 +513,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
if not formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
partition_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(partition_ids, list) and not all(
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
isinstance(x, int) for x in partition_ids
|
||||
):
|
||||
return json_error(
|
||||
@ -560,7 +527,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
db.session.commit()
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
log(f"formsemestre_set_partitions_order({partition_ids})")
|
||||
log(f"formsemestre_order_partitions({partition_ids})")
|
||||
return [
|
||||
partition.to_dict()
|
||||
for partition in formsemestre.partitions.order_by(Partition.numero)
|
||||
@ -575,13 +542,8 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_order_groups(partition_id: int):
|
||||
"""Modifie l'ordre des groupes de la partition.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
[ group_id1, group_id2, ... ]
|
||||
```
|
||||
"""Modifie l'ordre des groupes de la partition
|
||||
JSON args: [group_id1, group_id2, ...]
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -592,7 +554,7 @@ def partition_order_groups(partition_id: int):
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
group_ids = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if not isinstance(group_ids, list) and not all(
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
isinstance(x, int) for x in group_ids
|
||||
):
|
||||
return json_error(
|
||||
@ -617,13 +579,10 @@ def partition_order_groups(partition_id: int):
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def partition_edit(partition_id: int):
|
||||
"""Modification d'une partition dans un semestre.
|
||||
"""Modification d'une partition dans un semestre
|
||||
|
||||
Tous les champs sont optionnels.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json"
|
||||
All fields are optional:
|
||||
{
|
||||
"partition_name": str,
|
||||
"numero":int,
|
||||
@ -631,7 +590,6 @@ def partition_edit(partition_id: int):
|
||||
"show_in_lists":bool,
|
||||
"groups_editable":bool
|
||||
}
|
||||
```
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
if g.scodoc_dept:
|
||||
@ -695,9 +653,9 @@ def partition_edit(partition_id: int):
|
||||
def partition_delete(partition_id: int):
|
||||
"""Suppression d'une partition (et de tous ses groupes).
|
||||
|
||||
* Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
|
||||
Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
|
||||
pas être supprimée.
|
||||
* Note 2: Si la partition de parcours est supprimée, les étudiants
|
||||
Note 2: Si la partition de parcours est supprimée, les étudiants
|
||||
sont désinscrits des parcours.
|
||||
"""
|
||||
query = Partition.query.filter_by(id=partition_id)
|
||||
|
@ -3,18 +3,12 @@ from app import db, log
|
||||
from app.api import api_bp as bp
|
||||
from app.auth.logic import basic_auth, token_auth
|
||||
|
||||
"""
|
||||
CATEGORY
|
||||
--------
|
||||
Authentification API
|
||||
"""
|
||||
|
||||
|
||||
@bp.route("/tokens", methods=["POST"])
|
||||
@basic_auth.login_required
|
||||
@as_json
|
||||
def token_get():
|
||||
"Renvoie un jeton jwt pour l'utilisateur courant."
|
||||
def get_token():
|
||||
"renvoie un jeton jwt pour l'utilisateur courant"
|
||||
token = basic_auth.current_user().get_token()
|
||||
log(f"API: giving token to {basic_auth.current_user()}")
|
||||
db.session.commit()
|
||||
@ -23,8 +17,8 @@ def token_get():
|
||||
|
||||
@bp.route("/tokens", methods=["DELETE"])
|
||||
@token_auth.login_required
|
||||
def token_revoke():
|
||||
"Révoque le jeton de l'utilisateur courant."
|
||||
def revoke_token():
|
||||
"révoque le jeton de l'utilisateur courant"
|
||||
user = token_auth.current_user()
|
||||
user.revoke_token()
|
||||
db.session.commit()
|
||||
|
@ -6,10 +6,6 @@
|
||||
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
|
||||
CATEGORY
|
||||
--------
|
||||
Utilisateurs
|
||||
"""
|
||||
|
||||
from flask import g, request
|
||||
@ -18,14 +14,15 @@ from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.api import api_permission_required as permission_required
|
||||
from app.auth.models import User, Role, UserRole
|
||||
from app.auth.models import is_valid_password
|
||||
from app.decorators import scodoc
|
||||
from app.models import Departement
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, ScoDocSiteConfig
|
||||
from app.scodoc import sco_edt_cal
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@bp.route("/user/<int:uid>")
|
||||
@ -36,7 +33,7 @@ from app.scodoc.sco_utils import json_error
|
||||
@as_json
|
||||
def user_info(uid: int):
|
||||
"""
|
||||
Info sur un compte utilisateur ScoDoc.
|
||||
Info sur un compte utilisateur scodoc
|
||||
"""
|
||||
user: User = db.session.get(User, uid)
|
||||
if user is None:
|
||||
@ -57,22 +54,11 @@ def user_info(uid: int):
|
||||
@as_json
|
||||
def users_info_query():
|
||||
"""Utilisateurs, filtrés par dept, active ou début nom
|
||||
|
||||
Exemple:
|
||||
```
|
||||
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
|
||||
```
|
||||
|
||||
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés.
|
||||
Si accès via API web, le département de l'URL est ignoré, seules
|
||||
les permissions de l'utilisateur sont prises en compte.
|
||||
|
||||
QUERY
|
||||
-----
|
||||
active: bool
|
||||
departement: string
|
||||
starts_with: string
|
||||
|
||||
"""
|
||||
query = User.query
|
||||
active = request.args.get("active")
|
||||
@ -121,10 +107,7 @@ def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
||||
@as_json
|
||||
def user_create():
|
||||
"""Création d'un utilisateur
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"active":bool (default True),
|
||||
"dept": str or null,
|
||||
@ -133,7 +116,6 @@ def user_create():
|
||||
"user_name": str,
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = args.get("user_name")
|
||||
@ -170,10 +152,8 @@ def user_create():
|
||||
@permission_required(Permission.UsersAdmin)
|
||||
@as_json
|
||||
def user_edit(uid: int):
|
||||
"""Modification d'un utilisateur.
|
||||
|
||||
"""Modification d'un utilisateur
|
||||
Champs modifiables:
|
||||
```json
|
||||
{
|
||||
"dept": str or null,
|
||||
"nom": str,
|
||||
@ -181,7 +161,6 @@ def user_edit(uid: int):
|
||||
"active":bool
|
||||
...
|
||||
}
|
||||
```
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user: User = User.query.get_or_404(uid)
|
||||
@ -220,15 +199,11 @@ def user_edit(uid: int):
|
||||
@permission_required(Permission.UsersAdmin)
|
||||
@as_json
|
||||
def user_password(uid: int):
|
||||
"""Modification du mot de passe d'un utilisateur.
|
||||
|
||||
"""Modification du mot de passe d'un utilisateur
|
||||
Champs modifiables:
|
||||
```json
|
||||
{
|
||||
"password": str
|
||||
}
|
||||
```.
|
||||
|
||||
Si le mot de passe ne convient pas, erreur 400.
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -262,7 +237,7 @@ def user_password(uid: int):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
"""Ajoute un rôle à l'utilisateur dans le département donné."""
|
||||
"""Add a role in the given dept to the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
@ -291,7 +266,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
"""Retire le rôle (dans le département donné) à cet utilisateur."""
|
||||
"""Remove the role (in the given dept) from the user"""
|
||||
user: User = User.query.get_or_404(uid)
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
if dept is not None: # check
|
||||
@ -317,8 +292,8 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def permissions_list():
|
||||
"""Liste des noms de permissions définies."""
|
||||
def list_permissions():
|
||||
"""Liste des noms de permissions définies"""
|
||||
return list(Permission.permission_by_name.keys())
|
||||
|
||||
|
||||
@ -328,7 +303,7 @@ def permissions_list():
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def role_get(role_name: str):
|
||||
def list_role(role_name: str):
|
||||
"""Un rôle"""
|
||||
return Role.query.filter_by(name=role_name).first_or_404().to_dict()
|
||||
|
||||
@ -339,8 +314,8 @@ def role_get(role_name: str):
|
||||
@scodoc
|
||||
@permission_required(Permission.UsersView)
|
||||
@as_json
|
||||
def roles_list():
|
||||
"""Tous les rôles définis."""
|
||||
def list_roles():
|
||||
"""Tous les rôles définis"""
|
||||
return [role.to_dict() for role in Role.query]
|
||||
|
||||
|
||||
@ -357,7 +332,7 @@ def roles_list():
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_add(role_name: str, perm_name: str):
|
||||
"""Ajoute une permission à un rôle."""
|
||||
"""Add permission to role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
permission = Permission.get_by_name(perm_name)
|
||||
if permission is None:
|
||||
@ -382,7 +357,7 @@ def role_permission_add(role_name: str, perm_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_permission_remove(role_name: str, perm_name: str):
|
||||
"""Retire une permission d'un rôle."""
|
||||
"""Remove permission from role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
permission = Permission.get_by_name(perm_name)
|
||||
if permission is None:
|
||||
@ -401,15 +376,10 @@ def role_permission_remove(role_name: str, perm_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_create(role_name: str):
|
||||
"""Création d'un nouveau rôle avec les permissions données.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
"""Create a new role with permissions.
|
||||
{
|
||||
"permissions" : [ 'ScoView', ... ]
|
||||
}
|
||||
```
|
||||
"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first()
|
||||
if role:
|
||||
@ -434,16 +404,11 @@ def role_create(role_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_edit(role_name: str):
|
||||
"""Édition d'un rôle. On peut spécifier un nom et/ou des permissions.
|
||||
|
||||
DATA
|
||||
----
|
||||
```json
|
||||
"""Edit a role. On peut spécifier un nom et/ou des permissions.
|
||||
{
|
||||
"name" : name
|
||||
"permissions" : [ 'ScoView', ... ]
|
||||
}
|
||||
```
|
||||
"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
@ -471,7 +436,7 @@ def role_edit(role_name: str):
|
||||
@permission_required(Permission.ScoSuperAdmin)
|
||||
@as_json
|
||||
def role_delete(role_name: str):
|
||||
"""Suprression d'un rôle."""
|
||||
"""Delete a role"""
|
||||
role: Role = Role.query.filter_by(name=role_name).first_or_404()
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
|
@ -35,9 +35,9 @@ def after_cas_login():
|
||||
if user.cas_allow_login:
|
||||
current_app.logger.info(f"CAS: login {user.user_name}")
|
||||
if login_user(user):
|
||||
flask.session["scodoc_cas_login_date"] = (
|
||||
datetime.datetime.now().isoformat()
|
||||
)
|
||||
flask.session[
|
||||
"scodoc_cas_login_date"
|
||||
] = datetime.datetime.now().isoformat()
|
||||
user.cas_last_login = datetime.datetime.utcnow()
|
||||
if flask.session.get("CAS_EDT_ID"):
|
||||
# essaie de récupérer l'edt_id s'il est présent
|
||||
@ -45,10 +45,8 @@ def after_cas_login():
|
||||
# via l'expression `cas_edt_id_from_xml_regexp`
|
||||
# voir flask_cas.routing
|
||||
edt_id = flask.session.get("CAS_EDT_ID")
|
||||
current_app.logger.info(
|
||||
f"""after_cas_login: storing edt_id for {
|
||||
user.user_name}: '{edt_id}'"""
|
||||
)
|
||||
current_app.logger.info(f"""after_cas_login: storing edt_id for {
|
||||
user.user_name}: '{edt_id}'""")
|
||||
user.edt_id = edt_id
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
@ -57,17 +55,12 @@ def after_cas_login():
|
||||
current_app.logger.info(
|
||||
f"CAS login denied for {user.user_name} (not allowed to use CAS)"
|
||||
)
|
||||
else: # pas d'utilisateur ScoDoc ou bien compte inactif
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS login denied for {
|
||||
user.user_name if user else ""
|
||||
} cas_id={cas_id} (unknown or inactive)"""
|
||||
)
|
||||
if ScoDocSiteConfig.is_cas_forced():
|
||||
# Dans ce cas, pas de redirect vers la page de login pour éviter de boucler
|
||||
raise ScoValueError(
|
||||
"compte ScoDoc inexistant ou inactif pour cet utilisateur CAS"
|
||||
)
|
||||
else:
|
||||
current_app.logger.info(
|
||||
f"""CAS attribute '{ScoDocSiteConfig.get("cas_attribute_id")}' not found !
|
||||
|
@ -14,15 +14,6 @@ import cracklib # pylint: disable=import-error
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
from sqlalchemy.exc import (
|
||||
IntegrityError,
|
||||
DataError,
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
StatementError,
|
||||
InterfaceError,
|
||||
)
|
||||
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
|
||||
@ -57,13 +48,13 @@ def is_valid_password(cleartxt) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is valid"
|
||||
def invalid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is invalid"
|
||||
return (
|
||||
user_name
|
||||
and (len(user_name) >= 2)
|
||||
and (len(user_name) < USERNAME_STR_LEN)
|
||||
and VALID_LOGIN_EXP.match(user_name)
|
||||
not user_name
|
||||
or (len(user_name) < 2)
|
||||
or (len(user_name) >= USERNAME_STR_LEN)
|
||||
or not VALID_LOGIN_EXP.match(user_name)
|
||||
)
|
||||
|
||||
|
||||
@ -132,7 +123,7 @@ class User(UserMixin, ScoDocModel):
|
||||
# check login:
|
||||
if not "user_name" in kwargs:
|
||||
raise ValueError("missing user_name argument")
|
||||
if not is_valid_user_name(kwargs["user_name"]):
|
||||
if invalid_user_name(kwargs["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
||||
@ -338,8 +329,7 @@ class User(UserMixin, ScoDocModel):
|
||||
if new_user:
|
||||
if "user_name" in data:
|
||||
# never change name of existing users
|
||||
# (see change_user_name method to do that)
|
||||
if not is_valid_user_name(data["user_name"]):
|
||||
if invalid_user_name(data["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
||||
self.user_name = data["user_name"]
|
||||
if "password" in data:
|
||||
@ -532,64 +522,6 @@ class User(UserMixin, ScoDocModel):
|
||||
|
||||
# nomnoacc était le nom en minuscules sans accents (inutile)
|
||||
|
||||
def change_user_name(self, new_user_name: str):
|
||||
"""Modify user name, update all relevant tables.
|
||||
commit session.
|
||||
"""
|
||||
# Safety check
|
||||
new_user_name = new_user_name.strip()
|
||||
if (
|
||||
not is_valid_user_name(new_user_name)
|
||||
or User.query.filter_by(user_name=new_user_name).count() > 0
|
||||
):
|
||||
raise ValueError("invalid user_name")
|
||||
# Le user_name est utilisé dans d'autres tables (sans être une clé)
|
||||
# BulAppreciations.author
|
||||
# EntrepriseHistorique.authenticated_user
|
||||
# EtudAnnotation.author
|
||||
# ScolarNews.authenticated_user
|
||||
# Scolog.authenticated_user
|
||||
from app.models import (
|
||||
BulAppreciations,
|
||||
EtudAnnotation,
|
||||
ScolarNews,
|
||||
Scolog,
|
||||
)
|
||||
from app.entreprises.models import EntrepriseHistorique
|
||||
|
||||
try:
|
||||
# Update all instances of EtudAnnotation
|
||||
db.session.query(BulAppreciations).filter(
|
||||
BulAppreciations.author == self.user_name
|
||||
).update({BulAppreciations.author: new_user_name})
|
||||
db.session.query(EntrepriseHistorique).filter(
|
||||
EntrepriseHistorique.authenticated_user == self.user_name
|
||||
).update({EntrepriseHistorique.authenticated_user: new_user_name})
|
||||
db.session.query(EtudAnnotation).filter(
|
||||
EtudAnnotation.author == self.user_name
|
||||
).update({EtudAnnotation.author: new_user_name})
|
||||
db.session.query(ScolarNews).filter(
|
||||
ScolarNews.authenticated_user == self.user_name
|
||||
).update({ScolarNews.authenticated_user: new_user_name})
|
||||
db.session.query(Scolog).filter(
|
||||
Scolog.authenticated_user == self.user_name
|
||||
).update({Scolog.authenticated_user: new_user_name})
|
||||
# And update ourself:
|
||||
self.user_name = new_user_name
|
||||
db.session.add(self)
|
||||
db.session.commit()
|
||||
except (
|
||||
IntegrityError,
|
||||
DataError,
|
||||
DatabaseError,
|
||||
OperationalError,
|
||||
ProgrammingError,
|
||||
StatementError,
|
||||
InterfaceError,
|
||||
) as exc:
|
||||
db.session.rollback()
|
||||
raise exc
|
||||
|
||||
|
||||
class AnonymousUser(AnonymousUserMixin):
|
||||
"Notre utilisateur anonyme"
|
||||
|
@ -18,7 +18,7 @@ from app.auth.forms import (
|
||||
ResetPasswordRequestForm,
|
||||
UserCreationForm,
|
||||
)
|
||||
from app.auth.models import Role, User, is_valid_user_name
|
||||
from app.auth.models import Role, User, invalid_user_name
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
from app.forms.generic import SimpleConfirmationForm
|
||||
@ -35,12 +35,10 @@ def _login_form():
|
||||
form = LoginForm()
|
||||
if form.validate_on_submit():
|
||||
# note: ceci est la première requête SQL déclenchée par un utilisateur arrivant
|
||||
user = (
|
||||
User.query.filter_by(user_name=form.user_name.data).first()
|
||||
if is_valid_user_name(form.user_name.data)
|
||||
else None
|
||||
)
|
||||
|
||||
if invalid_user_name(form.user_name.data):
|
||||
user = None
|
||||
else:
|
||||
user = User.query.filter_by(user_name=form.user_name.data).first()
|
||||
if user is None or not user.check_password(form.password.data):
|
||||
current_app.logger.info("login: invalid (%s)", form.user_name.data)
|
||||
flash(_("Nom ou mot de passe invalide"))
|
||||
|
@ -60,7 +60,7 @@ def form_ue_choix_parcours(ue: UniteEns) -> str:
|
||||
f"""<label><input type="checkbox" name="{parcour.id}" value="{parcour.id}"
|
||||
{'checked' if parcour.id in ue_pids else ""}
|
||||
onclick="set_ue_parcour(this);"
|
||||
data-setter="{url_for("apiweb.ue_set_parcours",
|
||||
data-setter="{url_for("apiweb.set_ue_parcours",
|
||||
scodoc_dept=g.scodoc_dept, ue_id=ue.id)}"
|
||||
>{parcour.code}{ects_parcour_txt}</label>"""
|
||||
)
|
||||
|
@ -576,6 +576,7 @@ class BulletinBUT:
|
||||
show_uevalid=self.prefs["bul_show_uevalid"],
|
||||
show_mention=self.prefs["bul_show_mention"],
|
||||
)
|
||||
|
||||
d.update(infos)
|
||||
# --- Rangs
|
||||
d["rang_nt"] = (
|
||||
|
@ -39,7 +39,6 @@ from app.scodoc.codes_cursus import UE_STANDARD
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_pv_lettres_inviduelles import add_dut120_infos
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
@ -68,6 +67,7 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
|
||||
raise ScoValueError("formation non BUT")
|
||||
|
||||
args = _build_bulletin_but_infos(etud, formsemestre, fmt=fmt)
|
||||
|
||||
if fmt == "pdf":
|
||||
filename = scu.bul_filename(formsemestre, etud, prefix="bul-but")
|
||||
bul_pdf = bulletin_but_court_pdf.make_bulletin_but_court_pdf(args)
|
||||
@ -153,5 +153,4 @@ def _build_bulletin_but_infos(
|
||||
if ue.type == UE_STANDARD and ue.acronyme in ue_acronyms
|
||||
],
|
||||
}
|
||||
add_dut120_infos(formsemestre, etud.id, args)
|
||||
return args
|
||||
|
@ -97,8 +97,6 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
tuple[int, str], ScolarFormSemestreValidation
|
||||
] = None,
|
||||
ues_acronyms: list[str] = None,
|
||||
diplome_dut120: bool = False,
|
||||
diplome_dut120_descr: str = "",
|
||||
):
|
||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
||||
self.bul = bul
|
||||
@ -112,8 +110,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
self.title = title
|
||||
self.ue_validation_by_niveau = ue_validation_by_niveau
|
||||
self.ues_acronyms = ues_acronyms # sans UEs sport
|
||||
self.diplome_dut120 = diplome_dut120
|
||||
self.diplome_dut120_descr = diplome_dut120_descr
|
||||
|
||||
self.nb_ues = len(self.ues_acronyms)
|
||||
# Styles PDF
|
||||
self.style_base = styles.ParagraphStyle("style_base")
|
||||
@ -246,17 +243,13 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
)
|
||||
table_abs_ues.hAlign = "RIGHT"
|
||||
# Ligne (en bas) avec table cursus et boite jury
|
||||
# table_content = [self.table_cursus_but()]
|
||||
# if self.prefs["bul_show_decision"]:
|
||||
# table_content.append([Spacer(1, 8 * mm), self.boite_decisions_jury()])
|
||||
table_content = [self.table_cursus_but()]
|
||||
table_content.append(
|
||||
[Spacer(1, 8 * mm), self.boite_decisions_jury()]
|
||||
if self.prefs["bul_show_decision"]
|
||||
else []
|
||||
)
|
||||
table_cursus_jury = Table(
|
||||
[table_content],
|
||||
[
|
||||
[
|
||||
self.table_cursus_but(),
|
||||
[Spacer(1, 8 * mm), self.boite_decisions_jury()],
|
||||
]
|
||||
],
|
||||
colWidths=(self.width_page_avail - 84 * mm, 84 * mm),
|
||||
style=style_table_2cols,
|
||||
)
|
||||
@ -530,17 +523,14 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
def boite_decisions_jury(self):
|
||||
"""La boite en bas à droite avec jury"""
|
||||
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
|
||||
|
||||
if self.bul["semestre"].get("decision_annee", None):
|
||||
txt += f"""
|
||||
Décision année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||
Décision saisie le {
|
||||
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime(scu.DATE_FMT)
|
||||
}, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
|
||||
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
|
||||
<br/>
|
||||
{self.bul.get("diplomation", "")}
|
||||
"""
|
||||
if self.diplome_dut120_descr:
|
||||
txt += f"""<br/>{self.diplome_dut120_descr}."""
|
||||
|
||||
if self.bul["semestre"].get("autorisation_inscription", None):
|
||||
txt += (
|
||||
"<br/>Autorisé à s'inscrire en <b>"
|
||||
|
@ -203,7 +203,7 @@ def bulletin_but_xml_compat(
|
||||
e.date_debut.isoformat() if e.date_debut else ""
|
||||
),
|
||||
date_fin=(
|
||||
e.date_fin.isoformat() if e.date_fin else ""
|
||||
e.date_fin.isoformat() if e.date_debut else ""
|
||||
),
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
|
@ -14,11 +14,9 @@ Classe raccordant avec ScoDoc 7:
|
||||
|
||||
"""
|
||||
import collections
|
||||
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
|
||||
@ -31,7 +29,7 @@ from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
)
|
||||
from app.models.ues import UEParcours
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.models.but_validations import ApcValidationRCUE
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
@ -44,9 +42,9 @@ from app.scodoc import sco_cursus_dut
|
||||
|
||||
|
||||
class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
"""Pour compat ScoDoc 7"""
|
||||
"""Pour compat ScoDoc 7: à revoir pour le BUT"""
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
def __init__(self, etud: dict, formsemestre_id: int, res: ResultatsSemestreBUT):
|
||||
super().__init__(etud, formsemestre_id, res)
|
||||
# Ajustements pour le BUT
|
||||
self.can_compensate_with_prev = False # jamais de compensation à la mode DUT
|
||||
@ -56,22 +54,8 @@ class SituationEtudCursusBUT(sco_cursus_dut.SituationEtudCursusClassic):
|
||||
return False
|
||||
|
||||
def parcours_validated(self):
|
||||
"True si le parcours (ici diplôme BUT) est validé"
|
||||
return but_parcours_validated(
|
||||
self.etud.id, self.cur_sem.formation.referentiel_competence_id
|
||||
)
|
||||
|
||||
|
||||
def but_parcours_validated(etudid: int, referentiel_competence_id: int) -> bool:
|
||||
"""Détermine si le parcours BUT est validé:
|
||||
ne regarde que si une validation BUT3 est enregistrée
|
||||
"""
|
||||
return any(
|
||||
sco_codes.code_annee_validant(v.code)
|
||||
for v in ApcValidationAnnee.query.filter_by(
|
||||
etudid=etudid, ordre=3, referentiel_competence_id=referentiel_competence_id
|
||||
)
|
||||
)
|
||||
"True si le parcours est validé"
|
||||
return False # XXX TODO
|
||||
|
||||
|
||||
class EtudCursusBUT:
|
||||
@ -209,10 +193,6 @@ class EtudCursusBUT:
|
||||
# slow, utile pour affichage fiche
|
||||
return annee in [n.annee for n in self.competences[competence_id].niveaux]
|
||||
|
||||
def get_ects_acquis(self) -> int:
|
||||
"Nombre d'ECTS validés par etud dans le BUT de ce référentiel"
|
||||
return but_ects_valides(self.etud, self.formation.referentiel_competence.id)
|
||||
|
||||
def load_validation_by_niveau(self) -> dict[int, list[ApcValidationRCUE]]:
|
||||
"""Cherche les validations de jury enregistrées pour chaque niveau
|
||||
Résultat: { niveau_id : [ ApcValidationRCUE ] }
|
||||
@ -307,136 +287,104 @@ class FormSemestreCursusBUT:
|
||||
)
|
||||
return niveaux_by_annee
|
||||
|
||||
# def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
# validation_par_competence_et_annee = {}
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# # On s'assurer qu'elle concerne notre cursus !
|
||||
# ue = validation_rcue.ue2
|
||||
# if ue.id not in self.ue_ids:
|
||||
# if (
|
||||
# ue.formation.referentiel_competences_id
|
||||
# == self.referentiel_competences_id
|
||||
# ):
|
||||
# self.ue_ids = ue.id
|
||||
# else:
|
||||
# continue # skip this validation
|
||||
# niveau = validation_rcue.niveau()
|
||||
# if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
# validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
# previous_validation = validation_par_competence_et_annee.get(
|
||||
# niveau.competence.id
|
||||
# ).get(validation_rcue.annee())
|
||||
# # prend la "meilleure" validation
|
||||
# if (not previous_validation) or (
|
||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
# ):
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
# niveau.annee
|
||||
# ] = validation_rcue
|
||||
# return validation_par_competence_et_annee
|
||||
def get_etud_validation_par_competence_et_annee(self, etud: Identite):
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
validation_par_competence_et_annee = {}
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# On s'assurer qu'elle concerne notre cursus !
|
||||
ue = validation_rcue.ue2
|
||||
if ue.id not in self.ue_ids:
|
||||
if (
|
||||
ue.formation.referentiel_competences_id
|
||||
== self.referentiel_competences_id
|
||||
):
|
||||
self.ue_ids = ue.id
|
||||
else:
|
||||
continue # skip this validation
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in validation_par_competence_et_annee:
|
||||
validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
return validation_par_competence_et_annee
|
||||
|
||||
# def list_etud_inscriptions(self, etud: Identite):
|
||||
# "Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
# self.niveaux_by_annee = {}
|
||||
# "{ annee : liste des niveaux à valider }"
|
||||
# self.niveaux: dict[int, ApcNiveau] = {}
|
||||
# "cache les niveaux"
|
||||
# for annee in (1, 2, 3):
|
||||
# niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
# annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
# )[1]
|
||||
# # groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
# self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
# niveaux_d[self.parcour.id] if self.parcour else []
|
||||
# )
|
||||
# self.niveaux.update(
|
||||
# {niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
# )
|
||||
def list_etud_inscriptions(self, etud: Identite):
|
||||
"Le parcours à valider: celui du DERNIER semestre suivi (peut être None)"
|
||||
self.niveaux_by_annee = {}
|
||||
"{ annee : liste des niveaux à valider }"
|
||||
self.niveaux: dict[int, ApcNiveau] = {}
|
||||
"cache les niveaux"
|
||||
for annee in (1, 2, 3):
|
||||
niveaux_d = formation.referentiel_competence.get_niveaux_by_parcours(
|
||||
annee, [self.parcour] if self.parcour else None # XXX WIP
|
||||
)[1]
|
||||
# groupe les niveaux de tronc commun et ceux spécifiques au parcour
|
||||
self.niveaux_by_annee[annee] = niveaux_d["TC"] + (
|
||||
niveaux_d[self.parcour.id] if self.parcour else []
|
||||
)
|
||||
self.niveaux.update(
|
||||
{niveau.id: niveau for niveau in self.niveaux_by_annee[annee]}
|
||||
)
|
||||
|
||||
# self.validation_par_competence_et_annee = {}
|
||||
# """{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
# for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
# niveau = validation_rcue.niveau()
|
||||
# if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
# previous_validation = self.validation_par_competence_et_annee.get(
|
||||
# niveau.competence.id
|
||||
# ).get(validation_rcue.annee())
|
||||
# # prend la "meilleure" validation
|
||||
# if (not previous_validation) or (
|
||||
# sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
# > sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
# ):
|
||||
# self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
# niveau.annee
|
||||
# ] = validation_rcue
|
||||
self.validation_par_competence_et_annee = {}
|
||||
"""{ competence_id : { 'BUT1' : validation_rcue (la "meilleure"), ... } }"""
|
||||
for validation_rcue in ApcValidationRCUE.query.filter_by(etud=etud):
|
||||
niveau = validation_rcue.niveau()
|
||||
if not niveau.competence.id in self.validation_par_competence_et_annee:
|
||||
self.validation_par_competence_et_annee[niveau.competence.id] = {}
|
||||
previous_validation = self.validation_par_competence_et_annee.get(
|
||||
niveau.competence.id
|
||||
).get(validation_rcue.annee())
|
||||
# prend la "meilleure" validation
|
||||
if (not previous_validation) or (
|
||||
sco_codes.BUT_CODES_ORDER[validation_rcue.code]
|
||||
> sco_codes.BUT_CODES_ORDER[previous_validation["code"]]
|
||||
):
|
||||
self.validation_par_competence_et_annee[niveau.competence.id][
|
||||
niveau.annee
|
||||
] = validation_rcue
|
||||
|
||||
# self.competences = {
|
||||
# competence.id: competence
|
||||
# for competence in (
|
||||
# self.parcour.query_competences()
|
||||
# if self.parcour
|
||||
# else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
# )
|
||||
# }
|
||||
# "cache { competence_id : competence }"
|
||||
self.competences = {
|
||||
competence.id: competence
|
||||
for competence in (
|
||||
self.parcour.query_competences()
|
||||
if self.parcour
|
||||
else self.formation.referentiel_competence.get_competences_tronc_commun()
|
||||
)
|
||||
}
|
||||
"cache { competence_id : competence }"
|
||||
|
||||
|
||||
def but_ects_valides(
|
||||
etud: Identite,
|
||||
referentiel_competence_id: int,
|
||||
annees_but: None | Iterable[str] = None,
|
||||
) -> int:
|
||||
def but_ects_valides(etud: Identite, referentiel_competence_id: int) -> float:
|
||||
"""Nombre d'ECTS validés par etud dans le BUT de référentiel indiqué.
|
||||
Ne prend que les UE associées à des niveaux de compétences,
|
||||
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 or 0.0
|
||||
|
||||
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,
|
||||
) -> list[ScolarFormSemestreValidation]:
|
||||
"""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)
|
||||
.join(UniteEns)
|
||||
.join(ApcNiveau)
|
||||
)
|
||||
# restreint à certaines années (utile pour les ECTS du DUT120)
|
||||
if annees_but:
|
||||
validations = validations.filter(ApcNiveau.annee.in_(annees_but))
|
||||
# restreint au référentiel de compétence
|
||||
validations = validations.join(ApcCompetence).filter_by(
|
||||
referentiel_id=referentiel_competence_id
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id)
|
||||
)
|
||||
|
||||
# Tri (nb: fait en python pour gérer les validations externes qui n'ont pas de formsemestre)
|
||||
return sorted(
|
||||
validations,
|
||||
key=lambda v: (
|
||||
(v.formsemestre.semestre_id, v.ue.numero, v.ue.acronyme)
|
||||
if v.formsemestre
|
||||
else (v.ue.semestre_idx or -2, v.ue.numero, v.ue.acronyme)
|
||||
),
|
||||
)
|
||||
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 sum(ects_dict.values()) if ects_dict else 0.0
|
||||
|
||||
|
||||
def etud_ues_de_but1_non_validees(
|
||||
|
@ -64,7 +64,6 @@ import re
|
||||
|
||||
import numpy as np
|
||||
from flask import flash, g, url_for
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
@ -82,10 +81,8 @@ from app.models import Evaluation, ModuleImpl, Scolog, ScolarAutorisationInscrip
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.models.ues import UniteEns
|
||||
from app.models.validations import ScolarFormSemestreValidation
|
||||
@ -93,7 +90,6 @@ from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus as sco_codes
|
||||
from app.scodoc.codes_cursus import (
|
||||
code_rcue_validant,
|
||||
code_ue_validant,
|
||||
BUT_CODES_ORDER,
|
||||
CODES_RCUE_VALIDES,
|
||||
CODES_UE_VALIDES,
|
||||
@ -425,11 +421,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
+ '</div><div class="warning warning-info">'.join(messages)
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
# Présente les codes unifiés, avec le code proposé en tête et les autres triés
|
||||
codes_set = set(self.codes)
|
||||
codes_set.remove(self.codes[0])
|
||||
self.codes = [self.codes[0]] + sorted(x or "" for x in codes_set)
|
||||
self.codes = [self.codes[0]] + sorted((c or "") for c in self.codes[1:])
|
||||
|
||||
def passage_de_droit_en_but3(self) -> tuple[bool, str]:
|
||||
"""Vérifie si les conditions supplémentaires de passage BUT2 vers BUT3 sont satisfaites"""
|
||||
@ -761,13 +753,13 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
self.validation.date = datetime.now()
|
||||
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
log(f"Recording {self}: {code}")
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation année BUT{self.annee_but}: {code}",
|
||||
)
|
||||
db.session.commit()
|
||||
if mark_recorded:
|
||||
self.recorded = True
|
||||
self.invalidate_formsemestre_cache()
|
||||
@ -893,7 +885,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
not only_validantes
|
||||
) or code in sco_codes.CODES_ANNEE_BUT_VALIDES_DE_DROIT:
|
||||
modif |= self.record(code)
|
||||
self.record_autorisation_inscription(code)
|
||||
self.record_autorisation_inscription(code)
|
||||
return modif
|
||||
|
||||
def erase(self, only_one_sem=False):
|
||||
@ -904,8 +896,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
Si only_one_sem, n'efface que les décisions UE et les
|
||||
autorisations de passage du semestre d'origine du deca.
|
||||
|
||||
Efface les validations de DUT120 issues du semestre d'origine du deca.
|
||||
|
||||
Dans tous les cas, efface les validations de l'année en cours.
|
||||
(commite la session.)
|
||||
"""
|
||||
@ -955,17 +945,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
msg=f"Validation année BUT{self.annee_but}: effacée",
|
||||
)
|
||||
|
||||
# Efface les validations de DUT120 issues du semestre d'origine du deca.
|
||||
for validation in ValidationDUT120.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=self.formsemestre.id
|
||||
):
|
||||
db.session.delete(validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg="Validation DUT120 effacée",
|
||||
)
|
||||
|
||||
# Efface éventuelles validations de semestre
|
||||
# (en principe inutilisées en BUT)
|
||||
# et autres UEs (en cas de changement d'architecture de formation depuis le jury ?)
|
||||
@ -1007,36 +986,6 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
pour PV jurys
|
||||
"""
|
||||
validations = []
|
||||
# Validations antérieures émises par ce formsemestre
|
||||
for res in (self.res_impair, self.res_pair):
|
||||
if res:
|
||||
validations_anterieures = (
|
||||
ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=self.etud.id, formsemestre_id=res.formsemestre.id
|
||||
)
|
||||
.filter(
|
||||
ScolarFormSemestreValidation.semestre_id
|
||||
!= res.formsemestre.semestre_id
|
||||
)
|
||||
.join(UniteEns)
|
||||
.join(Formation)
|
||||
.filter_by(formation_code=res.formsemestre.formation.formation_code)
|
||||
.order_by(
|
||||
sa.desc(UniteEns.semestre_idx),
|
||||
UniteEns.acronyme,
|
||||
sa.desc(ScolarFormSemestreValidation.event_date),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
if validations_anterieures:
|
||||
validations.append(
|
||||
", ".join(
|
||||
v.ue.acronyme
|
||||
for v in validations_anterieures
|
||||
if v and v.ue and code_ue_validant(v.code)
|
||||
)
|
||||
)
|
||||
# Validations des UEs des deux semestres de l'année
|
||||
for res in (self.res_impair, self.res_pair):
|
||||
if res:
|
||||
dec_ues = [
|
||||
@ -1045,10 +994,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
if ue.type == UE_STANDARD and ue.id in self.decisions_ues
|
||||
]
|
||||
valids = [dec_ue.descr_validation() for dec_ue in dec_ues]
|
||||
# présentation de la liste des UEs:
|
||||
if valids:
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
|
||||
validations.append(", ".join(v for v in valids if v))
|
||||
return line_sep.join(validations)
|
||||
|
||||
def descr_pb_coherence(self) -> list[str]:
|
||||
@ -1088,8 +1034,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
return messages
|
||||
|
||||
def valide_diplome(self) -> bool:
|
||||
"Vrai si l'étudiant a validé son diplôme (décision enregistrée)"
|
||||
return self.annee_but == 3 and sco_codes.code_annee_validant(self.code_valide)
|
||||
"Vrai si l'étudiant à validé son diplôme"
|
||||
return False # TODO XXX
|
||||
|
||||
|
||||
def list_ue_parcour_etud(
|
||||
@ -1228,12 +1174,13 @@ class DecisionsProposeesRCUE(DecisionsProposees):
|
||||
code=code,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation {self.rcue}: {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"rcue.record {self}: {code}")
|
||||
|
||||
# Modifie au besoin les codes d'UE
|
||||
@ -1646,12 +1593,13 @@ class DecisionsProposeesUE(DecisionsProposees):
|
||||
moy_ue=self.moy_ue,
|
||||
)
|
||||
db.session.add(self.validation)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="jury_but",
|
||||
etudid=self.etud.id,
|
||||
msg=f"Validation UE {self.ue.id} {self.ue.acronyme}({self.moy_ue}): {code}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"DecisionsProposeesUE: recording {self.validation}")
|
||||
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=self.formsemestre.id)
|
||||
|
@ -8,12 +8,11 @@
|
||||
"""
|
||||
from flask import g, request, url_for
|
||||
|
||||
from openpyxl.styles import Alignment
|
||||
from openpyxl.styles import Font, Border, Side, Alignment, PatternFill
|
||||
|
||||
from app import log
|
||||
from app.but import jury_but
|
||||
from app.but.cursus_but import but_ects_valides
|
||||
from app.models.but_validations import ValidationDUT120
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -156,14 +155,6 @@ def pvjury_table_but(
|
||||
deca = None
|
||||
|
||||
ects_but_valides = but_ects_valides(etud, referentiel_competence_id)
|
||||
has_diplome = deca.valide_diplome() if deca else False
|
||||
diplome_lst = ["ADM"] if has_diplome else []
|
||||
validation_dut120 = ValidationDUT120.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=formsemestre.id
|
||||
).first()
|
||||
if validation_dut120:
|
||||
diplome_lst.append("Diplôme de DUT validé.")
|
||||
diplome_str = ". ".join(diplome_lst)
|
||||
row = {
|
||||
"nom_pv": (
|
||||
etud.code_ine or etud.code_nip or etud.id
|
||||
@ -181,12 +172,8 @@ def pvjury_table_but(
|
||||
etudid=etud.id,
|
||||
),
|
||||
"cursus": _descr_cursus_but(etud),
|
||||
"ects": (
|
||||
f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}"""
|
||||
if deca
|
||||
else ""
|
||||
),
|
||||
"_ects_xls": deca.ects_annee() if deca else "",
|
||||
"ects": f"""{deca.ects_annee():g}<br><br>Tot. {ects_but_valides:g}""",
|
||||
"_ects_xls": deca.ects_annee(),
|
||||
"ects_but": ects_but_valides,
|
||||
"ues": deca.descr_ues_validation(line_sep=line_sep) if deca else "-",
|
||||
"niveaux": (
|
||||
@ -194,15 +181,10 @@ def pvjury_table_but(
|
||||
),
|
||||
"decision_but": deca.code_valide if deca else "",
|
||||
"devenir": (
|
||||
"Diplôme obtenu"
|
||||
if has_diplome
|
||||
else (
|
||||
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else ""
|
||||
)
|
||||
", ".join([f"S{i}" for i in deca.get_autorisations_passage()])
|
||||
if deca
|
||||
else ""
|
||||
),
|
||||
"diplome": diplome_str,
|
||||
# pour exports excel seulement:
|
||||
"civilite": etud.civilite_etat_civil_str,
|
||||
"nom": etud.nom,
|
||||
@ -211,7 +193,7 @@ def pvjury_table_but(
|
||||
"code_nip": etud.code_nip,
|
||||
"code_ine": etud.code_ine,
|
||||
}
|
||||
if (deca and deca.valide_diplome()) or not only_diplome:
|
||||
if deca.valide_diplome() or not only_diplome:
|
||||
rows.append(row)
|
||||
|
||||
rows.sort(key=lambda x: x["_nom_pv_order"])
|
||||
|
@ -9,14 +9,14 @@
|
||||
from flask import g, url_for
|
||||
|
||||
from app import db
|
||||
from app.but import jury_but, jury_dut120
|
||||
from app.models import Identite, FormSemestre, ScolarNews, ValidationDUT120
|
||||
from app.but import jury_but
|
||||
from app.models import Identite, FormSemestre, ScolarNews
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def formsemestre_validation_auto_but(
|
||||
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False, with_dut120=True
|
||||
formsemestre: FormSemestre, only_adm: bool = True, dry_run=False
|
||||
) -> tuple[int, list[jury_but.DecisionsProposeesAnnee]]:
|
||||
"""Calcul automatique des décisions de jury sur une "année" BUT.
|
||||
|
||||
@ -27,8 +27,6 @@ def formsemestre_validation_auto_but(
|
||||
En revanche, si only_adm est faux, on enregistre la première décision proposée par ScoDoc
|
||||
(mode à n'utiliser que pour les tests unitaires vérifiant la saisie des jurys)
|
||||
|
||||
Enregistre aussi le DUT120.
|
||||
|
||||
Returns:
|
||||
- En mode normal, (nombre d'étudiants pour lesquels on a enregistré au moins un code, []])
|
||||
- En mode dry_run, (0, list[DecisionsProposeesAnnee])
|
||||
@ -42,10 +40,7 @@ def formsemestre_validation_auto_but(
|
||||
etud = Identite.get_etud(etudid)
|
||||
deca = jury_but.DecisionsProposeesAnnee(etud, formsemestre)
|
||||
if not dry_run:
|
||||
modified = deca.record_all(only_validantes=only_adm)
|
||||
modified |= validation_dut120_auto(etud, formsemestre)
|
||||
if modified:
|
||||
nb_etud_modif += 1
|
||||
nb_etud_modif += deca.record_all(only_validantes=only_adm)
|
||||
else:
|
||||
decas.append(deca)
|
||||
|
||||
@ -61,28 +56,3 @@ def formsemestre_validation_auto_but(
|
||||
),
|
||||
)
|
||||
return nb_etud_modif, decas
|
||||
|
||||
|
||||
def validation_dut120_auto(etud: Identite, formsemestre: FormSemestre) -> bool:
|
||||
"""Si l'étudiant n'a pas déjà validé son DUT120 dans cette spécialité
|
||||
et qu'il satisfait les confitions, l'enregistre.
|
||||
Returns True si nouvelle décision enregistrée.
|
||||
"""
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if not refcomp:
|
||||
raise ScoValueError("formation non associée à un référentiel de compétences")
|
||||
validation = ValidationDUT120.query.filter_by(
|
||||
etudid=etud.id, referentiel_competence_id=refcomp.id
|
||||
).first()
|
||||
if validation:
|
||||
return False # déjà enregistré
|
||||
if jury_dut120.etud_valide_dut120(etud, refcomp.id):
|
||||
new_validation = ValidationDUT120(
|
||||
etudid=etud.id,
|
||||
referentiel_competence_id=refcomp.id,
|
||||
formsemestre_id=formsemestre.id, # Replace with appropriate value
|
||||
)
|
||||
db.session.add(new_validation)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False # ne peut pas valider
|
||||
|
@ -1,112 +0,0 @@
|
||||
##############################################################################
|
||||
# ScoDoc
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury DUT120: gestion et vues
|
||||
|
||||
Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS
|
||||
de BUT 1 et BUT 2.
|
||||
|
||||
"""
|
||||
import time
|
||||
from flask import flash, g, redirect, render_template, request, url_for
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField
|
||||
|
||||
from app import db, log
|
||||
from app.but import cursus_but
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import FormSemestre, Identite, Scolog, ValidationDUT120
|
||||
from app.scodoc.sco_exceptions import ScoPermissionDenied, ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import notes_bp as bp
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def etud_valide_dut120(etud: Identite, referentiel_competence_id: int) -> bool:
|
||||
"""Vrai si l'étudiant satisfait les conditions pour valider le DUT120"""
|
||||
ects_but1_but2 = cursus_but.but_ects_valides(
|
||||
etud, referentiel_competence_id, annees_but=("BUT1", "BUT2")
|
||||
)
|
||||
return ects_but1_but2 >= 120
|
||||
|
||||
|
||||
class ValidationDUT120Form(FlaskForm):
|
||||
"Formulaire validation DUT120"
|
||||
submit = SubmitField("Enregistrer le diplôme DUT 120")
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/validate_dut120/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>",
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def validate_dut120_etud(etudid: int, formsemestre_id: int):
|
||||
"""Formulaire validation individuelle du DUT120"""
|
||||
# Check arguments
|
||||
etud = Identite.get_etud(etudid)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
refcomp = formsemestre.formation.referentiel_competence
|
||||
if not refcomp:
|
||||
raise ScoValueError("formation non associée à un référentiel de compétences")
|
||||
# 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,
|
||||
)
|
||||
)
|
||||
|
||||
ects_but1_but2 = cursus_but.but_ects_valides(
|
||||
etud, refcomp.id, annees_but=("BUT1", "BUT2")
|
||||
)
|
||||
|
||||
form = ValidationDUT120Form()
|
||||
# Check if ValidationDUT120 instance already exists
|
||||
existing_validation = ValidationDUT120.query.filter_by(
|
||||
etudid=etud.id, referentiel_competence_id=refcomp.id
|
||||
).first()
|
||||
if existing_validation:
|
||||
flash("DUT120 déjà validé", "info")
|
||||
etud_can_validate_dut = False
|
||||
# Check if the student meets the criteria
|
||||
elif ects_but1_but2 < 120:
|
||||
flash("L'étudiant ne remplit pas les conditions", "warning")
|
||||
etud_can_validate_dut = False # here existing_validation is None
|
||||
else:
|
||||
etud_can_validate_dut = True
|
||||
|
||||
if etud_can_validate_dut and request.method == "POST" and form.validate_on_submit():
|
||||
new_validation = ValidationDUT120(
|
||||
etudid=etud.id,
|
||||
referentiel_competence_id=refcomp.id,
|
||||
formsemestre_id=formsemestre.id, # Replace with appropriate value
|
||||
)
|
||||
db.session.add(new_validation)
|
||||
Scolog.logdb(
|
||||
"jury_but",
|
||||
etudid=etud.id,
|
||||
msg=f"Validation DUT120 enregistrée depuis S{formsemestre.semestre_id}",
|
||||
)
|
||||
db.session.commit()
|
||||
log(f"ValidationDUT120 enregistrée pour {etud} depuis {formsemestre}")
|
||||
flash("Validation DUT120 enregistrée", "success")
|
||||
return redirect(etud.url_fiche())
|
||||
|
||||
return render_template(
|
||||
"but/validate_dut120.j2",
|
||||
ects_but1_but2=ects_but1_but2,
|
||||
etud=etud,
|
||||
etud_can_validate_dut=etud_can_validate_dut,
|
||||
form=form,
|
||||
formsemestre=formsemestre,
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
time=time,
|
||||
title="Délivrance du DUT",
|
||||
validation=existing_validation,
|
||||
)
|
@ -10,25 +10,23 @@ Non spécifique au BUT.
|
||||
"""
|
||||
|
||||
from flask import render_template
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Identite,
|
||||
UniteEns,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import ScoData
|
||||
|
||||
|
||||
def jury_delete_manual(etud: Identite):
|
||||
"""Vue présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant (si permission) de les supprimer une à une.
|
||||
"""Vue (réservée au chef de dept.)
|
||||
présentant *toutes* les décisions de jury concernant cet étudiant
|
||||
et permettant de les supprimer une à une.
|
||||
"""
|
||||
sem_vals = ScolarFormSemestreValidation.query.filter_by(
|
||||
etudid=etud.id, ue_id=None
|
||||
@ -62,12 +60,8 @@ def jury_delete_manual(etud: Identite):
|
||||
sem_vals=sem_vals,
|
||||
ue_vals=ue_vals,
|
||||
autorisations=autorisations,
|
||||
dut120_vals=ValidationDUT120.query.filter_by(etudid=etud.id).order_by(
|
||||
ValidationDUT120.date
|
||||
),
|
||||
rcue_vals=rcue_vals,
|
||||
annee_but_vals=annee_but_vals,
|
||||
sco=ScoData(),
|
||||
title=f"Toutes les décisions de jury enregistrées pour {etud.html_link_fiche()}",
|
||||
read_only=not current_user.has_permission(Permission.EtudInscrit),
|
||||
)
|
||||
|
@ -4,10 +4,13 @@
|
||||
# See LICENSE
|
||||
##############################################################################
|
||||
|
||||
"""Jury édition manuelle des décisions RCUE antérieures
|
||||
"""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
|
||||
|
@ -340,19 +340,19 @@ class ModuleImplResults:
|
||||
]
|
||||
|
||||
def get_evaluations_bonus(self, modimpl: ModuleImpl) -> list[Evaluation]:
|
||||
"""Les évaluations bonus non bloquées de ce module, ou liste vide s'il n'en a pas."""
|
||||
"""Les évaluations bonus de ce module, ou liste vide s'il n'en a pas."""
|
||||
return [
|
||||
e
|
||||
for e in modimpl.evaluations
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
]
|
||||
|
||||
def get_evaluations_bonus_idx(self, modimpl: ModuleImpl) -> list[int]:
|
||||
"""Les indices des évaluations bonus non bloquées"""
|
||||
"""Les indices des évaluations bonus"""
|
||||
return [
|
||||
i
|
||||
for (i, e) in enumerate(modimpl.evaluations)
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS and not e.is_blocked()
|
||||
if e.evaluation_type == Evaluation.EVALUATION_BONUS
|
||||
]
|
||||
|
||||
|
||||
|
@ -150,7 +150,7 @@ class ResultatsSemestre(ResultatsCache):
|
||||
def etud_ects_tot_sem(self, etudid: int) -> float:
|
||||
"""Le total des ECTS associées à ce semestre (que l'étudiant peut ou non valider)"""
|
||||
etud_ues = self.etud_ues(etudid)
|
||||
return sum([ue.ects or 0.0 for ue in etud_ues]) if etud_ues else 0.0
|
||||
return sum([ue.ects or 0 for ue in etud_ues]) if etud_ues else 0.0
|
||||
|
||||
def modimpl_notes(self, modimpl_id: int, ue_id: int) -> np.ndarray:
|
||||
"""Les notes moyennes des étudiants du sem. à ce modimpl dans cette ue.
|
||||
|
@ -322,7 +322,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
validations = self.get_formsemestre_validations()
|
||||
return validations.decisions_jury_ues.get(etudid, None)
|
||||
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> float:
|
||||
def get_etud_ects_valides(self, etudid: int, decisions_ues: dict = False) -> 0:
|
||||
"""Le total des ECTS validés (et enregistrés) par l'étudiant dans ce semestre.
|
||||
NB: avant jury, rien d'enregistré, donc zéro ECTS.
|
||||
Optimisation: si decisions_ues est passé, l'utilise, sinon appelle get_etud_decisions_ue()
|
||||
@ -331,7 +331,7 @@ class NotesTableCompat(ResultatsSemestre):
|
||||
decisions_ues = self.get_etud_decisions_ue(etudid)
|
||||
if not decisions_ues:
|
||||
return 0.0
|
||||
return float(sum(d.get("ects", 0) for d in decisions_ues.values()))
|
||||
return sum([d.get("ects", 0.0) for d in decisions_ues.values()])
|
||||
|
||||
def get_etud_decision_sem(self, etudid: int) -> dict:
|
||||
"""Decision du jury semestre prise pour cet etudiant, ou None s'il n'y en pas eu.
|
||||
|
@ -84,9 +84,6 @@ def scodoc(func):
|
||||
|
||||
|
||||
def permission_required(permission):
|
||||
"""Vérifie les permissions"""
|
||||
|
||||
# Attention: l'API utilise api_permission_required
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
|
@ -151,7 +151,7 @@ class EntrepriseHistorique(db.Model):
|
||||
__tablename__ = "are_historique"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
authenticated_user = db.Column(db.Text) # user_name login sans contrainte
|
||||
authenticated_user = db.Column(db.Text)
|
||||
entreprise_id = db.Column(db.Integer)
|
||||
object = db.Column(db.Text)
|
||||
object_id = db.Column(db.Integer)
|
||||
|
@ -170,7 +170,13 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
)
|
||||
etat = SelectField(
|
||||
"État du justificatif",
|
||||
choices=[], # sera rempli dynamiquement
|
||||
choices=[
|
||||
("", "Choisir..."), # Placeholder
|
||||
(scu.EtatJustificatif.ATTENTE.value, "En attente de validation"),
|
||||
(scu.EtatJustificatif.NON_VALIDE.value, "Non valide"),
|
||||
(scu.EtatJustificatif.MODIFIE.value, "Modifié"),
|
||||
(scu.EtatJustificatif.VALIDE.value, "Valide"),
|
||||
],
|
||||
validators=[DataRequired(message="This field is required.")],
|
||||
)
|
||||
fichiers = MultipleFileField(label="Ajouter des fichiers")
|
||||
|
@ -140,8 +140,8 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
)
|
||||
edt_ics_title_regexp = StringField(
|
||||
label="Extraction du titre",
|
||||
description=r"""expression régulière python dont le premier groupe
|
||||
sera le titre de l'évènement affiché dans le calendrier ScoDoc.
|
||||
description=r"""expression régulière python dont le premier groupe doit
|
||||
sera le titre de l'évènement affcihé dans le calendrier ScoDoc.
|
||||
Exemple: <tt>Matière : \w+ - ([\w\.\s']+)</tt>
|
||||
""",
|
||||
validators=[Optional(), check_ics_regexp],
|
||||
|
@ -1,56 +0,0 @@
|
||||
"""
|
||||
Formulaire FlaskWTF pour les groupes
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, validators
|
||||
|
||||
|
||||
class FeuilleAppelPreForm(FlaskForm):
|
||||
"""
|
||||
Formulaire utiliser dans le téléchargement des feuilles d'émargement
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Init form, adding a filed for our error messages"
|
||||
super().__init__(*args, **kwargs)
|
||||
self.ok = True
|
||||
self.error_messages: list[str] = []
|
||||
|
||||
def set_error(self, err_msg, field=None):
|
||||
"Set error message both in form and field"
|
||||
self.ok = False
|
||||
self.error_messages.append(err_msg)
|
||||
if field:
|
||||
field.errors.append(err_msg)
|
||||
|
||||
discipline = StringField(
|
||||
"Discipline",
|
||||
)
|
||||
|
||||
ens = StringField(
|
||||
"Enseignant",
|
||||
)
|
||||
|
||||
date = StringField(
|
||||
"Date de la séance",
|
||||
validators=[validators.Length(max=10)],
|
||||
render_kw={
|
||||
"class": "datepicker",
|
||||
"size": 10,
|
||||
"id": "date",
|
||||
},
|
||||
)
|
||||
|
||||
heure = StringField(
|
||||
"Heure de début de la séance",
|
||||
default="",
|
||||
validators=[validators.Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "heure",
|
||||
},
|
||||
)
|
||||
|
||||
submit = SubmitField("Télécharger la liste d'émargement")
|
@ -3,9 +3,7 @@
|
||||
"""Modèles base de données ScoDoc
|
||||
"""
|
||||
|
||||
from flask import abort, g
|
||||
import sqlalchemy
|
||||
import app
|
||||
from app import db
|
||||
|
||||
CODE_STR_LEN = 16 # chaine pour les codes
|
||||
@ -118,42 +116,6 @@ class ScoDocModel(db.Model):
|
||||
args = {field.name: field.data for field in form}
|
||||
return self.from_dict(args)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, oid: int, accept_none=False):
|
||||
"""Instance du modèle ou ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Ne fonctionne que si le modèle a un attribut dept_id.
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à une validation.
|
||||
"""
|
||||
if not isinstance(oid, int):
|
||||
try:
|
||||
oid = int(oid)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "oid invalide")
|
||||
|
||||
if g.scodoc_dept:
|
||||
if hasattr(cls, "_sco_dept_relations"):
|
||||
# Quand dept_id n'est pas dans le modèle courant,
|
||||
# cet attribut indique la liste des tables à joindre pour
|
||||
# obtenir le departement.
|
||||
query = cls.query.filter_by(id=oid)
|
||||
for relation_name in cls._sco_dept_relations:
|
||||
query = query.join(getattr(app.models, relation_name))
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
else:
|
||||
# département accessible dans le modèle courant
|
||||
query = cls.query.filter_by(id=oid, dept_id=g.scodoc_dept_id)
|
||||
else:
|
||||
# Pas de département courant (API non départementale)
|
||||
query = cls.query.filter_by(id=oid)
|
||||
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
|
||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||
from app.models.departements import Departement
|
||||
@ -211,11 +173,7 @@ from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
ApcSituationPro,
|
||||
)
|
||||
from app.models.but_validations import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ValidationDUT120,
|
||||
)
|
||||
from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
|
@ -7,10 +7,7 @@ from app import db
|
||||
|
||||
|
||||
class Absence(db.Model):
|
||||
"""LEGACY
|
||||
Ce modèle n'est PLUS UTILISE depuis ScoDoc 9.6 et remplacé par assiduité.
|
||||
une absence (sur une demi-journée)
|
||||
"""
|
||||
"""une absence (sur une demi-journée)"""
|
||||
|
||||
__tablename__ = "absences"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -21,7 +21,6 @@ from app.scodoc import sco_abs_notification
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
@ -189,12 +188,6 @@ class Assiduite(ScoDocModel):
|
||||
):
|
||||
raise ScoValueError("La date de fin n'est pas un jour travaillé")
|
||||
|
||||
# Vérification de l'activation du module
|
||||
if (err_msg := has_assiduites_disable_pref(formsemestre_date_debut)) or (
|
||||
err_msg := has_assiduites_disable_pref(formsemestre_date_fin)
|
||||
):
|
||||
raise ScoValueError(err_msg)
|
||||
|
||||
# Vérification de non duplication des périodes
|
||||
assiduites: Query = etud.assiduites
|
||||
if is_period_conflicting(date_debut, date_fin, assiduites, Assiduite):
|
||||
@ -348,7 +341,7 @@ class Assiduite(ScoDocModel):
|
||||
"""
|
||||
Retourne le module associé à l'assiduité
|
||||
Si traduire est vrai, retourne le titre du module précédé du code
|
||||
Sinon retourne l'objet Module ou None
|
||||
Sinon rentourne l'objet Module ou None
|
||||
"""
|
||||
|
||||
if self.moduleimpl_id is not None:
|
||||
@ -358,7 +351,7 @@ class Assiduite(ScoDocModel):
|
||||
return f"{mod.code} {mod.titre}"
|
||||
return mod
|
||||
|
||||
if self.external_data is not None and "module" in self.external_data:
|
||||
elif self.external_data is not None and "module" in self.external_data:
|
||||
return (
|
||||
"Autre module (pas dans la liste)"
|
||||
if self.external_data["module"] == "Autre"
|
||||
@ -565,13 +558,13 @@ class Justificatif(ScoDocModel):
|
||||
Raises ScoValueError si paramètres incorrects.
|
||||
"""
|
||||
nouv_justificatif = cls.create_from_dict(locals())
|
||||
db.session.commit()
|
||||
log(f"create_justificatif: etudid={etudiant.id} {nouv_justificatif}")
|
||||
Scolog.logdb(
|
||||
method="create_justificatif",
|
||||
etudid=etudiant.id,
|
||||
msg=f"justificatif: {nouv_justificatif}",
|
||||
)
|
||||
db.session.commit()
|
||||
return nouv_justificatif
|
||||
|
||||
def supprime(self):
|
||||
@ -824,29 +817,3 @@ def get_formsemestre_from_data(data: dict[str, datetime | int]) -> FormSemestre:
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
def has_assiduites_disable_pref(formsemestre: FormSemestre) -> str | bool:
|
||||
"""
|
||||
Vérifie si le semestre possède la préférence "assiduites_disable"
|
||||
et renvoie le message d'erreur associé.
|
||||
|
||||
La préférence est un text field. Il est considéré comme vide si :
|
||||
- la chaine de caractère est vide
|
||||
- si elle n'est composée que de caractères d'espacement (espace, tabulation, retour à la ligne)
|
||||
|
||||
|
||||
Si la chaine est vide, la fonction renvoie False
|
||||
"""
|
||||
|
||||
# Si pas de formsemestre, on ne peut pas vérifier la préférence
|
||||
# On considère que la préférence n'est pas activée
|
||||
if formsemestre is None:
|
||||
return False
|
||||
|
||||
pref: str = (
|
||||
sco_preferences.get_preference("assiduites_disable", formsemestre.id) or ""
|
||||
)
|
||||
pref = pref.strip()
|
||||
|
||||
return pref if pref else False
|
||||
|
@ -604,7 +604,6 @@ app_critiques_modules = db.Table(
|
||||
db.ForeignKey("apc_app_critique.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
db.UniqueConstraint("module_id", "app_crit_id", name="uix_module_id_app_crit_id"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -2,12 +2,9 @@
|
||||
|
||||
"""Décisions de jury (validations) des RCUE et années du BUT
|
||||
"""
|
||||
from collections import defaultdict
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
from app.models import CODE_STR_LEN, ScoDocModel
|
||||
from app.models import CODE_STR_LEN
|
||||
from app.models.but_refcomp import ApcNiveau
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.formsemestre import FormSemestre
|
||||
@ -16,7 +13,7 @@ from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
class ApcValidationRCUE(ScoDocModel):
|
||||
class ApcValidationRCUE(db.Model):
|
||||
"""Validation des niveaux de compétences
|
||||
|
||||
aka "regroupements cohérents d'UE" dans le jargon BUT.
|
||||
@ -61,8 +58,6 @@ class ApcValidationRCUE(ScoDocModel):
|
||||
ue2 = db.relationship("UniteEns", foreign_keys=ue2_id)
|
||||
parcour = db.relationship("ApcParcours")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud} {
|
||||
self.ue1}/{self.ue2}:{self.code!r}>"""
|
||||
@ -118,14 +113,8 @@ class ApcValidationRCUE(ScoDocModel):
|
||||
"formsemestre_id": self.formsemestre_id,
|
||||
}
|
||||
|
||||
def get_codes_apogee(self) -> set[str]:
|
||||
"""Les codes Apogée associés à cette validation RCUE.
|
||||
Prend les codes des deux UEs
|
||||
"""
|
||||
return self.ue1.get_codes_apogee_rcue() | self.ue2.get_codes_apogee_rcue()
|
||||
|
||||
|
||||
class ApcValidationAnnee(ScoDocModel):
|
||||
class ApcValidationAnnee(db.Model):
|
||||
"""Validation des années du BUT"""
|
||||
|
||||
__tablename__ = "apc_validation_annee"
|
||||
@ -156,8 +145,6 @@ class ApcValidationAnnee(ScoDocModel):
|
||||
etud = db.relationship("Identite", backref="apc_validations_annees")
|
||||
formsemestre = db.relationship("FormSemestre", backref="apc_validations_annees")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.etud
|
||||
} BUT{self.ordre}/{self.annee_scolaire}:{self.code!r}>"""
|
||||
@ -215,9 +202,17 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
.order_by(UniteEns.numero, UniteEns.acronyme)
|
||||
)
|
||||
decisions["decision_rcue"] = [v.to_dict_bul() for v in validations_rcues]
|
||||
titres_rcues = _build_decisions_rcue_list(decisions["decision_rcue"])
|
||||
titres_rcues = []
|
||||
for dec_rcue in decisions["decision_rcue"]:
|
||||
niveau = dec_rcue["niveau"]
|
||||
if niveau is None:
|
||||
titres_rcues.append(f"""pas de compétence: code {dec_rcue["code"]}""")
|
||||
else:
|
||||
titres_rcues.append(
|
||||
f"""{niveau["competence"]["titre"]} {niveau["ordre"]}: {
|
||||
dec_rcue["code"]}"""
|
||||
)
|
||||
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
|
||||
decisions["descr_decisions_rcue_list"] = titres_rcues
|
||||
decisions["descr_decisions_niveaux"] = (
|
||||
"Niveaux de compétences: " + decisions["descr_decisions_rcue"]
|
||||
)
|
||||
@ -241,112 +236,3 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
|
||||
else:
|
||||
decisions["decision_annee"] = None
|
||||
return decisions
|
||||
|
||||
|
||||
def _build_decisions_rcue_list(decisions_rcue: dict) -> list[str]:
|
||||
"""Formatte liste des décisions niveaux de compétences / RCUE pour
|
||||
lettres individuelles.
|
||||
Le résulat est trié par compétence et donne pour chaque niveau avec validation:
|
||||
[ 'Administrer: niveau 1 ADM, niveau 2 ADJ', ... ]
|
||||
"""
|
||||
# Construit { id_competence : validation }
|
||||
# où validation est {'code': 'CMP', 'niveau': {'annee': 'BUT3', 'competence': {}, ... }
|
||||
validation_by_competence = defaultdict(list)
|
||||
for validation in decisions_rcue:
|
||||
if validation:
|
||||
# Attention, certaines validations de RCUE peuvent ne plus être associées
|
||||
# à un niveau de compétence si l'UE a été déassociée (ce qui ne devrait pas être fait)
|
||||
competence_id = (
|
||||
(validation.get("niveau") or {}).get("competence") or {}
|
||||
).get("id_orebut")
|
||||
validation_by_competence[competence_id].append(validation)
|
||||
# Tri des listes de validation par numéro de compétence
|
||||
validations_niveaux = sorted(
|
||||
validation_by_competence.values(),
|
||||
key=lambda v: (
|
||||
((v[0].get("niveau") or {}).get("competence") or {}).get("numero", 0)
|
||||
if v
|
||||
else -1
|
||||
),
|
||||
)
|
||||
titres_rcues = []
|
||||
empty = {} # pour syntaxe f-string
|
||||
for validations in validations_niveaux:
|
||||
if validations:
|
||||
v = validations[0]
|
||||
titre_competence = ((v.get("niveau") or {}).get("competence", {})).get(
|
||||
"titre", "sans titre ! A vérifier !"
|
||||
)
|
||||
titres_rcues.append(
|
||||
f"""{titre_competence} : """
|
||||
+ ", ".join(
|
||||
[
|
||||
f"niveau {((v.get('niveau') or empty).get('ordre') or '?')} {v.get('code', '?')}"
|
||||
for v in validations
|
||||
]
|
||||
)
|
||||
)
|
||||
return titres_rcues
|
||||
|
||||
|
||||
class ValidationDUT120(ScoDocModel):
|
||||
"""Validations du DUT 120
|
||||
Ce diplôme est attribué sur demande aux étudiants de BUT ayant acquis les 120 ECTS
|
||||
de BUT 1 et BUT 2.
|
||||
"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
index=True,
|
||||
nullable=False,
|
||||
)
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
"""le semestre origine, dans la plupart des cas le S4 (le diplôme DUT120
|
||||
apparaîtra sur les PV de ce formsemestre)"""
|
||||
referentiel_competence_id = db.Column(
|
||||
db.Integer, db.ForeignKey("apc_referentiel_competences.id"), nullable=False
|
||||
) # pas de cascade, on ne doit pas supprimer un référentiel utilisé
|
||||
"""Identifie la spécialité de DUT décernée"""
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), nullable=False
|
||||
)
|
||||
"""Date de délivrance"""
|
||||
|
||||
etud = db.relationship("Identite", backref="validations_dut120")
|
||||
formsemestre = db.relationship("FormSemestre", backref="validations_dut120")
|
||||
|
||||
_sco_dept_relations = ("Identite",) # pour accéder au département
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<ValidationDUT120 {self.etud}>"""
|
||||
|
||||
def html(self) -> str:
|
||||
"Affichage html"
|
||||
date_str = (
|
||||
f"""le {self.date.strftime(scu.DATEATIME_FMT)}"""
|
||||
if self.date
|
||||
else "(sans date)"
|
||||
)
|
||||
link = (
|
||||
self.formsemestre.html_link_status(
|
||||
label=f"{self.formsemestre.titre_formation(with_sem_idx=1)}",
|
||||
title=self.formsemestre.titre_annee(),
|
||||
)
|
||||
if self.formsemestre
|
||||
else "externe/antérieure"
|
||||
)
|
||||
specialite = (
|
||||
self.formsemestre.formation.referentiel_competence.get_title()
|
||||
if self.formsemestre.formation.referentiel_competence
|
||||
else "(désassociée!)"
|
||||
)
|
||||
return f"""Diplôme de <b>DUT en 120 ECTS du {specialite}</b> émis par
|
||||
{link}
|
||||
{date_str}
|
||||
"""
|
||||
|
@ -261,7 +261,7 @@ class ScoDocSiteConfig(db.Model):
|
||||
|
||||
@classmethod
|
||||
def is_bul_pdf_disabled(cls) -> bool:
|
||||
"""True si on interdit les exports PDF des bulletins"""
|
||||
"""True si on interdit les exports PDF des bulltins"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
|
||||
return cfg is not None and cfg.value
|
||||
|
||||
|
@ -52,17 +52,6 @@ class Departement(db.Model):
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
|
||||
|
||||
@classmethod
|
||||
def get_departement(cls, dept_ident: str | int) -> "Departement":
|
||||
"Le département, par id ou acronyme. Erreur 404 si pas trouvé."
|
||||
try:
|
||||
dept_id = int(dept_ident)
|
||||
except ValueError:
|
||||
dept_id = None
|
||||
if dept_id is None:
|
||||
return cls.query.filter_by(acronym=dept_ident).first_or_404()
|
||||
return cls.query.get_or_404(dept_id)
|
||||
|
||||
def to_dict(self, with_dept_name=True, with_dept_preferences=False):
|
||||
data = {
|
||||
"id": self.id,
|
||||
|
@ -179,9 +179,7 @@ class Identite(models.ScoDocModel):
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return (
|
||||
f"""<a class="etudlink" href="{self.url_fiche()}">{self.nom_prenom()}</a>"""
|
||||
)
|
||||
return f"""<a class="etudlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
|
||||
|
||||
def url_fiche(self) -> str:
|
||||
"url de la fiche étudiant"
|
||||
@ -199,28 +197,18 @@ class Identite(models.ScoDocModel):
|
||||
return cls.query.filter_by(**args).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def get_etud(cls, etudid: int, accept_none=False) -> "Identite":
|
||||
"""Etudiant ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||
pas à un étudiant.
|
||||
"""
|
||||
def get_etud(cls, etudid: int) -> "Identite":
|
||||
"""Etudiant ou 404, cherche uniquement dans le département courant"""
|
||||
if not isinstance(etudid, int):
|
||||
try:
|
||||
etudid = int(etudid)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "etudid invalide")
|
||||
|
||||
query = (
|
||||
cls.query.filter_by(id=etudid, dept_id=g.scodoc_dept_id)
|
||||
if g.scodoc_dept
|
||||
else cls.query.filter_by(id=etudid)
|
||||
)
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
if g.scodoc_dept:
|
||||
return cls.query.filter_by(
|
||||
id=etudid, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=etudid).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_etud(cls, **args) -> "Identite":
|
||||
@ -316,7 +304,7 @@ class Identite(models.ScoDocModel):
|
||||
|
||||
@property
|
||||
def nomprenom(self, reverse=False) -> str:
|
||||
"""DEPRECATED: préférer nom_prenom()
|
||||
"""DEPRECATED
|
||||
Civilité/prénom/nom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
Prend l'identité courante et non celle de l'état civil si elles diffèrent.
|
||||
@ -371,15 +359,14 @@ class Identite(models.ScoDocModel):
|
||||
"Le mail associé à la première adresse de l'étudiant, ou None"
|
||||
return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
|
||||
|
||||
def get_formsemestres(self, recent_first=True) -> list:
|
||||
def get_formsemestres(self) -> list:
|
||||
"""Liste des formsemestres dans lesquels l'étudiant est (a été) inscrit,
|
||||
triée par date_debut, le plus récent d'abord (comme "sems" de scodoc7)
|
||||
(si recent_first=False, le plus ancien en tête)
|
||||
triée par date_debut
|
||||
"""
|
||||
return sorted(
|
||||
[ins.formsemestre for ins in self.formsemestre_inscriptions],
|
||||
key=attrgetter("date_debut"),
|
||||
reverse=recent_first,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_modimpls_by_formsemestre(
|
||||
@ -830,7 +817,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
|
||||
dest_endpoint = "notes.index_html"
|
||||
parameters = {}
|
||||
|
||||
err_page = f"""<h3>Code étudiant ({code_name}) dupliqué !</h3>
|
||||
err_page = f"""<h3><h3>Code étudiant ({code_name}) dupliqué !</h3>
|
||||
<p class="help">Le {code_name} {args[code_name]} est déjà utilisé: un seul étudiant peut avoir
|
||||
ce code. Vérifier votre valeur ou supprimer l'autre étudiant avec cette valeur.
|
||||
</p>
|
||||
@ -845,7 +832,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
|
||||
|
||||
log(f"*** error: code {code_name} duplique: {args[code_name]}")
|
||||
|
||||
raise ScoGenError(err_page, safe=True)
|
||||
raise ScoGenError(err_page)
|
||||
|
||||
|
||||
def make_etud_args(
|
||||
@ -1080,9 +1067,8 @@ class Admission(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
|
||||
class ItemSuivi(models.ScoDocModel):
|
||||
"""Suivi scolarité / débouchés"""
|
||||
|
||||
# Suivi scolarité / débouchés
|
||||
class ItemSuivi(db.Model):
|
||||
__tablename__ = "itemsuivi"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -1094,8 +1080,6 @@ class ItemSuivi(models.ScoDocModel):
|
||||
item_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
situation = db.Column(db.Text)
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
|
||||
class ItemSuiviTag(db.Model):
|
||||
__tablename__ = "itemsuivi_tags"
|
||||
@ -1117,7 +1101,7 @@ itemsuivi_tags_assoc = db.Table(
|
||||
)
|
||||
|
||||
|
||||
class EtudAnnotation(models.ScoDocModel):
|
||||
class EtudAnnotation(db.Model):
|
||||
"""Annotation sur un étudiant"""
|
||||
|
||||
__tablename__ = "etud_annotations"
|
||||
@ -1128,8 +1112,6 @@ class EtudAnnotation(models.ScoDocModel):
|
||||
author = db.Column(db.Text) # le pseudo (user_name), was zope_authenticated_user
|
||||
comment = db.Column(db.Text)
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
def to_dict(self):
|
||||
"""Représentation dictionnaire."""
|
||||
e = dict(self.__dict__)
|
||||
|
@ -60,8 +60,6 @@ class Evaluation(models.ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
ues = db.relationship("UniteEns", secondary="evaluation_ue_poids", viewonly=True)
|
||||
|
||||
_sco_dept_relations = ("ModuleImpl", "FormSemestre") # accès au dept_id
|
||||
|
||||
EVALUATION_NORMALE = 0 # valeurs stockées en base, ne pas changer !
|
||||
EVALUATION_RATTRAPAGE = 1
|
||||
EVALUATION_SESSION2 = 2
|
||||
@ -269,12 +267,10 @@ class Evaluation(models.ScoDocModel):
|
||||
|
||||
@classmethod
|
||||
def get_evaluation(
|
||||
cls, evaluation_id: int | str, dept_id: int = None, accept_none=False
|
||||
cls, evaluation_id: int | str, dept_id: int = None
|
||||
) -> "Evaluation":
|
||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant.
|
||||
Si accept_none, return None si l'id est invalide ou n'existe pas.
|
||||
"""
|
||||
from app.models import FormSemestre
|
||||
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
|
||||
if not isinstance(evaluation_id, int):
|
||||
try:
|
||||
@ -286,8 +282,6 @@ class Evaluation(models.ScoDocModel):
|
||||
query = cls.query.filter_by(id=evaluation_id)
|
||||
if dept_id is not None:
|
||||
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
@classmethod
|
||||
@ -369,8 +363,6 @@ class Evaluation(models.ScoDocModel):
|
||||
return f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||
if self.date_debut.date() == self.date_fin.date(): # même jour
|
||||
if self.date_debut.time() == self.date_fin.time():
|
||||
if self.date_fin.time() == datetime.time(0, 0):
|
||||
return f"le {self.date_debut.strftime('%d/%m/%Y')}" # sans heure
|
||||
return (
|
||||
f"le {self.date_debut.strftime('%d/%m/%Y')} à {_h(self.date_debut)}"
|
||||
)
|
||||
|
@ -12,12 +12,12 @@ from app import db
|
||||
from app import email
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
|
||||
class Scolog(ScoDocModel):
|
||||
class Scolog(db.Model):
|
||||
"""Log des actions (journal modif etudiants)"""
|
||||
|
||||
__tablename__ = "scolog"
|
||||
@ -27,15 +27,14 @@ class Scolog(ScoDocModel):
|
||||
method = db.Column(db.Text)
|
||||
msg = db.Column(db.Text)
|
||||
etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression
|
||||
authenticated_user = db.Column(db.Text) # user_name login, sans contrainte
|
||||
authenticated_user = db.Column(db.Text) # login, sans contrainte
|
||||
# zope_remote_addr suppressed
|
||||
|
||||
@classmethod
|
||||
def logdb(
|
||||
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
|
||||
):
|
||||
"""Add entry in student's log (replacement for old scolog.logdb).
|
||||
Par défaut ne commite pas."""
|
||||
"""Add entry in student's log (replacement for old scolog.logdb)"""
|
||||
entry = Scolog(
|
||||
method=method,
|
||||
msg=msg,
|
||||
@ -46,21 +45,6 @@ class Scolog(ScoDocModel):
|
||||
if commit:
|
||||
db.session.commit()
|
||||
|
||||
def to_dict(self, convert_date=False) -> dict:
|
||||
"convert to dict"
|
||||
return {
|
||||
"etudid": self.etudid,
|
||||
"date": (
|
||||
(self.date.strftime(scu.DATETIME_FMT) if convert_date else self.date)
|
||||
if self.date
|
||||
else ""
|
||||
),
|
||||
"_date_order": self.date.isoformat() if self.date else "",
|
||||
"authenticated_user": self.authenticated_user or "",
|
||||
"msg": self.msg or "",
|
||||
"method": self.method or "",
|
||||
}
|
||||
|
||||
|
||||
class ScolarNews(db.Model):
|
||||
"""Nouvelles pour page d'accueil"""
|
||||
@ -92,9 +76,7 @@ class ScolarNews(db.Model):
|
||||
date = db.Column(
|
||||
db.DateTime(timezone=True), server_default=db.func.now(), index=True
|
||||
)
|
||||
authenticated_user = db.Column(
|
||||
db.Text, index=True
|
||||
) # user_name login, sans contrainte
|
||||
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
|
||||
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
|
||||
type = db.Column(db.String(SHORT_STR_LEN), index=True)
|
||||
object = db.Column(
|
||||
|
@ -7,7 +7,7 @@ from flask_sqlalchemy.query import Query
|
||||
import app
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import ScoDocModel, SHORT_STR_LEN
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.but_refcomp import (
|
||||
ApcAnneeParcours,
|
||||
ApcCompetence,
|
||||
@ -23,7 +23,7 @@ from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
|
||||
|
||||
class Formation(ScoDocModel):
|
||||
class Formation(db.Model):
|
||||
"""Programme pédagogique d'une formation"""
|
||||
|
||||
__tablename__ = "notes_formations"
|
||||
@ -297,7 +297,7 @@ class Formation(ScoDocModel):
|
||||
db.session.commit()
|
||||
|
||||
|
||||
class Matiere(ScoDocModel):
|
||||
class Matiere(db.Model):
|
||||
"""Matières: regroupe les modules d'une UE
|
||||
La matière a peu d'utilité en dehors de la présentation des modules
|
||||
d'une UE.
|
||||
@ -313,7 +313,6 @@ class Matiere(ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
|
||||
|
||||
modules = db.relationship("Module", lazy="dynamic", backref="matiere")
|
||||
_sco_dept_relations = ("UniteEns", "Formation") # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, ue_id={
|
||||
|
@ -36,7 +36,6 @@ from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.evaluations import Evaluation
|
||||
from app.models.events import ScolarNews
|
||||
from app.models.formations import Formation
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
from app.models.moduleimpls import (
|
||||
@ -123,11 +122,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
)
|
||||
"autorise les enseignants à créer des évals dans leurs modimpls"
|
||||
elt_sem_apo = db.Column(db.Text()) # peut être fort long !
|
||||
"code element semestre Apogée, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
"code element semestre Apogee, eg 'VRTW1' ou 'V2INCS4,V2INLS4,...'"
|
||||
elt_annee_apo = db.Column(db.Text())
|
||||
"code element annee Apogee, eg 'VRT1A' ou 'V2INLA,V2INCA,...'"
|
||||
elt_passage_apo = db.Column(db.Text())
|
||||
"code element passage Apogée"
|
||||
|
||||
# Data pour groups_auto_assignment
|
||||
# (ce champ est utilisé uniquement via l'API par le front js)
|
||||
@ -210,70 +207,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
).first_or_404()
|
||||
return cls.query.filter_by(id=formsemestre_id).first_or_404()
|
||||
|
||||
@classmethod
|
||||
def create_formsemestre(cls, args: dict, silent=False) -> "FormSemestre":
|
||||
"""Création d'un formsemestre, avec toutes les valeurs par défaut
|
||||
et notification (sauf si silent).
|
||||
Crée la partition par défaut.
|
||||
"""
|
||||
# was sco_formsemestre.do_formsemestre_create
|
||||
if "dept_id" not in args:
|
||||
args["dept_id"] = g.scodoc_dept_id
|
||||
formsemestre: "FormSemestre" = cls.create_from_dict(args)
|
||||
db.session.flush()
|
||||
for etape in args["etapes"]:
|
||||
formsemestre.add_etape(etape)
|
||||
db.session.commit()
|
||||
for u in args["responsables"]:
|
||||
formsemestre.responsables.append(u)
|
||||
# create default partition
|
||||
partition = Partition(
|
||||
formsemestre=formsemestre, partition_name=None, numero=1000000
|
||||
)
|
||||
db.session.add(partition)
|
||||
partition.create_group(default=True)
|
||||
db.session.commit()
|
||||
|
||||
if not silent:
|
||||
url = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=formsemestre.departement.acronym,
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_SEM,
|
||||
text=f"""Création du semestre <a href="{url}">{formsemestre.titre}</a>""",
|
||||
url=url,
|
||||
max_frequency=0,
|
||||
)
|
||||
|
||||
return formsemestre
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
"""
|
||||
if "date_debut" in args:
|
||||
args["date_debut"] = scu.convert_fr_date(args["date_debut"])
|
||||
if "date_fin" in args:
|
||||
args["date_fin"] = scu.convert_fr_date(args["date_fin"])
|
||||
if "etat" in args:
|
||||
args["etat"] = bool(args["etat"])
|
||||
if "bul_bgcolor" in args:
|
||||
args["bul_bgcolor"] = args.get("bul_bgcolor") or "white"
|
||||
if "titre" in args:
|
||||
args["titre"] = args.get("titre") or "sans titre"
|
||||
return args
|
||||
|
||||
@classmethod
|
||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||
Add 'etapes' to excluded."""
|
||||
# on ne peut pas affecter directement etapes
|
||||
return super().filter_model_attributes(data, (excluded or set()) | {"etapes"})
|
||||
|
||||
def sort_key(self) -> tuple:
|
||||
"""clé pour tris par ordre de date_debut, le plus ancien en tête
|
||||
(pour avoir le plus récent d'abord, sort avec reverse=True)"""
|
||||
@ -677,41 +610,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def est_in_semestre_scolaire(
|
||||
cls,
|
||||
date_debut: datetime.date,
|
||||
year=False,
|
||||
periode=None,
|
||||
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
) -> bool:
|
||||
"""Vrai si la date_debut est dans la période indiquée (1,2,0)
|
||||
du semestre `periode` de l'année scolaire indiquée
|
||||
(ou, à défaut, de celle en cours).
|
||||
|
||||
La période utilise les même conventions que semset["sem_id"];
|
||||
* 1 : première période
|
||||
* 2 : deuxième période
|
||||
* 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
|
||||
)
|
||||
"""
|
||||
if not year:
|
||||
year = scu.annee_scolaire()
|
||||
# n'utilise pas le jour pivot
|
||||
jour_pivot_annee = jour_pivot_periode = 1
|
||||
# calcule l'année universitaire et la période
|
||||
sem_annee, sem_periode = cls.comp_periode(
|
||||
date_debut,
|
||||
mois_pivot_annee,
|
||||
mois_pivot_periode,
|
||||
jour_pivot_annee,
|
||||
jour_pivot_periode,
|
||||
)
|
||||
if periode is None or periode == 0:
|
||||
return sem_annee == year
|
||||
return sem_annee == year and sem_periode == periode
|
||||
|
||||
def est_terminal(self) -> bool:
|
||||
"Vrai si dernier semestre de son cursus (ou formation mono-semestre)"
|
||||
return (self.semestre_id < 0) or (
|
||||
@ -796,7 +694,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
FormSemestre.titre,
|
||||
)
|
||||
|
||||
def etapes_apo_vdi(self) -> list["ApoEtapeVDI"]:
|
||||
def etapes_apo_vdi(self) -> list[ApoEtapeVDI]:
|
||||
"Liste des vdis"
|
||||
# was read_formsemestre_etapes
|
||||
return [e.as_apovdi() for e in self.etapes if e.etape_apo]
|
||||
@ -809,9 +707,9 @@ class FormSemestre(models.ScoDocModel):
|
||||
return ""
|
||||
return ", ".join(sorted([etape.etape_apo for etape in self.etapes if etape]))
|
||||
|
||||
def add_etape(self, etape_apo: str | ApoEtapeVDI):
|
||||
def add_etape(self, etape_apo: str):
|
||||
"Ajoute une étape"
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=str(etape_apo))
|
||||
etape = FormSemestreEtape(formsemestre_id=self.id, etape_apo=etape_apo)
|
||||
db.session.add(etape)
|
||||
|
||||
def regroupements_coherents_etud(self) -> list[tuple[UniteEns, UniteEns]]:
|
||||
@ -845,11 +743,11 @@ class FormSemestre(models.ScoDocModel):
|
||||
else:
|
||||
return ", ".join([u.get_nomcomplet() for u in self.responsables])
|
||||
|
||||
def est_responsable(self, user: User) -> bool:
|
||||
def est_responsable(self, user: User):
|
||||
"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 = None) -> bool:
|
||||
def est_chef_or_diretud(self, user: User = None):
|
||||
"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 +765,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
return True # typiquement admin, chef dept
|
||||
return self.est_responsable(user)
|
||||
|
||||
def can_edit_jury(self, user: User | None = None):
|
||||
def can_edit_jury(self, user: User = None):
|
||||
"""Vrai si utilisateur (par def. current) peut saisir decision de jury
|
||||
dans ce semestre: vérifie permission et verrouillage.
|
||||
"""
|
||||
@ -995,12 +893,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
def get_codes_apogee(self, category=None) -> set[str]:
|
||||
"""Les codes Apogée (codés en base comme "VRT1,VRT2")
|
||||
category:
|
||||
None: tous,
|
||||
"etapes": étapes associées,
|
||||
"sem: code semestre"
|
||||
"annee": code annuel
|
||||
"passage": code passage
|
||||
category: None: tous, "etapes": étapes associées, "sem: code semestre", "annee": code annuel
|
||||
"""
|
||||
codes = set()
|
||||
if category is None or category == "etapes":
|
||||
@ -1009,8 +902,6 @@ class FormSemestre(models.ScoDocModel):
|
||||
codes |= {x.strip() for x in self.elt_sem_apo.split(",") if x}
|
||||
if (category is None or category == "annee") and self.elt_annee_apo:
|
||||
codes |= {x.strip() for x in self.elt_annee_apo.split(",") if x}
|
||||
if (category is None or category == "passage") and self.elt_passage_apo:
|
||||
codes |= {x.strip() for x in self.elt_passage_apo.split(",") if x}
|
||||
return codes
|
||||
|
||||
def get_inscrits(self, include_demdef=False, order=False) -> list[Identite]:
|
||||
@ -1047,7 +938,7 @@ class FormSemestre(models.ScoDocModel):
|
||||
|
||||
def etudids_actifs(self) -> tuple[list[int], set[int]]:
|
||||
"""Liste les etudids inscrits (incluant DEM et DEF),
|
||||
qui sera l'index des dataframes de notes
|
||||
qui ser al'index des dataframes de notes
|
||||
et donne l'ensemble des inscrits non DEM ni DEF.
|
||||
"""
|
||||
return [inscr.etudid for inscr in self.inscriptions], {
|
||||
@ -1334,18 +1225,10 @@ class FormSemestreEtape(db.Model):
|
||||
"Etape False if code empty"
|
||||
return self.etape_apo is not None and (len(self.etape_apo) > 0)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, ApoEtapeVDI):
|
||||
return self.as_apovdi() == other
|
||||
return str(self) == str(other)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Etape {self.id} apo={self.etape_apo!r}>"
|
||||
|
||||
def __str__(self):
|
||||
return self.etape_apo or ""
|
||||
|
||||
def as_apovdi(self) -> "ApoEtapeVDI":
|
||||
def as_apovdi(self) -> ApoEtapeVDI:
|
||||
return ApoEtapeVDI(self.etape_apo)
|
||||
|
||||
|
||||
@ -1498,9 +1381,8 @@ class FormSemestreInscription(db.Model):
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} etudid={self.etudid} sem={
|
||||
self.formsemestre_id} (S{self.formsemestre.semestre_id}) etat={self.etat} {
|
||||
('parcours="'+str(self.parcour.code)+'"') if self.parcour else ''
|
||||
} {('etape="'+self.etape+'"') if self.etape else ''}>"""
|
||||
self.formsemestre_id} etat={self.etat} {
|
||||
('parcours='+str(self.parcour)) if self.parcour else ''}>"""
|
||||
|
||||
|
||||
class NotesSemSet(db.Model):
|
||||
|
@ -54,7 +54,6 @@ class Partition(ScoDocModel):
|
||||
cascade="all, delete-orphan",
|
||||
order_by="GroupDescr.numero, GroupDescr.group_name",
|
||||
)
|
||||
_sco_dept_relations = ("FormSemestre",)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Partition, self).__init__(**kwargs)
|
||||
@ -94,10 +93,6 @@ class Partition(ScoDocModel):
|
||||
):
|
||||
group.remove_etud(etud)
|
||||
|
||||
def is_default(self) -> bool:
|
||||
"vrai si partition par défault (tous les étudiants)"
|
||||
return not self.partition_name
|
||||
|
||||
def is_parcours(self) -> bool:
|
||||
"Vrai s'il s'agit de la partition de parcours"
|
||||
return self.partition_name == scu.PARTITION_PARCOURS
|
||||
@ -226,11 +221,6 @@ class GroupDescr(ScoDocModel):
|
||||
numero = db.Column(db.Integer, nullable=False, default=0)
|
||||
"Numero = ordre de presentation"
|
||||
|
||||
_sco_dept_relations = (
|
||||
"Partition",
|
||||
"FormSemestre",
|
||||
)
|
||||
|
||||
etuds = db.relationship(
|
||||
"Identite",
|
||||
secondary="group_membership",
|
||||
@ -342,12 +332,13 @@ class GroupDescr(ScoDocModel):
|
||||
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
|
||||
if etud in self.etuds:
|
||||
self.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
Scolog.logdb(
|
||||
method="group_remove_etud",
|
||||
etudid=etud.id,
|
||||
msg=f"Retrait du groupe {self.group_name} de {self.partition.partition_name}",
|
||||
commit=True,
|
||||
)
|
||||
db.session.commit()
|
||||
# Update parcours
|
||||
if self.partition.partition_name == scu.PARTITION_PARCOURS:
|
||||
self.partition.formsemestre.update_inscriptions_parcours_from_groups(
|
||||
|
@ -60,8 +60,6 @@ class ModuleImpl(ScoDocModel):
|
||||
)
|
||||
"enseignants du module (sans le responsable)"
|
||||
|
||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
|
||||
@ -255,60 +253,21 @@ class ModuleImpl(ScoDocModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def can_change_inscriptions(self, user: User | None = None, raise_exc=True) -> bool:
|
||||
"""check si user peut inscrire/désinsincrire des étudiants à ce module.
|
||||
Autorise ScoEtudInscrit ou responsables semestre.
|
||||
"""
|
||||
user = current_user if user is None else user
|
||||
if not self.formsemestre.etat:
|
||||
if raise_exc:
|
||||
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||
return False
|
||||
# -- check access
|
||||
# resp. module ou ou perm. EtudInscrit ou resp. semestre
|
||||
if (
|
||||
user.id != self.responsable_id
|
||||
and not user.has_permission(Permission.EtudInscrit)
|
||||
and user.id not in (u.id for u in self.formsemestre.responsables)
|
||||
):
|
||||
if raise_exc:
|
||||
raise AccessDenied(f"Modification impossible pour {user}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def est_inscrit(self, etud: Identite):
|
||||
def est_inscrit(self, etud: Identite) -> bool:
|
||||
"""
|
||||
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).
|
||||
(lent, pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
Retourne ModuleImplInscription si inscrit au module, False sinon.
|
||||
Retourne Vrai si inscrit au module, faux sinon.
|
||||
"""
|
||||
# vérifie inscrit au moduleimpl ET au formsemestre
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
inscription = (
|
||||
ModuleImplInscription.query.filter_by(etudid=etud.id, moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=etud.id)
|
||||
.first()
|
||||
is_module: int = (
|
||||
ModuleImplInscription.query.filter_by(
|
||||
etudid=etud.id, moduleimpl_id=self.id
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
return inscription or False
|
||||
|
||||
def query_inscriptions(self) -> Query:
|
||||
"""Query ModuleImplInscription: inscrits au moduleimpl et au formsemestre
|
||||
(pas de cache: pour un accès rapide, utiliser nt.modimpl_inscr_df).
|
||||
"""
|
||||
from app.models.formsemestre import FormSemestre, FormSemestreInscription
|
||||
|
||||
return (
|
||||
ModuleImplInscription.query.filter_by(moduleimpl_id=self.id)
|
||||
.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.join(FormSemestreInscription)
|
||||
.filter_by(etudid=ModuleImplInscription.etudid)
|
||||
)
|
||||
return is_module
|
||||
|
||||
|
||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||
|
@ -75,8 +75,6 @@ class Module(models.ScoDocModel):
|
||||
backref=db.backref("modules", lazy=True),
|
||||
)
|
||||
|
||||
_sco_dept_relations = "Formation" # accès au dept_id
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.ue_coefs = []
|
||||
super(Module, self).__init__(**kwargs)
|
||||
@ -108,76 +106,31 @@ class Module(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
@classmethod
|
||||
def filter_model_attributes(cls, args: dict, excluded: set[str] = None) -> dict:
|
||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||
Add 'id' to excluded."""
|
||||
# on ne peut pas affecter directement parcours
|
||||
return super().filter_model_attributes(args, (excluded or set()) | {"parcours"})
|
||||
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
||||
"""Update object's fields given in dict. Add to session but don't commit.
|
||||
True if modification.
|
||||
- can't change ue nor formation
|
||||
- can change matiere_id, iff new matiere in same ue
|
||||
- can change parcours: parcours list of ApcParcour id or instances.
|
||||
"""
|
||||
# Vérifie les changements de matiere
|
||||
new_matiere_id = args.get("matiere_id", self.matiere_id)
|
||||
if new_matiere_id != self.matiere_id:
|
||||
# exists ?
|
||||
from app.models import Matiere
|
||||
|
||||
matiere = db.session.get(Matiere, new_matiere_id)
|
||||
if matiere is None or matiere.ue_id != self.ue_id:
|
||||
raise ScoValueError("invalid matiere")
|
||||
|
||||
modified = super().from_dict(
|
||||
args, excluded=(excluded or set()) | {"formation_id", "ue_id"}
|
||||
)
|
||||
|
||||
existing_parcours = {p.id for p in self.parcours}
|
||||
new_parcours = args.get("parcours", []) or []
|
||||
if existing_parcours != set(new_parcours):
|
||||
self._set_parcours_from_list(new_parcours)
|
||||
return True
|
||||
return modified
|
||||
return super().filter_model_attributes(data, (excluded or set()) | {"parcours"})
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, data: dict) -> "Module":
|
||||
"""Create from given dict, add parcours.
|
||||
Flush session."""
|
||||
module = super().create_from_dict(data)
|
||||
db.session.flush()
|
||||
module._set_parcours_from_list(data.get("parcours", []) or [])
|
||||
return module
|
||||
|
||||
def _set_parcours_from_list(self, parcours: list[ApcParcours | int]):
|
||||
"""Ajoute ces parcours à la liste des parcours du module.
|
||||
Chaque élément est soit un objet parcours soit un id.
|
||||
S'assure que chaque parcours est dans le référentiel de compétence
|
||||
associé à la formation du module.
|
||||
"""
|
||||
for p in parcours:
|
||||
"""Create from given dict, add parcours"""
|
||||
mod = super().create_from_dict(data)
|
||||
for p in data.get("parcours", []) or []:
|
||||
if isinstance(p, ApcParcours):
|
||||
parcour: ApcParcours = p
|
||||
if p.referentiel_id != self.formation.referentiel_competence.id:
|
||||
raise ScoValueError("Parcours hors référentiel du module")
|
||||
else:
|
||||
try:
|
||||
pid = int(p)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("id de parcours invalide") from exc
|
||||
query = (
|
||||
ApcParcours.query.filter_by(id=pid)
|
||||
.join(ApcReferentielCompetences)
|
||||
.filter_by(id=self.formation.referentiel_competence.id)
|
||||
)
|
||||
pid = int(p)
|
||||
query = ApcParcours.query.filter_by(id=pid)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
query = query.join(ApcReferentielCompetences).filter_by(
|
||||
dept_id=g.scodoc_dept_id
|
||||
)
|
||||
parcour: ApcParcours = query.first()
|
||||
if parcour is None:
|
||||
raise ScoValueError("Parcours invalide")
|
||||
self.parcours.append(parcour)
|
||||
mod.parcours.append(parcour)
|
||||
return mod
|
||||
|
||||
def clone(self):
|
||||
"""Create a new copy of this module."""
|
||||
@ -210,29 +163,16 @@ class Module(models.ScoDocModel):
|
||||
mod.app_critiques.append(app_critique)
|
||||
return mod
|
||||
|
||||
def to_dict(
|
||||
self,
|
||||
convert_objects=False,
|
||||
with_matiere=False,
|
||||
with_ue=False,
|
||||
with_parcours_ids=False,
|
||||
) -> dict:
|
||||
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict:
|
||||
"""If convert_objects, convert all attributes to native types
|
||||
(suitable jor json encoding).
|
||||
If convert_objects and with_parcours_ids, give parcours as a list of id (API)
|
||||
"""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
d.pop("formation", None)
|
||||
if convert_objects:
|
||||
if with_parcours_ids:
|
||||
d["parcours"] = [p.id for p in self.parcours]
|
||||
else:
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["parcours"] = [p.to_dict() for p in self.parcours]
|
||||
d["ue_coefs"] = [
|
||||
c.to_dict(convert_objects=False)
|
||||
for c in self.ue_coefs
|
||||
# note: don't convert_objects: we do wan't the details of the UEs here
|
||||
c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs
|
||||
]
|
||||
d["app_critiques"] = {x.code: x.to_dict() for x in self.app_critiques}
|
||||
if not with_matiere:
|
||||
|
@ -5,12 +5,11 @@
|
||||
|
||||
import sqlalchemy as sa
|
||||
from app import db
|
||||
from app import models
|
||||
from app.scodoc import safehtml
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
class BulAppreciations(models.ScoDocModel):
|
||||
class BulAppreciations(db.Model):
|
||||
"""Appréciations sur bulletins"""
|
||||
|
||||
__tablename__ = "notes_appreciations"
|
||||
@ -28,8 +27,6 @@ class BulAppreciations(models.ScoDocModel):
|
||||
author = db.Column(db.Text) # le pseudo (user_name), sans contrainte
|
||||
comment = db.Column(db.Text) # texte libre
|
||||
|
||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||
|
||||
@classmethod
|
||||
def get_appreciations_list(
|
||||
cls, formsemestre_id: int, etudid: int
|
||||
|
@ -3,10 +3,10 @@
|
||||
"""Model : preferences
|
||||
"""
|
||||
|
||||
from app import db, models
|
||||
from app import db
|
||||
|
||||
|
||||
class ScoPreference(models.ScoDocModel):
|
||||
class ScoPreference(db.Model):
|
||||
"""ScoDoc preferences (par département)"""
|
||||
|
||||
__tablename__ = "sco_prefs"
|
||||
@ -19,8 +19,5 @@ class ScoPreference(models.ScoDocModel):
|
||||
value = db.Column(db.Text())
|
||||
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
|
||||
|
||||
_sco_dept_relations = ("FormSemestre",) # accès au dept_id
|
||||
|
||||
def __repr__(self):
|
||||
return f"""<{self.__class__.__name__} {self.id} {self.departement.acronym
|
||||
} {self.name}={self.value}>"""
|
||||
return f"<{self.__class__.__name__} {self.id} {self.departement.acronym} {self.name}={self.value}>"
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""ScoDoc 9 models : Unités d'Enseignement (UE)
|
||||
"""
|
||||
|
||||
from flask import abort, g
|
||||
from flask import g
|
||||
import pandas as pd
|
||||
|
||||
from app import db, log
|
||||
@ -46,8 +46,6 @@ class UniteEns(models.ScoDocModel):
|
||||
# coef UE, utilise seulement si l'option use_ue_coefs est activée:
|
||||
coefficient = db.Column(db.Float)
|
||||
|
||||
# id de l'élément Apogée du RCUE (utilisé pour les UEs de sem. pair du BUT)
|
||||
code_apogee_rcue = db.Column(db.String(APO_CODE_STR_LEN))
|
||||
# coef. pour le calcul de moyennes de RCUE. Par défaut, 1.
|
||||
coef_rcue = db.Column(db.Float, nullable=False, default=1.0, server_default="1.0")
|
||||
|
||||
@ -123,16 +121,6 @@ class UniteEns(models.ScoDocModel):
|
||||
|
||||
return args
|
||||
|
||||
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
|
||||
"""Update object's fields given in dict. Add to session but don't commit.
|
||||
True if modification.
|
||||
- can't change formation nor niveau_competence
|
||||
"""
|
||||
return super().from_dict(
|
||||
args,
|
||||
excluded=(excluded or set()) | {"formation_id", "niveau_competence_id"},
|
||||
)
|
||||
|
||||
def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
|
||||
"""as a dict, with the same conversions as in ScoDoc7.
|
||||
If convert_objects, convert all attributes to native types
|
||||
@ -265,30 +253,6 @@ class UniteEns(models.ScoDocModel):
|
||||
log(f"ue.set_ects( ue_id={self.id}, acronyme={self.acronyme}, ects={ects} )")
|
||||
db.session.add(self)
|
||||
|
||||
@classmethod
|
||||
def get_ue(cls, ue_id: int, accept_none=False) -> "UniteEns":
|
||||
"""UE ou 404 (ou None si accept_none),
|
||||
cherche uniquement dans le département courant.
|
||||
Si accept_none, return None si l'id est invalide ou inexistant.
|
||||
"""
|
||||
if not isinstance(ue_id, int):
|
||||
try:
|
||||
ue_id = int(ue_id)
|
||||
except (TypeError, ValueError):
|
||||
if accept_none:
|
||||
return None
|
||||
abort(404, "ue_id invalide")
|
||||
|
||||
query = cls.query.filter_by(id=ue_id)
|
||||
if g.scodoc_dept:
|
||||
from app.models import Formation
|
||||
|
||||
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
|
||||
|
||||
if accept_none:
|
||||
return query.first()
|
||||
return query.first_or_404()
|
||||
|
||||
def get_ressources(self):
|
||||
"Liste des modules ressources rattachés à cette UE"
|
||||
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()
|
||||
@ -310,12 +274,6 @@ class UniteEns(models.ScoDocModel):
|
||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||
return set()
|
||||
|
||||
def get_codes_apogee_rcue(self) -> set[str]:
|
||||
"""Les codes Apogée RCUE (codés en base comme "VRT1,VRT2")"""
|
||||
if self.code_apogee_rcue:
|
||||
return {x.strip() for x in self.code_apogee_rcue.split(",") if x}
|
||||
return set()
|
||||
|
||||
def _parcours_niveaux_ids(self, parcours=list[ApcParcours]) -> set[int]:
|
||||
"""set des ids de niveaux communs à tous les parcours listés"""
|
||||
return set.intersection(
|
||||
@ -396,21 +354,7 @@ class UniteEns(models.ScoDocModel):
|
||||
|
||||
return True, ""
|
||||
|
||||
def is_used_in_validation_rcue(self) -> bool:
|
||||
"""Vrai si cette UE est utilisée dans une validation enregistrée d'RCUE."""
|
||||
from app.models.but_validations import ApcValidationRCUE
|
||||
|
||||
return (
|
||||
ApcValidationRCUE.query.filter(
|
||||
db.or_(
|
||||
ApcValidationRCUE.ue1_id == self.id,
|
||||
ApcValidationRCUE.ue2_id == self.id,
|
||||
)
|
||||
).count()
|
||||
> 0
|
||||
)
|
||||
|
||||
def set_niveau_competence(self, niveau: ApcNiveau | None) -> tuple[bool, str]:
|
||||
def set_niveau_competence(self, niveau: ApcNiveau) -> tuple[bool, str]:
|
||||
"""Associe cette UE au niveau de compétence indiqué.
|
||||
Le niveau doit être dans l'un des parcours de l'UE (si elle n'est pas
|
||||
de tronc commun).
|
||||
@ -418,12 +362,7 @@ class UniteEns(models.ScoDocModel):
|
||||
Sinon, raises ScoFormationConflict.
|
||||
|
||||
Si niveau est None, désassocie.
|
||||
|
||||
Si l'UE est utilisée dans un validation de RCUE, on ne peut plus la changer de niveau.
|
||||
|
||||
Returns
|
||||
- True if (de)association done, False on error.
|
||||
- Error message (string)
|
||||
Returns True if (de)association done, False on error.
|
||||
"""
|
||||
# Sanity checks
|
||||
if not self.formation.referentiel_competence:
|
||||
@ -431,12 +370,6 @@ class UniteEns(models.ScoDocModel):
|
||||
False,
|
||||
"La formation n'est pas associée à un référentiel de compétences",
|
||||
)
|
||||
# UE utilisée dans des validations RCUE ?
|
||||
if self.is_used_in_validation_rcue():
|
||||
return (
|
||||
False,
|
||||
"UE utilisée dans un RCUE validé: son niveau ne peut plus être modifié",
|
||||
)
|
||||
if niveau is not None:
|
||||
if self.niveau_competence_id is not None:
|
||||
return (
|
||||
|
@ -2,15 +2,12 @@
|
||||
|
||||
"""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
|
||||
@ -116,7 +113,6 @@ class ScolarFormSemestreValidation(db.Model):
|
||||
if self.ue.parcours else ""}
|
||||
{("émise par " + link)}
|
||||
: <b>{self.code}</b>{moyenne}
|
||||
<b>{(self.ue.ects or 0):g} ECTS</b>
|
||||
le {self.event_date.strftime(scu.DATEATIME_FMT)}
|
||||
"""
|
||||
else:
|
||||
@ -135,27 +131,6 @@ 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"""
|
||||
@ -211,7 +186,7 @@ class ScolarAutorisationInscription(db.Model):
|
||||
origin_formsemestre_id: int,
|
||||
semestre_id: int,
|
||||
):
|
||||
"""Ajoute une autorisation (don't commit)"""
|
||||
"""Ajoute une autorisation"""
|
||||
autorisation = cls(
|
||||
etudid=etudid,
|
||||
formation_code=formation_code,
|
||||
|
@ -200,7 +200,7 @@ CODES_RCUE_VALIDES = CODES_RCUE_VALIDES_DE_DROIT | {ADJ, ADSUP}
|
||||
|
||||
# Pour le BUT:
|
||||
CODES_ANNEE_BUT_VALIDES_DE_DROIT = {ADM, PASD} # PASD pour enregistrement auto
|
||||
CODES_ANNEE_BUT_VALIDES = {ADJ, ADM, ADSUP}
|
||||
CODES_ANNEE_BUT_VALIDES = {ADM, ADSUP}
|
||||
CODES_ANNEE_ARRET = {DEF, DEM, ABAN, ABL}
|
||||
BUT_BARRE_UE8 = 8.0 - NOTES_TOLERANCE
|
||||
BUT_BARRE_UE = BUT_BARRE_RCUE = 10.0 - NOTES_TOLERANCE
|
||||
|
@ -55,8 +55,6 @@ from reportlab.lib.colors import Color
|
||||
from reportlab.lib import styles
|
||||
from reportlab.lib.units import cm
|
||||
|
||||
from flask import render_template
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc import sco_excel
|
||||
@ -317,10 +315,7 @@ class GenTable:
|
||||
|
||||
def get_titles_list(self):
|
||||
"list of titles"
|
||||
titles = [self.titles.get(cid, "") for cid in self.columns_ids]
|
||||
if "row_title" in self.titles and "row_title" not in self.columns_ids:
|
||||
titles.insert(0, self.titles["row_title"])
|
||||
return titles
|
||||
return [self.titles.get(cid, "") for cid in self.columns_ids]
|
||||
|
||||
def gen(self, fmt="html", columns_ids=None):
|
||||
"""Build representation of the table in the specified format.
|
||||
@ -683,15 +678,16 @@ class GenTable:
|
||||
fmt="html",
|
||||
page_title="",
|
||||
filename=None,
|
||||
javascripts=(),
|
||||
cssstyles=[],
|
||||
javascripts=[],
|
||||
with_html_headers=True,
|
||||
publish=True,
|
||||
init_qtip=False,
|
||||
):
|
||||
"""
|
||||
Build page at given format
|
||||
This is a simple page with only a title and the table.
|
||||
If not publish, do not set response header for non HTML formats.
|
||||
If with_html_headers, render a full page using ScoDoc template.
|
||||
If not publish, does not set response header
|
||||
"""
|
||||
if not filename:
|
||||
filename = self.filename
|
||||
@ -699,16 +695,21 @@ class GenTable:
|
||||
html_title = self.html_title or title
|
||||
if fmt == "html":
|
||||
H = []
|
||||
if with_html_headers:
|
||||
H.append(
|
||||
self.html_header
|
||||
or html_sco_header.sco_header(
|
||||
cssstyles=cssstyles,
|
||||
page_title=page_title,
|
||||
javascripts=javascripts,
|
||||
init_qtip=init_qtip,
|
||||
)
|
||||
)
|
||||
if html_title:
|
||||
H.append(html_title)
|
||||
H.append(self.html())
|
||||
if with_html_headers:
|
||||
return render_template(
|
||||
"sco_page.j2",
|
||||
content="\n".join(H),
|
||||
title=page_title,
|
||||
javascripts=javascripts,
|
||||
)
|
||||
H.append(html_sco_header.sco_footer())
|
||||
return "\n".join(H)
|
||||
elif fmt == "pdf":
|
||||
pdf_objs = self.pdf()
|
||||
|
@ -97,18 +97,10 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
|
||||
|
||||
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
|
||||
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
|
||||
<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css" />
|
||||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{
|
||||
if (document.getElementById('gtrcontent')) {{
|
||||
enableTooltips("gtrcontent");
|
||||
}}
|
||||
if (document.getElementById('sidebar')) {{
|
||||
enableTooltips("sidebar");
|
||||
}}
|
||||
}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||
</script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
@ -116,7 +108,6 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>
|
||||
|
||||
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
@ -226,14 +217,8 @@ def sco_header(
|
||||
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
|
||||
<script>
|
||||
window.onload=function(){{
|
||||
if (document.getElementById('gtrcontent')) {{
|
||||
enableTooltips("gtrcontent");
|
||||
}}
|
||||
if (document.getElementById('sidebar')) {{
|
||||
enableTooltips("sidebar");
|
||||
}}
|
||||
}};
|
||||
window.onload=function(){{enableTooltips("gtrcontent"); enableTooltips("sidebar");}};
|
||||
|
||||
const SCO_URL="{url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)}";
|
||||
const SCO_TIMEZONE="{scu.TIME_ZONE}";
|
||||
</script>"""
|
||||
|
@ -31,7 +31,6 @@
|
||||
|
||||
Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
@ -43,15 +43,52 @@ Pour chaque étudiant commun:
|
||||
comparer les résultats
|
||||
|
||||
"""
|
||||
from flask import g, render_template, url_for
|
||||
from flask import g, url_for
|
||||
|
||||
from app import log
|
||||
from app.scodoc import sco_apogee_csv, sco_apogee_reader
|
||||
from app.scodoc.sco_apogee_csv import ApoData
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
_HELP_TXT = """
|
||||
<div class="help">
|
||||
<p>Outil de comparaison de fichiers (maquettes CSV) Apogée.
|
||||
</p>
|
||||
<p>Cet outil compare deux fichiers fournis. Aucune donnée stockée dans ScoDoc n'est utilisée.
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def apo_compare_csv_form():
|
||||
"""Form: submit 2 CSV files to compare them."""
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||
"""<h2>Comparaison de fichiers Apogée</h2>
|
||||
<form id="apo_csv_add" action="apo_compare_csv" method="post" enctype="multipart/form-data">
|
||||
""",
|
||||
_HELP_TXT,
|
||||
"""
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée A:
|
||||
<input type="file" size="30" name="file_a"/>
|
||||
</div>
|
||||
<div class="apo_compare_csv_form_but">
|
||||
Fichier Apogée B:
|
||||
<input type="file" size="30" name="file_b"/>
|
||||
</div>
|
||||
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
||||
<div class="apo_compare_csv_form_submit">
|
||||
<input type="submit" value="Comparer ces fichiers"/>
|
||||
</div>
|
||||
</form>""",
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||
"""Page comparing 2 Apogee CSV files"""
|
||||
@ -77,12 +114,17 @@ def apo_compare_csv(file_a, file_b, autodetect=True):
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
) from exc
|
||||
|
||||
return render_template(
|
||||
"apogee/apo_compare_csv.j2",
|
||||
title="Comparaison de fichiers Apogée",
|
||||
content=_apo_compare_csv(apo_data_a, apo_data_b),
|
||||
)
|
||||
H = [
|
||||
html_sco_header.sco_header(page_title="Comparaison de fichiers Apogée"),
|
||||
"<h2>Comparaison de fichiers Apogée</h2>",
|
||||
_HELP_TXT,
|
||||
'<div class="apo_compare_csv">',
|
||||
_apo_compare_csv(apo_data_a, apo_data_b),
|
||||
"</div>",
|
||||
"""<p><a href="apo_compare_csv_form" class="stdlink">Autre comparaison</a></p>""",
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def _load_apo_data(csvfile, autodetect=True):
|
||||
|
@ -43,13 +43,14 @@ import re
|
||||
import time
|
||||
from zipfile import ZipFile
|
||||
|
||||
from flask import g, send_file
|
||||
from flask import send_file
|
||||
import numpy as np
|
||||
|
||||
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
@ -78,6 +79,7 @@ from app.scodoc.codes_cursus import (
|
||||
)
|
||||
from app.scodoc import sco_cursus
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_etud
|
||||
|
||||
|
||||
def _apo_fmt_note(note, fmt="%3.2f"):
|
||||
@ -97,7 +99,7 @@ class EtuCol:
|
||||
"""Valeurs colonnes d'un element pour un etudiant"""
|
||||
|
||||
def __init__(self, nip, apo_elt, init_vals):
|
||||
pass
|
||||
pass # XXX
|
||||
|
||||
|
||||
ETUD_OK = "ok"
|
||||
@ -130,7 +132,7 @@ class ApoEtud(dict):
|
||||
"Vrai si BUT"
|
||||
self.col_elts = {}
|
||||
"{'V1RT': {'R': 'ADM', 'J': '', 'B': 20, 'N': '12.14'}}"
|
||||
self.etud: Identite | None = None
|
||||
self.etud: Identite = None
|
||||
"etudiant ScoDoc associé"
|
||||
self.etat = None # ETUD_OK, ...
|
||||
self.is_nar = False
|
||||
@ -148,9 +150,9 @@ class ApoEtud(dict):
|
||||
_apo_fmt_note, fmt=ScoDocSiteConfig.get_code_apo("NOTES_FMT") or "3.2f"
|
||||
)
|
||||
# Initialisés par associate_sco:
|
||||
self.autre_formsemestre: FormSemestre = None
|
||||
self.autre_sem: dict = None
|
||||
self.autre_res: NotesTableCompat = None
|
||||
self.cur_formsemestre: FormSemestre = None
|
||||
self.cur_sem: dict = None
|
||||
self.cur_res: NotesTableCompat = None
|
||||
self.new_cols = {}
|
||||
"{ col_id : value to record in csv }"
|
||||
@ -169,18 +171,24 @@ class ApoEtud(dict):
|
||||
Sinon, cherche le semestre, et met l'état à ETUD_OK ou ETUD_NON_INSCRIT.
|
||||
"""
|
||||
|
||||
self.etud = Identite.query.filter_by(
|
||||
code_nip=self["nip"], dept_id=g.scodoc_dept_id
|
||||
).first()
|
||||
if not self.etud:
|
||||
# futur: #WIP
|
||||
# etud: Identite = Identite.query.filter_by(code_nip=self["nip"], dept_id=g.scodoc_dept_id).first()
|
||||
# self.etud = etud
|
||||
etuds = sco_etud.get_etud_info(code_nip=self["nip"], filled=True)
|
||||
if not etuds:
|
||||
# pas dans ScoDoc
|
||||
self.etud = None
|
||||
self.log.append("non inscrit dans ScoDoc")
|
||||
self.etat = ETUD_ORPHELIN
|
||||
else:
|
||||
# futur: #WIP
|
||||
# formsemestre_ids = {
|
||||
# ins.formsemestre_id for ins in etud.formsemestre_inscriptions
|
||||
# }
|
||||
# in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
self.etud = etuds[0]
|
||||
# cherche le semestre ScoDoc correspondant à l'un de ceux de l'etape:
|
||||
formsemestre_ids = {
|
||||
ins.formsemestre_id for ins in self.etud.formsemestre_inscriptions
|
||||
}
|
||||
formsemestre_ids = {s["formsemestre_id"] for s in self.etud["sems"]}
|
||||
in_formsemestre_ids = formsemestre_ids.intersection(etape_formsemestre_ids)
|
||||
if not in_formsemestre_ids:
|
||||
self.log.append(
|
||||
@ -220,9 +228,7 @@ class ApoEtud(dict):
|
||||
self.new_cols[col_id] = self.cols[col_id]
|
||||
except KeyError as exc:
|
||||
raise ScoFormatError(
|
||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{
|
||||
col_id}</tt> non déclarée ?""",
|
||||
safe=True,
|
||||
f"""Fichier Apogee invalide : ligne mal formatée ? <br>colonne <tt>{col_id}</tt> non déclarée ?"""
|
||||
) from exc
|
||||
else:
|
||||
try:
|
||||
@ -248,7 +254,7 @@ class ApoEtud(dict):
|
||||
# codes = set([apo_data.apo_csv.cols[col_id].code for col_id in apo_data.apo_csv.col_ids])
|
||||
# return codes - set(sco_elts)
|
||||
|
||||
def search_elt_in_sem(self, code: str, sem: dict) -> dict:
|
||||
def search_elt_in_sem(self, code, sem) -> dict:
|
||||
"""
|
||||
VET code jury etape (en BUT, le code annuel)
|
||||
ELP élément pédagogique: UE, module
|
||||
@ -261,17 +267,13 @@ class ApoEtud(dict):
|
||||
Args:
|
||||
code (str): code apo de l'element cherché
|
||||
sem (dict): semestre dans lequel on cherche l'élément
|
||||
|
||||
Utilise notamment:
|
||||
cur_formsemestre : semestre "courant" pour résultats annuels (VET)
|
||||
autre_formsemestre : autre formsemestre utilisé pour les résultats annuels (VET)
|
||||
cur_sem (dict): semestre "courant" pour résultats annuels (VET)
|
||||
autre_sem (dict): autre semestre utilisé pour calculer les résultats annuels (VET)
|
||||
|
||||
Returns:
|
||||
dict: with N, B, J, R keys, ou None si elt non trouvé
|
||||
"""
|
||||
if not self.etud:
|
||||
return None
|
||||
etudid = self.etud.id
|
||||
etudid = self.etud["etudid"]
|
||||
if not self.cur_res:
|
||||
log("search_elt_in_sem: no cur_res !")
|
||||
return None
|
||||
@ -314,10 +316,10 @@ class ApoEtud(dict):
|
||||
code in {x.strip() for x in sem["elt_annee_apo"].split(",")}
|
||||
):
|
||||
export_res_etape = self.export_res_etape
|
||||
if (not export_res_etape) and self.cur_formsemestre:
|
||||
if (not export_res_etape) and self.cur_sem:
|
||||
# exporte toujours le résultat de l'étape si l'étudiant est diplômé
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
self.etud, self.cur_formsemestre.id
|
||||
self.etud, self.cur_sem["formsemestre_id"]
|
||||
)
|
||||
export_res_etape = Se.all_other_validated()
|
||||
|
||||
@ -327,15 +329,35 @@ class ApoEtud(dict):
|
||||
self.log.append("export étape désactivé")
|
||||
return VOID_APO_RES
|
||||
|
||||
# Element passage
|
||||
res_passage = self.search_elt_passage(code, res)
|
||||
if res_passage:
|
||||
return res_passage
|
||||
|
||||
# Elements UE
|
||||
res_ue = self.search_elt_ue(code, res)
|
||||
if res_ue:
|
||||
return res_ue
|
||||
decisions_ue = res.get_etud_decisions_ue(etudid)
|
||||
for ue in res.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in {
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if (
|
||||
decisions_ue and ue["ue_id"] in decisions_ue
|
||||
) or self.export_res_sdj:
|
||||
ue_status = res.get_etud_ue_status(etudid, ue["ue_id"])
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
||||
code_decision_ue
|
||||
)
|
||||
else:
|
||||
code_decision_ue_apo = ""
|
||||
return dict(
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=code_decision_ue_apo,
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
|
||||
# Elements Modules
|
||||
modimpls = res.get_modimpls_dict()
|
||||
@ -355,79 +377,9 @@ class ApoEtud(dict):
|
||||
|
||||
if module_code_found:
|
||||
return VOID_APO_RES
|
||||
|
||||
# RCUE du BUT (validations enregistrées seulement, pas avant jury)
|
||||
if res.is_apc:
|
||||
for val_rcue in ApcValidationRCUE.query.filter_by(
|
||||
etudid=etudid, formsemestre_id=sem["formsemestre_id"]
|
||||
):
|
||||
if code in val_rcue.get_codes_apogee():
|
||||
return dict(
|
||||
N="", # n'exporte pas de moyenne RCUE
|
||||
B=20,
|
||||
J="",
|
||||
R=ScoDocSiteConfig.get_code_apo(val_rcue.code),
|
||||
M="",
|
||||
)
|
||||
#
|
||||
return None # element Apogee non trouvé dans ce semestre
|
||||
|
||||
def search_elt_ue(self, code: str, res: NotesTableCompat) -> dict:
|
||||
"""Cherche un résultat d'UE pour ce code Apogée.
|
||||
dict vide si pas de résultat trouvé pour ce code.
|
||||
"""
|
||||
decisions_ue = res.get_etud_decisions_ue(self.etud.id)
|
||||
for ue in res.get_ues_stat_dict():
|
||||
if ue["code_apogee"] and code in {
|
||||
x.strip() for x in ue["code_apogee"].split(",")
|
||||
}:
|
||||
if self.export_res_ues:
|
||||
if (
|
||||
decisions_ue and ue["ue_id"] in decisions_ue
|
||||
) or self.export_res_sdj:
|
||||
# Si dispensé de cette UE, n'exporte rien
|
||||
if (self.etud.id, ue["ue_id"]) in res.dispense_ues:
|
||||
return VOID_APO_RES
|
||||
ue_status = res.get_etud_ue_status(self.etud.id, ue["ue_id"])
|
||||
if decisions_ue and ue["ue_id"] in decisions_ue:
|
||||
code_decision_ue = decisions_ue[ue["ue_id"]]["code"]
|
||||
code_decision_ue_apo = ScoDocSiteConfig.get_code_apo(
|
||||
code_decision_ue
|
||||
)
|
||||
else:
|
||||
code_decision_ue_apo = ""
|
||||
return dict(
|
||||
N=self.fmt_note(ue_status["moy"] if ue_status else ""),
|
||||
B=20,
|
||||
J="",
|
||||
R=code_decision_ue_apo,
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
else:
|
||||
return VOID_APO_RES
|
||||
return {} # no UE result found for this code
|
||||
|
||||
def search_elt_passage(self, code: str, res: NotesTableCompat) -> dict:
|
||||
"""Cherche un résultat de type "passage" pour ce code Apogée.
|
||||
dict vide si pas de résultat trouvé pour ce code.
|
||||
L'élement est rempli si:
|
||||
- code est dans les codes passage du formsemestre (sem)
|
||||
- autorisation d'inscription enregistre de sem vers sem d'indice suivant
|
||||
"""
|
||||
if res.formsemestre.semestre_id < 1:
|
||||
return {}
|
||||
next_semestre_id = res.formsemestre.semestre_id + 1
|
||||
if code in res.formsemestre.get_codes_apogee(category="passage"):
|
||||
if next_semestre_id in res.get_autorisations_inscription().get(
|
||||
self.etud.id, set()
|
||||
):
|
||||
return dict(
|
||||
N="", B=20, J="", R=ScoDocSiteConfig.get_code_apo("ADM"), M=""
|
||||
)
|
||||
return {}
|
||||
|
||||
def comp_elt_semestre(self, nt: NotesTableCompat, decision: dict, etudid: int):
|
||||
"""Calcul résultat apo semestre.
|
||||
Toujours vide pour en BUT/APC.
|
||||
@ -466,10 +418,11 @@ class ApoEtud(dict):
|
||||
#
|
||||
# XXX cette règle est discutable, à valider
|
||||
|
||||
if not self.cur_formsemestre:
|
||||
# log('comp_elt_annuel cur_sem=%s autre_sem=%s' % (cur_sem['formsemestre_id'], autre_sem['formsemestre_id']))
|
||||
if not self.cur_sem:
|
||||
# l'étudiant n'a pas de semestre courant ?!
|
||||
self.log.append("pas de semestre courant")
|
||||
log(f"comp_elt_annuel: etudid {etudid} has no cur_formsemestre")
|
||||
log(f"comp_elt_annuel: etudid {etudid} has no cur_sem")
|
||||
return VOID_APO_RES
|
||||
|
||||
if self.is_apc:
|
||||
@ -485,7 +438,7 @@ class ApoEtud(dict):
|
||||
# ne touche pas aux RATs
|
||||
return VOID_APO_RES
|
||||
|
||||
if not self.autre_formsemestre:
|
||||
if not self.autre_sem:
|
||||
# formations monosemestre, ou code VET semestriel,
|
||||
# ou jury intermediaire et etudiant non redoublant...
|
||||
return self.comp_elt_semestre(self.cur_res, cur_decision, etudid)
|
||||
@ -565,7 +518,7 @@ class ApoEtud(dict):
|
||||
self.validation_annee_but: ApcValidationAnnee = (
|
||||
ApcValidationAnnee.query.filter_by(
|
||||
formsemestre_id=formsemestre.id,
|
||||
etudid=self.etud.id,
|
||||
etudid=self.etud["etudid"],
|
||||
referentiel_competence_id=self.cur_res.formsemestre.formation.referentiel_competence_id,
|
||||
).first()
|
||||
)
|
||||
@ -574,7 +527,7 @@ class ApoEtud(dict):
|
||||
)
|
||||
|
||||
def etud_set_semestres_de_etape(self, apo_data: "ApoData"):
|
||||
"""Set .cur_formsemestre and .autre_formsemestre et charge les résultats.
|
||||
"""Set .cur_sem and .autre_sem et charge les résultats.
|
||||
Lorsqu'on a une formation semestrialisée mais avec un code étape annuel,
|
||||
il faut considérer les deux semestres ((S1,S2) ou (S3,S4)) pour calculer
|
||||
le code annuel (VET ou VRT1A (voir elt_annee_apo)).
|
||||
@ -582,48 +535,52 @@ class ApoEtud(dict):
|
||||
Pour les jurys intermediaires (janvier, S1 ou S3): (S2 ou S4) de la même
|
||||
étape lors d'une année précédente ?
|
||||
|
||||
Set cur_formsemestre: le formsemestre "courant"
|
||||
et autre_formsemestre, ou None s'il n'y en a pas.
|
||||
Set cur_sem: le semestre "courant" et autre_sem, ou None s'il n'y en a pas.
|
||||
"""
|
||||
# Cherche le formsemestre "courant":
|
||||
cur_formsemestres = [
|
||||
formsemestre
|
||||
for formsemestre in self.etud.get_formsemestres()
|
||||
# Cherche le semestre "courant":
|
||||
cur_sems = [
|
||||
sem
|
||||
for sem in self.etud["sems"]
|
||||
if (
|
||||
(formsemestre.semestre_id == apo_data.cur_semestre_id)
|
||||
and (apo_data.etape in formsemestre.etapes)
|
||||
(sem["semestre_id"] == apo_data.cur_semestre_id)
|
||||
and (apo_data.etape in sem["etapes"])
|
||||
and (
|
||||
FormSemestre.est_in_semestre_scolaire(
|
||||
formsemestre.date_debut,
|
||||
sco_formsemestre.sem_in_semestre_scolaire(
|
||||
sem,
|
||||
apo_data.annee_scolaire,
|
||||
0, # annee complete
|
||||
)
|
||||
)
|
||||
)
|
||||
]
|
||||
cur_formsemestre = None
|
||||
if cur_formsemestres:
|
||||
# prend le plus récent avec décision
|
||||
for formsemestre in cur_formsemestres:
|
||||
if not cur_sems:
|
||||
cur_sem = None
|
||||
else:
|
||||
# prend le plus recent avec decision
|
||||
cur_sem = None
|
||||
for sem in cur_sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if apo_data.export_res_sdj or res.etud_has_decision(self.etud.id):
|
||||
cur_formsemestre = formsemestre
|
||||
has_decision = res.etud_has_decision(self.etud["etudid"])
|
||||
if has_decision:
|
||||
cur_sem = sem
|
||||
self.cur_res = res
|
||||
break
|
||||
if cur_formsemestres is None:
|
||||
cur_formsemestre = cur_formsemestres[
|
||||
0
|
||||
] # aucun avec décision, prend le plus recent
|
||||
if res.formsemestre.id == cur_formsemestre.id:
|
||||
if cur_sem is None:
|
||||
cur_sem = cur_sems[0] # aucun avec décision, prend le plus recent
|
||||
if res.formsemestre.id == cur_sem["formsemestre_id"]:
|
||||
self.cur_res = res
|
||||
else:
|
||||
self.cur_res = res_sem.load_formsemestre_results(cur_formsemestre)
|
||||
formsemestre = FormSemestre.query.get_or_404(
|
||||
cur_sem["formsemestre_id"]
|
||||
)
|
||||
self.cur_res = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
self.cur_formsemestre = cur_formsemestre
|
||||
self.cur_sem = cur_sem
|
||||
|
||||
if apo_data.cur_semestre_id <= 0:
|
||||
# autre_formsemestre non pertinent pour sessions sans semestres:
|
||||
self.autre_formsemestre = None
|
||||
# "autre_sem" non pertinent pour sessions sans semestres:
|
||||
self.autre_sem = None
|
||||
self.autre_res = None
|
||||
return
|
||||
|
||||
@ -644,49 +601,52 @@ class ApoEtud(dict):
|
||||
courant_mois_debut = 1 # ou 2 (fev-jul)
|
||||
else:
|
||||
raise ValueError("invalid periode value !") # bug ?
|
||||
courant_date_debut = datetime.date(
|
||||
day=1, month=courant_mois_debut, year=courant_annee_debut
|
||||
courant_date_debut = "%d-%02d-01" % (
|
||||
courant_annee_debut,
|
||||
courant_mois_debut,
|
||||
)
|
||||
else:
|
||||
courant_date_debut = datetime.date(day=31, month=12, year=9999)
|
||||
courant_date_debut = "9999-99-99"
|
||||
|
||||
# etud['sems'] est la liste des semestres de l'étudiant, triés par date,
|
||||
# le plus récemment effectué en tête.
|
||||
# Cherche les semestres (antérieurs) de l'indice autre de la même étape apogée
|
||||
# s'il y en a plusieurs, choisit le plus récent ayant une décision
|
||||
|
||||
autres_sems = []
|
||||
for formsemestre in self.etud.get_formsemestres():
|
||||
for sem in self.etud["sems"]:
|
||||
if (
|
||||
formsemestre.semestre_id == autre_semestre_id
|
||||
and apo_data.etape_apogee in formsemestre.etapes
|
||||
sem["semestre_id"] == autre_semestre_id
|
||||
and apo_data.etape_apogee in sem["etapes"]
|
||||
):
|
||||
if (
|
||||
formsemestre.date_debut < courant_date_debut
|
||||
sem["date_debut_iso"] < courant_date_debut
|
||||
): # on demande juste qu'il ait démarré avant
|
||||
autres_sems.append(formsemestre)
|
||||
autres_sems.append(sem)
|
||||
if not autres_sems:
|
||||
autre_formsemestre = None
|
||||
autre_sem = None
|
||||
elif len(autres_sems) == 1:
|
||||
autre_formsemestre = autres_sems[0]
|
||||
autre_sem = autres_sems[0]
|
||||
else:
|
||||
autre_formsemestre = None
|
||||
for formsemestre in autres_sems:
|
||||
autre_sem = None
|
||||
for sem in autres_sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if res.is_apc:
|
||||
has_decision = res.etud_has_decision(self.etud.id)
|
||||
has_decision = res.etud_has_decision(self.etud["etudid"])
|
||||
else:
|
||||
has_decision = res.get_etud_decision_sem(self.etud.id)
|
||||
if has_decision or apo_data.export_res_sdj:
|
||||
autre_formsemestre = formsemestre
|
||||
has_decision = res.get_etud_decision_sem(self.etud["etudid"])
|
||||
if has_decision:
|
||||
autre_sem = sem
|
||||
break
|
||||
if autre_formsemestre is None:
|
||||
autre_formsemestre = autres_sems[
|
||||
0
|
||||
] # aucun avec decision, prend le plus recent
|
||||
if autre_sem is None:
|
||||
autre_sem = autres_sems[0] # aucun avec decision, prend le plus recent
|
||||
|
||||
self.autre_formsemestre = autre_formsemestre
|
||||
self.autre_sem = autre_sem
|
||||
# Charge les résultats:
|
||||
if autre_formsemestre:
|
||||
self.autre_res = res_sem.load_formsemestre_results(self.autre_formsemestre)
|
||||
if autre_sem:
|
||||
formsemestre = FormSemestre.query.get_or_404(autre_sem["formsemestre_id"])
|
||||
self.autre_res = res_sem.load_formsemestre_results(formsemestre)
|
||||
else:
|
||||
self.autre_res = None
|
||||
|
||||
@ -728,8 +688,7 @@ class ApoData:
|
||||
filename = self.orig_filename or e.filename
|
||||
raise ScoFormatError(
|
||||
f"""<h3>Erreur lecture du fichier Apogée <tt>{filename}</tt></h3>
|
||||
<p>{e.args[0]}</p>""",
|
||||
safe=True,
|
||||
<p>{e.args[0]}</p>"""
|
||||
) from e
|
||||
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
|
||||
self.vdi_apogee = self.get_vdi_apogee() # '111'
|
||||
@ -821,9 +780,7 @@ class ApoData:
|
||||
self.sems_periode = None
|
||||
|
||||
def get_etape_apogee(self) -> str:
|
||||
"""Le code etape: 'V1RT', donné par le code de l'élément VET.
|
||||
Le VET doit être parmi les colonnes de la section XX-APO_COLONNES-XX
|
||||
"""
|
||||
"""Le code etape: 'V1RT', donné par le code de l'élément VET"""
|
||||
for elt in self.apo_csv.apo_elts.values():
|
||||
if elt.type_objet == "VET":
|
||||
return elt.code
|
||||
@ -888,8 +845,7 @@ class ApoData:
|
||||
log(f"Colonnes presentes: {present}")
|
||||
raise ScoFormatError(
|
||||
f"""Fichier Apogee invalide<br>Colonnes declarees: <tt>{declared}</tt>
|
||||
<br>Colonnes presentes: <tt>{present}</tt>""",
|
||||
safe=True,
|
||||
<br>Colonnes presentes: <tt>{present}</tt>"""
|
||||
) from exc
|
||||
# l'ensemble de tous les codes des elements apo des semestres:
|
||||
sem_elems = reduce(set.union, list(self.get_codes_by_sem().values()), set())
|
||||
@ -917,16 +873,6 @@ class ApoData:
|
||||
codes_ues = set().union(
|
||||
*[ue.get_codes_apogee() for ue in formsemestre.get_ues(with_sport=True)]
|
||||
)
|
||||
codes_rcues = (
|
||||
set().union(
|
||||
*[
|
||||
ue.get_codes_apogee_rcue()
|
||||
for ue in formsemestre.get_ues(with_sport=True)
|
||||
]
|
||||
)
|
||||
if self.is_apc
|
||||
else set()
|
||||
)
|
||||
s = set()
|
||||
codes_by_sem[sem["formsemestre_id"]] = s
|
||||
for col_id in self.apo_csv.col_ids[4:]:
|
||||
@ -939,18 +885,13 @@ class ApoData:
|
||||
if code in codes_ues:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à un RCUE BUT
|
||||
if code in codes_rcues:
|
||||
s.add(code)
|
||||
continue
|
||||
# associé à un module:
|
||||
if code in codes_modules:
|
||||
s.add(code)
|
||||
|
||||
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
|
||||
return codes_by_sem
|
||||
|
||||
def build_cr_table(self) -> GenTable:
|
||||
def build_cr_table(self):
|
||||
"""Table compte rendu des décisions"""
|
||||
rows = [] # tableau compte rendu des decisions
|
||||
for apo_etud in self.etuds:
|
||||
@ -972,14 +913,14 @@ class ApoData:
|
||||
columns_ids = ["NIP", "nom", "prenom"]
|
||||
columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire"))
|
||||
|
||||
table = GenTable(
|
||||
T = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
titles=dict(zip(columns_ids, columns_ids)),
|
||||
rows=rows,
|
||||
table_id="build_cr_table",
|
||||
xls_sheet_name="Decisions ScoDoc",
|
||||
)
|
||||
return table
|
||||
return T
|
||||
|
||||
def build_adsup_table(self):
|
||||
"""Construit une table listant les ADSUP émis depuis les formsemestres
|
||||
|
@ -299,14 +299,11 @@ class ApoCSVReadWrite:
|
||||
for i, field in enumerate(fields):
|
||||
cols[self.col_ids[i]] = field
|
||||
except IndexError as exc:
|
||||
raise
|
||||
raise ScoFormatError(
|
||||
f"Fichier Apogee incorrect (colonnes excédentaires ? (<tt>{i}/{field}</tt>))",
|
||||
filename=self.get_filename(),
|
||||
safe=True,
|
||||
) from exc
|
||||
# Ajoute colonnes vides manquantes, pratique si on a édité le fichier Apo à la main...
|
||||
for i in range(len(fields), len(self.col_ids)):
|
||||
cols[self.col_ids[i]] = ""
|
||||
etud_tuples.append(
|
||||
ApoEtudTuple(
|
||||
nip=fields[0], # id etudiant
|
||||
@ -340,8 +337,6 @@ class ApoCSVReadWrite:
|
||||
fields = line.split(APO_SEP)
|
||||
if len(fields) == 2:
|
||||
k, v = fields
|
||||
elif len(fields) == 1:
|
||||
k, v = fields[0], ""
|
||||
else:
|
||||
log(f"Error read CSV: \nline={line}\nfields={fields}")
|
||||
log(dir(f))
|
||||
|
@ -139,7 +139,7 @@ class BaseArchiver:
|
||||
dirs = glob.glob(base + "*")
|
||||
return [os.path.split(x)[1] for x in dirs]
|
||||
|
||||
def list_obj_archives(self, oid: int, dept_id: int = None) -> list[str]:
|
||||
def list_obj_archives(self, oid: int, dept_id: int = None):
|
||||
"""Returns
|
||||
:return: list of archive identifiers for this object (paths to non empty dirs)
|
||||
"""
|
||||
|
@ -3,15 +3,13 @@ Ecrit par Matthias Hartmann.
|
||||
"""
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
from functools import wraps
|
||||
from pytz import UTC
|
||||
|
||||
from flask import g, request
|
||||
from flask import g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import log, db, set_sco_dept
|
||||
from app.models import (
|
||||
Evaluation,
|
||||
Identite,
|
||||
FormSemestre,
|
||||
FormSemestreInscription,
|
||||
@ -19,13 +17,12 @@ from app.models import (
|
||||
ModuleImplInscription,
|
||||
ScoDocSiteConfig,
|
||||
)
|
||||
from app.models.assiduites import Assiduite, Justificatif, has_assiduites_disable_pref
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_etud
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
class CountCalculator:
|
||||
@ -734,125 +731,6 @@ def create_absence_billet(
|
||||
return calculator.to_dict()["demi"]
|
||||
|
||||
|
||||
def get_evaluation_assiduites(evaluation: Evaluation) -> Query:
|
||||
"""
|
||||
Renvoie une query d'assiduité en fonction des étudiants inscrits à l'évaluation
|
||||
et de la date de l'évaluation.
|
||||
|
||||
Attention : Si l'évaluation n'a pas de date, renvoie une liste vide
|
||||
"""
|
||||
|
||||
# Evaluation sans date
|
||||
if evaluation.date_debut is None:
|
||||
return []
|
||||
|
||||
# Récupération des étudiants inscrits à l'évaluation
|
||||
etuds: Query = Identite.query.join(
|
||||
ModuleImplInscription, Identite.id == ModuleImplInscription.etudid
|
||||
).filter(ModuleImplInscription.moduleimpl_id == evaluation.moduleimpl_id)
|
||||
|
||||
etudids: list[int] = [etud.id for etud in etuds]
|
||||
|
||||
# Récupération des assiduités des étudiants inscrits à l'évaluation
|
||||
date_debut: datetime = evaluation.date_debut
|
||||
date_fin: datetime
|
||||
|
||||
if evaluation.date_fin is not None:
|
||||
date_fin = evaluation.date_fin
|
||||
else:
|
||||
# On met à la fin de la journée de date_debut
|
||||
date_fin = datetime.combine(date_debut.date(), time.max)
|
||||
|
||||
# Filtrage par rapport à la plage de l'évaluation
|
||||
assiduites: Query = Assiduite.query.filter(
|
||||
Assiduite.date_debut >= date_debut,
|
||||
Assiduite.date_fin <= date_fin,
|
||||
Assiduite.etudid.in_(etudids),
|
||||
)
|
||||
|
||||
return assiduites
|
||||
|
||||
|
||||
def get_etud_evaluations_assiduites(etud: Identite) -> list[dict]:
|
||||
"""
|
||||
Retourne la liste des évaluations d'un étudiant. Pour chaque évaluation,
|
||||
retourne la liste des assiduités concernant la plage de l'évaluation.
|
||||
"""
|
||||
|
||||
etud_evaluations_assiduites: list[dict] = []
|
||||
|
||||
# On récupère les moduleimpls puis les évaluations liés aux moduleimpls
|
||||
modsimpl_ids: list[int] = [
|
||||
modimp_inscr.moduleimpl_id
|
||||
for modimp_inscr in ModuleImplInscription.query.filter_by(etudid=etud.id)
|
||||
]
|
||||
evaluations: Query = Evaluation.query.filter(
|
||||
Evaluation.moduleimpl_id.in_(modsimpl_ids)
|
||||
)
|
||||
# Pour chaque évaluation, on récupère l'assiduité de l'étudiant sur la plage
|
||||
# de l'évaluation
|
||||
|
||||
for evaluation in evaluations:
|
||||
eval_assis: dict = {"evaluation_id": evaluation.id, "assiduites": []}
|
||||
# Pas d'assiduités si pas de date
|
||||
if evaluation.date_debut is not None:
|
||||
date_debut: datetime = evaluation.date_debut
|
||||
date_fin: datetime
|
||||
|
||||
if evaluation.date_fin is not None:
|
||||
date_fin = evaluation.date_fin
|
||||
else:
|
||||
# On met à la fin de la journée de date_debut
|
||||
date_fin = datetime.combine(date_debut.date(), time.max)
|
||||
|
||||
# Filtrage par rapport à la plage de l'évaluation
|
||||
assiduites: Query = etud.assiduites.filter(
|
||||
Assiduite.date_debut >= date_debut,
|
||||
Assiduite.date_fin <= date_fin,
|
||||
)
|
||||
# On récupère les assiduités et on met à jour le dictionnaire
|
||||
eval_assis["assiduites"] = [
|
||||
assi.to_dict(format_api=True) for assi in assiduites
|
||||
]
|
||||
|
||||
# On ajoute le dictionnaire à la liste des évaluations
|
||||
etud_evaluations_assiduites.append(eval_assis)
|
||||
|
||||
return etud_evaluations_assiduites
|
||||
|
||||
|
||||
# --- Décorateur ---
|
||||
|
||||
|
||||
def check_disabled(func):
|
||||
"""
|
||||
Vérifie sur le module a été désactivé dans les préférences du semestre.
|
||||
Récupère le formsemestre depuis l'url (formsemestre_id)
|
||||
Si le formsemestre est trouvé :
|
||||
- Vérifie si le module a été désactivé dans les préférences du semestre
|
||||
- Si le module a été désactivé, une ScoValueError est levée
|
||||
Sinon :
|
||||
Il ne se passe rien
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
def decorated_function(*args, **kwargs):
|
||||
# Récupération du formsemestre depuis l'url
|
||||
formsemestre_id = request.args.get("formsemestre_id")
|
||||
# Si on a un formsemestre_id
|
||||
if formsemestre_id:
|
||||
# Récupération du formsemestre
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
# Vériication si le module a été désactivé (avec la préférence)
|
||||
pref: str | bool = has_assiduites_disable_pref(formsemestre)
|
||||
# Le module est désactivé si on récupère un message d'erreur (str)
|
||||
if pref:
|
||||
raise ScoValueError(pref, dest_url=request.referrer)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
# Gestion du cache
|
||||
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
@ -873,7 +751,7 @@ def formsemestre_get_assiduites_count(
|
||||
) -> tuple[int, int, int]:
|
||||
"""Les comptes d'absences de cet étudiant dans ce semestre:
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
Utilise un cache (si moduleimpl_id n'est pas spécifié).
|
||||
Utilise un cache.
|
||||
"""
|
||||
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
|
||||
return get_assiduites_count_in_interval(
|
||||
@ -901,7 +779,7 @@ def get_assiduites_count_in_interval(
|
||||
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
|
||||
tuple (nb abs non justifiées, nb abs justifiées, nb abs total)
|
||||
On peut spécifier les dates comme datetime ou iso.
|
||||
Utilise un cache (si moduleimpl_id n'est pas spécifié).
|
||||
Utilise un cache.
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.strftime("%Y-%m-%d")
|
||||
date_fin_iso = date_fin_iso or date_fin.strftime("%Y-%m-%d")
|
||||
|
@ -69,7 +69,6 @@ from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_pv_dict
|
||||
from app.scodoc import sco_pv_lettres_inviduelles
|
||||
from app.scodoc import sco_users
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType, fmt_note
|
||||
@ -710,11 +709,7 @@ def etud_descr_situation_semestre(
|
||||
decisions_ue : noms (acronymes) des UE validées, séparées par des virgules.
|
||||
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
|
||||
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
|
||||
diplomation : "Diplôme obtenu." ou ""
|
||||
parcours_titre, parcours_code, refcomp_specialite, refcomp_specialite_long
|
||||
|
||||
diplome_dut120_descr: phrase explicative si DUT enregistré (en BUT)
|
||||
diplome_dut120: booléen, vrai si DUT enregistré (en BUT)
|
||||
"""
|
||||
# Fonction utilisée par tous les bulletins (APC ou classiques)
|
||||
infos = collections.defaultdict(str)
|
||||
@ -765,19 +760,17 @@ def etud_descr_situation_semestre(
|
||||
infos["descr_decisions_niveaux"] = infos["descr_decisions_rcue"] = ""
|
||||
infos["descr_decision_annee"] = ""
|
||||
|
||||
infos["descr_demission"] = f"Démission le {date_dem}." if date_dem else ""
|
||||
infos["date_demission"] = date_dem if date_dem else ""
|
||||
|
||||
if date_dem:
|
||||
infos["descr_demission"] = f"Démission le {date_dem}."
|
||||
infos["date_demission"] = date_dem
|
||||
infos["decision_jury"] = infos["descr_decision_jury"] = "Démission"
|
||||
infos["situation"] = ". ".join(
|
||||
[x for x in [infos["descr_inscription"], infos["descr_demission"]] if x]
|
||||
)
|
||||
return infos, None # ne donne pas les dec. de jury pour les demissionnaires
|
||||
|
||||
infos["descr_defaillance"] = f"Défaillant{ne}" if date_def else ""
|
||||
infos["date_defaillance"] = date_def or ""
|
||||
if date_def:
|
||||
infos["descr_defaillance"] = f"Défaillant{ne}"
|
||||
infos["date_defaillance"] = date_def
|
||||
infos["descr_decision_jury"] = f"Défaillant{ne}"
|
||||
|
||||
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
|
||||
@ -832,18 +825,12 @@ def etud_descr_situation_semestre(
|
||||
)
|
||||
else:
|
||||
descr_dec += " Diplôme obtenu."
|
||||
|
||||
infos["diplomation"] = "Diplôme obtenu." if pv["validation_parcours"] else ""
|
||||
# Ajoute diplome_dut120_descr et diplome_dut120
|
||||
sco_pv_lettres_inviduelles.add_dut120_infos(formsemestre, etudid, infos)
|
||||
|
||||
_format_situation_fields(
|
||||
infos,
|
||||
[
|
||||
"descr_inscription",
|
||||
"descr_defaillance",
|
||||
"descr_decisions_ue",
|
||||
"diplome_dut120_descr",
|
||||
"descr_decision_annee",
|
||||
],
|
||||
[descr_dec, descr_mention, descr_autorisations],
|
||||
@ -898,9 +885,7 @@ def _dates_insc_dem_def(etudid, formsemestre_id) -> tuple:
|
||||
def _format_situation_fields(
|
||||
infos, field_names: list[str], extra_values: list[str]
|
||||
) -> None:
|
||||
"""Réuni les champs pour former le paragraphe "situation", et ajoute la pontuation
|
||||
aux champs.
|
||||
"""
|
||||
"""Réuni les champs pour former le paragraphe "situation", et ajoute la pontuation aux champs."""
|
||||
infos["situation"] = ". ".join(
|
||||
x
|
||||
for x in [infos.get(field_name, "") for field_name in field_names]
|
||||
@ -1102,9 +1087,7 @@ def do_formsemestre_bulletinetud(
|
||||
flash(f"{etud.nomprenom} n'a pas d'adresse e-mail !")
|
||||
return False, bul_dict["filigranne"]
|
||||
else:
|
||||
mail_bulletin(
|
||||
formsemestre, etud, bul_dict, pdfdata, filename, recipient_addr
|
||||
)
|
||||
mail_bulletin(formsemestre.id, bul_dict, pdfdata, filename, recipient_addr)
|
||||
flash(f"mail envoyé à {recipient_addr}")
|
||||
|
||||
return True, bul_dict["filigranne"]
|
||||
@ -1112,28 +1095,22 @@ def do_formsemestre_bulletinetud(
|
||||
raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({fmt})")
|
||||
|
||||
|
||||
def mail_bulletin(
|
||||
formsemestre: FormSemestre,
|
||||
etud: Identite,
|
||||
infos: dict,
|
||||
pdfdata,
|
||||
filename,
|
||||
recipient_addr,
|
||||
):
|
||||
def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
|
||||
"""Send bulletin by email to etud
|
||||
If bul_mail_list_abs pref is true, put list of absences in mail body (text).
|
||||
"""
|
||||
webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre.id)
|
||||
etud = infos["etud"]
|
||||
webmaster = sco_preferences.get_preference("bul_mail_contact_addr", formsemestre_id)
|
||||
dept = scu.unescape_html(
|
||||
sco_preferences.get_preference("DeptName", formsemestre.id)
|
||||
sco_preferences.get_preference("DeptName", formsemestre_id)
|
||||
)
|
||||
copy_addr = sco_preferences.get_preference("email_copy_bulletins", formsemestre.id)
|
||||
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre.id)
|
||||
copy_addr = sco_preferences.get_preference("email_copy_bulletins", formsemestre_id)
|
||||
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
|
||||
|
||||
if intro_mail:
|
||||
try:
|
||||
hea = intro_mail % {
|
||||
"nomprenom": etud.nom_prenom(),
|
||||
"nomprenom": etud["nomprenom"],
|
||||
"dept": dept,
|
||||
"webmaster": webmaster,
|
||||
}
|
||||
@ -1147,12 +1124,12 @@ def mail_bulletin(
|
||||
if sco_preferences.get_preference("bul_mail_list_abs"):
|
||||
from app.views.assiduites import generate_bul_list
|
||||
|
||||
etud_identite: Identite = Identite.get_etud(etud["etudid"])
|
||||
form_semestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
hea += "\n\n"
|
||||
hea += generate_bul_list(etud, formsemestre)
|
||||
hea += generate_bul_list(etud_identite, form_semestre)
|
||||
|
||||
subject = f"""Relevé de notes du semestre {
|
||||
formsemestre.semestre_id if formsemestre.semestre_id >= 0 else ''
|
||||
} de {etud.nom_prenom()}"""
|
||||
subject = f"""Relevé de notes de {etud["nomprenom"]}"""
|
||||
recipients = [recipient_addr]
|
||||
sender = email.get_from_addr()
|
||||
if copy_addr:
|
||||
|
@ -445,10 +445,7 @@ def dict_decision_jury(
|
||||
...
|
||||
],
|
||||
'situation': 'Inscrit le 25/06/2021. Décision jury: Validé. UE acquises: '
|
||||
'UE31, UE32. Diplôme obtenu.',
|
||||
'diplomation' : 'Diplôme obtenu.' # (ou vide)
|
||||
|
||||
}
|
||||
'UE31, UE32. Diplôme obtenu.'}
|
||||
"""
|
||||
from app.scodoc import sco_bulletins
|
||||
|
||||
@ -461,10 +458,7 @@ def dict_decision_jury(
|
||||
formsemestre,
|
||||
show_uevalid=prefs["bul_show_uevalid"],
|
||||
)
|
||||
d["diplomation"] = infos["diplomation"]
|
||||
d["situation"] = infos["situation"]
|
||||
d["diplome_dut120"] = infos["diplome_dut120"]
|
||||
d["diplome_dut120_descr"] = infos["diplome_dut120_descr"]
|
||||
if dpv:
|
||||
decision = dpv["decisions"][0]
|
||||
etat = decision["etat"]
|
||||
|
102
app/scodoc/sco_compute_moy.py
Normal file
102
app/scodoc/sco_compute_moy.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""Calcul des moyennes de module (restes de fonctions ScoDoc 7)
|
||||
"""
|
||||
from app.models import ModuleImpl
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
def moduleimpl_has_expression(modimpl: ModuleImpl):
|
||||
"""True if we should use a user-defined expression
|
||||
En ScoDoc 9, utilisé pour afficher un avertissement, l'expression elle même
|
||||
n'est plus supportée.
|
||||
"""
|
||||
return (
|
||||
modimpl.computation_expr
|
||||
and modimpl.computation_expr.strip()
|
||||
and modimpl.computation_expr.strip()[0] != "#"
|
||||
)
|
||||
|
||||
|
||||
def formsemestre_expressions_use_abscounts(formsemestre_id):
|
||||
"""True si les notes de ce semestre dépendent des compteurs d'absences.
|
||||
Cela n'est normalement pas le cas, sauf si des formules utilisateur
|
||||
utilisent ces compteurs.
|
||||
"""
|
||||
# check presence of 'nbabs' in expressions
|
||||
ab = "nb_abs" # chaine recherchée
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# 1- moyennes d'UE:
|
||||
elist = formsemestre_ue_computation_expr_list(
|
||||
cnx, {"formsemestre_id": formsemestre_id}
|
||||
)
|
||||
for e in elist:
|
||||
expr = e["computation_expr"].strip()
|
||||
if expr and expr[0] != "#" and ab in expr:
|
||||
return True
|
||||
# 2- moyennes de modules
|
||||
# #sco9 il n'y a plus d'expressions
|
||||
# for mod in sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id):
|
||||
# if moduleimpl_has_expression(mod) and ab in mod["computation_expr"]:
|
||||
# return True
|
||||
return False
|
||||
|
||||
|
||||
_formsemestre_ue_computation_exprEditor = ndb.EditableTable(
|
||||
"notes_formsemestre_ue_computation_expr",
|
||||
"notes_formsemestre_ue_computation_expr_id",
|
||||
(
|
||||
"notes_formsemestre_ue_computation_expr_id",
|
||||
"formsemestre_id",
|
||||
"ue_id",
|
||||
"computation_expr",
|
||||
),
|
||||
html_quote=False, # does nt automatically quote
|
||||
)
|
||||
formsemestre_ue_computation_expr_create = _formsemestre_ue_computation_exprEditor.create
|
||||
formsemestre_ue_computation_expr_delete = _formsemestre_ue_computation_exprEditor.delete
|
||||
formsemestre_ue_computation_expr_list = _formsemestre_ue_computation_exprEditor.list
|
||||
formsemestre_ue_computation_expr_edit = _formsemestre_ue_computation_exprEditor.edit
|
||||
|
||||
|
||||
def get_ue_expression(formsemestre_id, ue_id, html_quote=False):
|
||||
"""Returns UE expression (formula), or None if no expression has been defined"""
|
||||
cnx = ndb.GetDBConnexion()
|
||||
el = formsemestre_ue_computation_expr_list(
|
||||
cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id}
|
||||
)
|
||||
if not el:
|
||||
return None
|
||||
else:
|
||||
expr = el[0]["computation_expr"].strip()
|
||||
if expr and expr[0] != "#":
|
||||
if html_quote:
|
||||
expr = ndb.quote_html(expr)
|
||||
return expr
|
||||
else:
|
||||
return None
|
@ -34,13 +34,13 @@ from app.scodoc import sco_cursus_dut
|
||||
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.comp import res_sem
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
# SituationEtudParcours -> get_situation_etud_cursus
|
||||
def get_situation_etud_cursus(
|
||||
etud: Identite, formsemestre_id: int
|
||||
etud: dict, formsemestre_id: int
|
||||
) -> sco_cursus_dut.SituationEtudCursus:
|
||||
"""renvoie une instance de SituationEtudCursus (ou sous-classe spécialisée)"""
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
|
@ -31,18 +31,13 @@
|
||||
from app import db
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarAutorisationInscription,
|
||||
Scolog,
|
||||
UniteEns,
|
||||
)
|
||||
from app.models import FormSemestre, UniteEns, ScolarAutorisationInscription
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import sco_cache, sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.codes_cursus import (
|
||||
CMP,
|
||||
@ -77,7 +72,7 @@ class DecisionSem(object):
|
||||
def __init__(
|
||||
self,
|
||||
code_etat=None,
|
||||
code_etat_ues: dict = None, # { ue_id : code }
|
||||
code_etat_ues={}, # { ue_id : code }
|
||||
new_code_prev="",
|
||||
explication="", # aide pour le jury
|
||||
formsemestre_id_utilise_pour_compenser=None, # None si code != ADC
|
||||
@ -86,7 +81,7 @@ class DecisionSem(object):
|
||||
rule_id=None, # id regle correspondante
|
||||
):
|
||||
self.code_etat = code_etat
|
||||
self.code_etat_ues = code_etat_ues or {}
|
||||
self.code_etat_ues = code_etat_ues
|
||||
self.new_code_prev = new_code_prev
|
||||
self.explication = explication
|
||||
self.formsemestre_id_utilise_pour_compenser = (
|
||||
@ -114,27 +109,20 @@ class DecisionSem(object):
|
||||
|
||||
class SituationEtudCursus:
|
||||
"Semestre dans un cursus"
|
||||
pass
|
||||
|
||||
|
||||
class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"Semestre dans un parcours"
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, nt: NotesTableCompat):
|
||||
def __init__(self, etud: dict, formsemestre_id: int, nt: NotesTableCompat):
|
||||
"""
|
||||
etud: dict filled by fill_etuds_info()
|
||||
"""
|
||||
assert formsemestre_id == nt.formsemestre.id
|
||||
self.etud = etud
|
||||
self.etudid = etud.id
|
||||
self.etudid = etud["etudid"]
|
||||
self.formsemestre_id = formsemestre_id
|
||||
self.formsemestres: list[FormSemestre] = []
|
||||
"les semestres parcourus, le plus ancien en tête"
|
||||
self.sem = sco_formsemestre.get_formsemestre(
|
||||
formsemestre_id
|
||||
) # TODO utiliser formsemestres
|
||||
self.cur_sem: FormSemestre = nt.formsemestre
|
||||
self.can_compensate: set[int] = set()
|
||||
"les formsemestre_id qui peuvent compenser le courant"
|
||||
self.sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
self.nt: NotesTableCompat = nt
|
||||
self.formation = self.nt.formsemestre.formation
|
||||
self.parcours = self.nt.parcours
|
||||
@ -142,20 +130,18 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
# pour le DUT, le dernier est toujours S4.
|
||||
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
|
||||
# (licences et autres formations en 1 seule session))
|
||||
self.semestre_non_terminal = self.cur_sem.semestre_id != self.parcours.NB_SEM
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
self.semestre_non_terminal = False
|
||||
# Liste des semestres du parcours de cet étudiant:
|
||||
self._comp_semestres()
|
||||
# Determine le semestre "precedent"
|
||||
self._search_prev()
|
||||
self.prev_formsemestre_id = self._search_prev()
|
||||
# Verifie barres
|
||||
self._comp_barres()
|
||||
# Verifie compensation
|
||||
if self.prev_formsemestre and self.cur_sem.gestion_compensation:
|
||||
self.can_compensate_with_prev = (
|
||||
self.prev_formsemestre.id in self.can_compensate
|
||||
)
|
||||
if self.prev and self.sem["gestion_compensation"]:
|
||||
self.can_compensate_with_prev = self.prev["can_compensate"]
|
||||
else:
|
||||
self.can_compensate_with_prev = False
|
||||
|
||||
@ -184,20 +170,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
if rule.conclusion[0] in self.parcours.UNUSED_CODES:
|
||||
continue
|
||||
# Saute regles REDOSEM si pas de semestres decales:
|
||||
if (not self.cur_sem.gestion_semestrielle) and rule.conclusion[
|
||||
if (not self.sem["gestion_semestrielle"]) and rule.conclusion[
|
||||
3
|
||||
] == "REDOSEM":
|
||||
continue
|
||||
if rule.match(state):
|
||||
if rule.conclusion[0] == ADC:
|
||||
# dans les regles on ne peut compenser qu'avec le PRECEDENT:
|
||||
fiduc = self.prev_formsemestre.id
|
||||
fiduc = self.prev_formsemestre_id
|
||||
assert fiduc
|
||||
else:
|
||||
fiduc = None
|
||||
# Detection d'incoherences (regles BUG)
|
||||
if rule.conclusion[5] == BUG:
|
||||
log(f"get_possible_choices: inconsistency: state={state}")
|
||||
log("get_possible_choices: inconsistency: state=%s" % str(state))
|
||||
#
|
||||
# valid_semestre = code_semestre_validant(rule.conclusion[0])
|
||||
choices.append(
|
||||
@ -217,15 +203,15 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"Phrase d'explication pour le code devenir"
|
||||
if not devenir:
|
||||
return ""
|
||||
s_idx = self.cur_sem.semestre_id # numero semestre courant
|
||||
if s_idx < 0: # formation sans semestres (eg licence)
|
||||
s = self.sem["semestre_id"] # numero semestre courant
|
||||
if s < 0: # formation sans semestres (eg licence)
|
||||
next_s = 1
|
||||
else:
|
||||
next_s = self._get_next_semestre_id()
|
||||
# log('s=%s next=%s' % (s, next_s))
|
||||
sess_abrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
SA = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if self.semestre_non_terminal and not self.all_other_validated():
|
||||
passage = f"Passe en {sess_abrv}{next_s}"
|
||||
passage = "Passe en %s%s" % (SA, next_s)
|
||||
else:
|
||||
passage = "Formation terminée"
|
||||
if devenir == NEXT:
|
||||
@ -233,23 +219,29 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
elif devenir == REO:
|
||||
return "Réorienté"
|
||||
elif devenir == REDOANNEE:
|
||||
return f"Redouble année (recommence {sess_abrv}{s_idx - 1})"
|
||||
return "Redouble année (recommence %s%s)" % (SA, (s - 1))
|
||||
elif devenir == REDOSEM:
|
||||
return f"Redouble semestre (recommence en {sess_abrv}{s_idx})"
|
||||
return "Redouble semestre (recommence en %s%s)" % (SA, s)
|
||||
elif devenir == RA_OR_NEXT:
|
||||
return passage + ", ou redouble année (en {sess_abrv}{s_idx - 1})"
|
||||
return passage + ", ou redouble année (en %s%s)" % (SA, (s - 1))
|
||||
elif devenir == RA_OR_RS:
|
||||
return f"""Redouble semestre {sess_abrv}{s_idx}, ou redouble année (en {
|
||||
sess_abrv}{s_idx - 1})"""
|
||||
return "Redouble semestre %s%s, ou redouble année (en %s%s)" % (
|
||||
SA,
|
||||
s,
|
||||
SA,
|
||||
s - 1,
|
||||
)
|
||||
elif devenir == RS_OR_NEXT:
|
||||
return f"{passage}, ou semestre {sess_abrv}{s_idx}"
|
||||
return passage + ", ou semestre %s%s" % (SA, s)
|
||||
elif devenir == NEXT_OR_NEXT2:
|
||||
# coherent avec get_next_semestre_ids
|
||||
return f"{passage}, ou en semestre {sess_abrv}{s_idx + 2}"
|
||||
return passage + ", ou en semestre %s%s" % (
|
||||
SA,
|
||||
s + 2,
|
||||
) # coherent avec get_next_semestre_ids
|
||||
elif devenir == NEXT2:
|
||||
return f"Passe en {sess_abrv}{s_idx + 2}"
|
||||
return "Passe en %s%s" % (SA, s + 2)
|
||||
else:
|
||||
log(f"explique_devenir: code devenir inconnu: {devenir}")
|
||||
log("explique_devenir: code devenir inconnu: %s" % devenir)
|
||||
return "Code devenir inconnu !"
|
||||
|
||||
def all_other_validated(self):
|
||||
@ -266,7 +258,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
|
||||
def _sems_validated(self, exclude_current=False):
|
||||
"True si semestres du parcours validés"
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
# mono-semestre: juste celui ci
|
||||
decision = self.nt.get_etud_decision_sem(self.etudid)
|
||||
return decision and code_semestre_validant(decision["code"])
|
||||
@ -274,8 +266,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
to_validate = set(
|
||||
range(1, self.parcours.NB_SEM + 1)
|
||||
) # ensemble des indices à valider
|
||||
if exclude_current and self.cur_sem.semestre_id in to_validate:
|
||||
to_validate.remove(self.cur_sem.semestre_id)
|
||||
if exclude_current and self.sem["semestre_id"] in to_validate:
|
||||
to_validate.remove(self.sem["semestre_id"])
|
||||
return self._sem_list_validated(to_validate)
|
||||
|
||||
def can_jump_to_next2(self):
|
||||
@ -283,20 +275,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
Il faut donc que tous les semestres 1...n-1 soient validés et que n+1 soit en attente.
|
||||
(et que le sem courant n soit validé, ce qui n'est pas testé ici)
|
||||
"""
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if not self.cur_sem.gestion_semestrielle:
|
||||
n = self.sem["semestre_id"]
|
||||
if not self.sem["gestion_semestrielle"]:
|
||||
return False # pas de semestre décalés
|
||||
if s_idx == NO_SEMESTRE_ID or s_idx > self.parcours.NB_SEM - 2:
|
||||
if n == NO_SEMESTRE_ID or n > self.parcours.NB_SEM - 2:
|
||||
return False # n+2 en dehors du parcours
|
||||
if self._sem_list_validated(set(range(1, s_idx))):
|
||||
# antérieurs validés, teste suivant
|
||||
n1 = s_idx + 1
|
||||
for formsemestre in self.formsemestres:
|
||||
if self._sem_list_validated(set(range(1, n))):
|
||||
# antérieurs validé, teste suivant
|
||||
n1 = n + 1
|
||||
for sem in self.get_semestres():
|
||||
if (
|
||||
formsemestre.semestre_id == n1
|
||||
and formsemestre.formation.formation_code
|
||||
== self.formation.formation_code
|
||||
sem["semestre_id"] == n1
|
||||
and sem["formation_code"] == self.formation.formation_code
|
||||
):
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre
|
||||
)
|
||||
@ -323,17 +315,19 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
return not sem_idx_set
|
||||
|
||||
def _comp_semestres(self):
|
||||
# plus ancien en tête:
|
||||
self.formsemestres = self.etud.get_formsemestres(recent_first=False)
|
||||
|
||||
# etud['sems'] est trie par date decroissante (voir fill_etuds_info)
|
||||
if not "sems" in self.etud:
|
||||
self.etud["sems"] = sco_etud.etud_inscriptions_infos(
|
||||
self.etud["etudid"], self.etud["ne"]
|
||||
)["sems"]
|
||||
sems = self.etud["sems"][:] # copy
|
||||
sems.reverse()
|
||||
# Nb max d'UE et acronymes
|
||||
ue_acros = {} # acronyme ue : 1
|
||||
nb_max_ue = 0
|
||||
sems = []
|
||||
for formsemestre in self.formsemestres: # plus ancien en tête
|
||||
for sem in sems:
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
sem = formsemestre.to_dict()
|
||||
sems.append(sem)
|
||||
ues = nt.get_ues_stat_dict(filter_sport=True)
|
||||
for ue in ues:
|
||||
ue_acros[ue["acronyme"]] = 1
|
||||
@ -344,48 +338,41 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
sem["formation_code"] = formsemestre.formation.formation_code
|
||||
# si sem peut servir à compenser le semestre courant, positionne
|
||||
# can_compensate
|
||||
if self.check_compensation_dut(sem, nt):
|
||||
self.can_compensate.add(formsemestre.id)
|
||||
sem["can_compensate"] = self.check_compensation_dut(sem, nt)
|
||||
|
||||
self.ue_acros = list(ue_acros.keys())
|
||||
self.ue_acros.sort()
|
||||
self.nb_max_ue = nb_max_ue
|
||||
self.sems = sems
|
||||
|
||||
def get_semestres(self) -> list[dict]:
|
||||
def get_semestres(self):
|
||||
"""Liste des semestres dans lesquels a été inscrit
|
||||
l'étudiant (quelle que soit la formation), le plus ancien en tête"""
|
||||
return self.sems
|
||||
|
||||
def get_cursus_descr(self, filter_futur=False, filter_formation_code=False) -> str:
|
||||
def get_cursus_descr(self, filter_futur=False, filter_formation_code=False):
|
||||
"""Description brève du parcours: "S1, S2, ..."
|
||||
Si filter_futur, ne mentionne pas les semestres qui sont après le semestre courant.
|
||||
Si filter_formation_code, restreint aux semestres de même code formation que le courant.
|
||||
"""
|
||||
cur_begin_date = self.cur_sem.date_debut
|
||||
cur_formation_code = self.cur_sem.formation.formation_code
|
||||
cur_begin_date = self.sem["dateord"]
|
||||
cur_formation_code = self.sem["formation_code"]
|
||||
p = []
|
||||
for formsemestre in self.formsemestres:
|
||||
inscription = formsemestre.etuds_inscriptions.get(self.etud.id)
|
||||
if inscription is None:
|
||||
return "non inscrit" # !!!
|
||||
if inscription.etat == scu.DEMISSION:
|
||||
for s in self.sems:
|
||||
if s["ins"]["etat"] == scu.DEMISSION:
|
||||
dem = " (dem.)"
|
||||
else:
|
||||
dem = ""
|
||||
if filter_futur and formsemestre.date_debut > cur_begin_date:
|
||||
if filter_futur and s["dateord"] > cur_begin_date:
|
||||
continue # skip semestres demarrant apres le courant
|
||||
if (
|
||||
filter_formation_code
|
||||
and formsemestre.formation.formation_code != cur_formation_code
|
||||
):
|
||||
if filter_formation_code and s["formation_code"] != cur_formation_code:
|
||||
continue # restreint aux semestres de la formation courante (pour les PV)
|
||||
session_abbrv = self.parcours.SESSION_ABBRV # 'S' ou 'A'
|
||||
if formsemestre.semestre_id < 0:
|
||||
if s["semestre_id"] < 0:
|
||||
session_abbrv = "A" # force, cas des DUT annuels par exemple
|
||||
p.append("%s%d%s" % (session_abbrv, -formsemestre.semestre_id, dem))
|
||||
p.append("%s%d%s" % (session_abbrv, -s["semestre_id"], dem))
|
||||
else:
|
||||
p.append("%s%d%s" % (session_abbrv, formsemestre.semestre_id, dem))
|
||||
p.append("%s%d%s" % (session_abbrv, s["semestre_id"], dem))
|
||||
return ", ".join(p)
|
||||
|
||||
def get_parcours_decisions(self):
|
||||
@ -394,7 +381,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
Returns: { semestre_id : code }
|
||||
"""
|
||||
r = {}
|
||||
if self.cur_sem.semestre_id == NO_SEMESTRE_ID:
|
||||
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
|
||||
indices = [NO_SEMESTRE_ID]
|
||||
else:
|
||||
indices = list(range(1, self.parcours.NB_SEM + 1))
|
||||
@ -437,83 +424,83 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"true si ce semestre pourrait etre compensé par un autre (e.g. barres UE > 8)"
|
||||
return self.barres_ue_ok
|
||||
|
||||
def _search_prev(self) -> FormSemestre | None:
|
||||
def _search_prev(self):
|
||||
"""Recherche semestre 'precedent'.
|
||||
positionne .prev_decision
|
||||
return prev_formsemestre_id
|
||||
"""
|
||||
self.prev_formsemestre = None
|
||||
self.prev = None
|
||||
self.prev_decision = None
|
||||
if len(self.formsemestres) < 2:
|
||||
if len(self.sems) < 2:
|
||||
return None
|
||||
# Cherche sem courant dans la liste triee par date_debut
|
||||
cur = None
|
||||
icur = -1
|
||||
for cur in self.formsemestres:
|
||||
for cur in self.sems:
|
||||
icur += 1
|
||||
if cur.id == self.formsemestre_id:
|
||||
if cur["formsemestre_id"] == self.formsemestre_id:
|
||||
break
|
||||
if not cur or cur.id != self.formsemestre_id:
|
||||
if not cur or cur["formsemestre_id"] != self.formsemestre_id:
|
||||
log(
|
||||
f"""*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={
|
||||
self.formsemestre_id}, etudid={self.etudid})"""
|
||||
f"*** SituationEtudCursus: search_prev: cur not found (formsemestre_id={self.formsemestre_id}, etudid={self.etudid})"
|
||||
)
|
||||
return None # pas de semestre courant !!!
|
||||
# Cherche semestre antérieur de même formation (code) et semestre_id precedent
|
||||
#
|
||||
# i = icur - 1 # part du courant, remonte vers le passé
|
||||
i = len(self.formsemestres) - 1 # par du dernier, remonte vers le passé
|
||||
prev_formsemestre = None
|
||||
i = len(self.sems) - 1 # par du dernier, remonte vers le passé
|
||||
prev = None
|
||||
while i >= 0:
|
||||
if (
|
||||
self.formsemestres[i].formation.formation_code
|
||||
== self.formation.formation_code
|
||||
and self.formsemestres[i].semestre_id == cur.semestre_id - 1
|
||||
self.sems[i]["formation_code"] == self.formation.formation_code
|
||||
and self.sems[i]["semestre_id"] == cur["semestre_id"] - 1
|
||||
):
|
||||
prev_formsemestre = self.formsemestres[i]
|
||||
prev = self.sems[i]
|
||||
break
|
||||
i -= 1
|
||||
if not prev_formsemestre:
|
||||
if not prev:
|
||||
return None # pas de precedent trouvé
|
||||
self.prev_formsemestre = prev_formsemestre
|
||||
self.prev = prev
|
||||
# Verifications basiques:
|
||||
# ?
|
||||
# Code etat du semestre precedent:
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_formsemestre)
|
||||
formsemestre = FormSemestre.query.get_or_404(prev["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
self.prev_decision = nt.get_etud_decision_sem(self.etudid)
|
||||
self.prev_moy_gen = nt.get_etud_moy_gen(self.etudid)
|
||||
self.prev_barres_ue_ok = nt.etud_check_conditions_ues(self.etudid)[0]
|
||||
return self.prev["formsemestre_id"]
|
||||
|
||||
def get_next_semestre_ids(self, devenir: str) -> list[int]:
|
||||
def get_next_semestre_ids(self, devenir):
|
||||
"""Liste des numeros de semestres autorises avec ce devenir
|
||||
Ne vérifie pas que le devenir est possible (doit être fait avant),
|
||||
juste que le rang du semestre est dans le parcours [1..NB_SEM]
|
||||
"""
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
s = self.sem["semestre_id"]
|
||||
if devenir == NEXT:
|
||||
ids = [self._get_next_semestre_id()]
|
||||
elif devenir == REDOANNEE:
|
||||
ids = [s_idx - 1]
|
||||
ids = [s - 1]
|
||||
elif devenir == REDOSEM:
|
||||
ids = [s_idx]
|
||||
ids = [s]
|
||||
elif devenir == RA_OR_NEXT:
|
||||
ids = [s_idx - 1, self._get_next_semestre_id()]
|
||||
ids = [s - 1, self._get_next_semestre_id()]
|
||||
elif devenir == RA_OR_RS:
|
||||
ids = [s_idx - 1, s_idx]
|
||||
ids = [s - 1, s]
|
||||
elif devenir == RS_OR_NEXT:
|
||||
ids = [s_idx, self._get_next_semestre_id()]
|
||||
ids = [s, self._get_next_semestre_id()]
|
||||
elif devenir == NEXT_OR_NEXT2:
|
||||
ids = [
|
||||
self._get_next_semestre_id(),
|
||||
s_idx + 2,
|
||||
s + 2,
|
||||
] # cohérent avec explique_devenir()
|
||||
elif devenir == NEXT2:
|
||||
ids = [s_idx + 2]
|
||||
ids = [s + 2]
|
||||
else:
|
||||
ids = [] # reoriente ou autre: pas de next !
|
||||
# clip [1..NB_SEM]
|
||||
r = []
|
||||
for idx in ids:
|
||||
if 0 < idx <= self.parcours.NB_SEM:
|
||||
if idx > 0 and idx <= self.parcours.NB_SEM:
|
||||
r.append(idx)
|
||||
return r
|
||||
|
||||
@ -521,27 +508,27 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
"""Indice du semestre suivant non validé.
|
||||
S'il n'y en a pas, ramène NB_SEM+1
|
||||
"""
|
||||
s_idx = self.cur_sem.semestre_id
|
||||
if s_idx >= self.parcours.NB_SEM:
|
||||
s = self.sem["semestre_id"]
|
||||
if s >= self.parcours.NB_SEM:
|
||||
return self.parcours.NB_SEM + 1
|
||||
validated = True
|
||||
while validated and (s_idx < self.parcours.NB_SEM):
|
||||
s_idx = s_idx + 1
|
||||
while validated and (s < self.parcours.NB_SEM):
|
||||
s = s + 1
|
||||
# semestre s validé ?
|
||||
validated = False
|
||||
for formsemestre in self.formsemestres:
|
||||
for sem in self.sems:
|
||||
if (
|
||||
formsemestre.formation.formation_code
|
||||
== self.formation.formation_code
|
||||
and formsemestre.semestre_id == s_idx
|
||||
sem["formation_code"] == self.formation.formation_code
|
||||
and sem["semestre_id"] == s
|
||||
):
|
||||
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(
|
||||
formsemestre
|
||||
)
|
||||
decision = nt.get_etud_decision_sem(self.etudid)
|
||||
if decision and code_semestre_validant(decision["code"]):
|
||||
validated = True
|
||||
return s_idx
|
||||
return s
|
||||
|
||||
def valide_decision(self, decision):
|
||||
"""Enregistre la decision (instance de DecisionSem)
|
||||
@ -556,11 +543,8 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
fsid = decision.formsemestre_id_utilise_pour_compenser
|
||||
if fsid:
|
||||
ok = False
|
||||
for formsemestre in self.formsemestres:
|
||||
if (
|
||||
formsemestre.id == fsid
|
||||
and formsemestre.id in self.can_compensate
|
||||
):
|
||||
for sem in self.sems:
|
||||
if sem["formsemestre_id"] == fsid and sem["can_compensate"]:
|
||||
ok = True
|
||||
break
|
||||
if not ok:
|
||||
@ -585,11 +569,13 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
decision.assiduite,
|
||||
decision.formsemestre_id_utilise_pour_compenser,
|
||||
)
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="validate_sem",
|
||||
etudid=self.etudid,
|
||||
commit=False,
|
||||
msg=f"formsemestre_id={self.formsemestre_id} code={decision.code_etat}",
|
||||
msg="formsemestre_id=%s code=%s"
|
||||
% (self.formsemestre_id, decision.code_etat),
|
||||
)
|
||||
# -- decisions UEs
|
||||
formsemestre_validate_ues(
|
||||
@ -599,7 +585,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
decision.assiduite,
|
||||
)
|
||||
# -- modification du code du semestre precedent
|
||||
if self.prev_formsemestre and decision.new_code_prev:
|
||||
if self.prev and decision.new_code_prev:
|
||||
if decision.new_code_prev == ADC:
|
||||
# ne compense le prec. qu'avec le sem. courant
|
||||
fsid = self.formsemestre_id
|
||||
@ -607,29 +593,30 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
fsid = None
|
||||
to_invalidate += formsemestre_update_validation_sem(
|
||||
cnx,
|
||||
self.prev_formsemestre.id,
|
||||
self.prev["formsemestre_id"],
|
||||
self.etudid,
|
||||
decision.new_code_prev,
|
||||
assidu=True,
|
||||
formsemestre_id_utilise_pour_compenser=fsid,
|
||||
)
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="validate_sem",
|
||||
etudid=self.etudid,
|
||||
commit=False,
|
||||
msg=f"formsemestre_id={self.prev_formsemestre.id} code={decision.new_code_prev}",
|
||||
msg="formsemestre_id=%s code=%s"
|
||||
% (self.prev["formsemestre_id"], decision.new_code_prev),
|
||||
)
|
||||
# modifs des codes d'UE (pourraient passer de ADM a CMP, meme sans modif des notes)
|
||||
formsemestre_validate_ues(
|
||||
self.prev_formsemestre.id,
|
||||
self.prev["formsemestre_id"],
|
||||
self.etudid,
|
||||
decision.new_code_prev,
|
||||
decision.assiduite, # attention: en toute rigueur il faudrait utiliser
|
||||
# une indication de l'assiduite au sem. precedent, que nous n'avons pas...
|
||||
decision.assiduite, # attention: en toute rigueur il faudrait utiliser une indication de l'assiduite au sem. precedent, que nous n'avons pas...
|
||||
)
|
||||
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=self.prev_formsemestre.id
|
||||
formsemestre_id=self.prev["formsemestre_id"]
|
||||
) # > modif decisions jury (sem, UE)
|
||||
|
||||
try:
|
||||
@ -711,7 +698,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
|
||||
class SituationEtudCursusECTS(SituationEtudCursusClassic):
|
||||
"""Gestion parcours basés sur ECTS"""
|
||||
|
||||
def __init__(self, etud: Identite, formsemestre_id: int, nt):
|
||||
def __init__(self, etud, formsemestre_id, nt):
|
||||
SituationEtudCursusClassic.__init__(self, etud, formsemestre_id, nt)
|
||||
|
||||
def could_be_compensated(self):
|
||||
@ -755,6 +742,14 @@ class SituationEtudCursusECTS(SituationEtudCursusClassic):
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def int_or_null(s):
|
||||
if s == "":
|
||||
return None
|
||||
else:
|
||||
return int(s)
|
||||
|
||||
|
||||
_scolar_formsemestre_validation_editor = ndb.EditableTable(
|
||||
"scolar_formsemestre_validation",
|
||||
"formsemestre_validation_id",
|
||||
@ -925,13 +920,13 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
|
||||
cnx, nt, formsemestre_id, etudid, ue_id, code_ue
|
||||
)
|
||||
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="validate_ue",
|
||||
etudid=etudid,
|
||||
msg=f"ue_id={ue_id} code={code_ue}",
|
||||
msg="ue_id=%s code=%s" % (ue_id, code_ue),
|
||||
commit=False,
|
||||
)
|
||||
db.session.commit()
|
||||
cnx.commit()
|
||||
|
||||
|
||||
|
@ -34,10 +34,11 @@ from flask import url_for, g, request
|
||||
from app import log
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Scolog
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc import safehtml
|
||||
from app.scodoc import html_sco_header
|
||||
@ -76,6 +77,7 @@ def report_debouche_date(start_year=None, fmt="html"):
|
||||
tab.base_url = f"{request.base_url}?start_year={start_year}"
|
||||
return tab.make_page(
|
||||
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
||||
init_qtip=True,
|
||||
javascripts=["js/etud_info.js"],
|
||||
fmt=fmt,
|
||||
with_html_headers=True,
|
||||
@ -289,8 +291,8 @@ def itemsuivi_suppress(itemsuivi_id):
|
||||
item = itemsuivi_get(cnx, itemsuivi_id, ignore_errors=True)
|
||||
if item:
|
||||
_itemsuivi_delete(cnx, itemsuivi_id)
|
||||
Scolog.logdb(method="itemsuivi_suppress", etudid=item["etudid"], commit=True)
|
||||
log(f"suppressed itemsuivi {itemsuivi_id}")
|
||||
logdb(cnx, method="itemsuivi_suppress", etudid=item["etudid"])
|
||||
log("suppressed itemsuivi %s" % (itemsuivi_id,))
|
||||
return ("", 204)
|
||||
|
||||
|
||||
@ -302,7 +304,7 @@ def itemsuivi_create(etudid, item_date=None, situation="", fmt=None):
|
||||
itemsuivi_id = _itemsuivi_create(
|
||||
cnx, args={"etudid": etudid, "item_date": item_date, "situation": situation}
|
||||
)
|
||||
Scolog.logdb(method="itemsuivi_create", etudid=etudid, commit=True)
|
||||
logdb(cnx, method="itemsuivi_create", etudid=etudid)
|
||||
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
|
||||
item = itemsuivi_get(cnx, itemsuivi_id)
|
||||
if fmt == "json":
|
||||
|
@ -137,7 +137,6 @@ def _convert_formsemestres_to_dicts(
|
||||
"bul_hide_xml": formsemestre.bul_hide_xml,
|
||||
"dateord": formsemestre.date_debut,
|
||||
"elt_annee_apo": formsemestre.elt_annee_apo,
|
||||
"elt_passage_apo": formsemestre.elt_passage_apo,
|
||||
"elt_sem_apo": formsemestre.elt_sem_apo,
|
||||
"etapes_apo_str": formsemestre.etapes_apo_str(),
|
||||
"formation": f"{formation.acronyme} v{formation.version}",
|
||||
@ -190,7 +189,6 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||
"formation",
|
||||
"etapes_apo_str",
|
||||
"elt_annee_apo",
|
||||
"elt_passage_apo",
|
||||
"elt_sem_apo",
|
||||
]
|
||||
if showcodes:
|
||||
@ -205,18 +203,9 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||
html_class=html_class,
|
||||
html_sortable=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-elt_annee_apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-elt_sem_apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-elt_passage_apo_save_url="{
|
||||
url_for('apiweb.formsemestre_set_elt_passage_apo', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-apo_save_url="{url_for('notes.formsemestre_set_apo_etapes', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_annee_apo_save_url="{url_for('notes.formsemestre_set_elt_annee_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-elt_sem_apo_save_url="{url_for('notes.formsemestre_set_elt_sem_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
@ -232,7 +221,6 @@ def _sem_table_gt(formsemestres: Query, showcodes=False, fmt="html") -> GenTable
|
||||
"etapes_apo_str": "Étape Apo.",
|
||||
"elt_annee_apo": "Elt. année Apo.",
|
||||
"elt_sem_apo": "Elt. sem. Apo.",
|
||||
"elt_passage_apo": "Elt. pass. Apo.",
|
||||
"formation": "Formation",
|
||||
},
|
||||
table_id="semlist",
|
||||
@ -294,9 +282,6 @@ def _style_sems(sems: list[dict], fmt="html") -> list[dict]:
|
||||
sem["_elt_sem_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_sem_apo']}" """
|
||||
)
|
||||
sem["_elt_passage_apo_td_attrs"] = (
|
||||
f""" data-oid="{sem['formsemestre_id']}" data-value="{sem['elt_passage_apo']}" """
|
||||
)
|
||||
return sems
|
||||
|
||||
|
||||
|
@ -47,6 +47,7 @@ from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
|
||||
def formation_delete(formation_id=None, dialog_confirmed=False):
|
||||
@ -186,7 +187,7 @@ def formation_edit(formation_id=None, create=False):
|
||||
"acronyme",
|
||||
{
|
||||
"size": 12,
|
||||
"explanation": "identifiant de la formation (par ex. BUT R&T)",
|
||||
"explanation": "identifiant de la formation (par ex. DUT R&T)",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
@ -194,7 +195,7 @@ def formation_edit(formation_id=None, create=False):
|
||||
"titre",
|
||||
{
|
||||
"size": 80,
|
||||
"explanation": "nom de la formation (ex: BUT Réseaux et Télécommunications)",
|
||||
"explanation": "nom complet de la formation (ex: DUT Réseaux et Télécommunications",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
|
@ -691,13 +691,9 @@ def module_edit(
|
||||
str(parcour.id) for parcour in ref_comp.parcours
|
||||
]
|
||||
+ ["-1"],
|
||||
"explanation": """Parcours dans lesquels est utilisé ce module (inutile
|
||||
hors BUT, pour les modules standards et dans les UEs de bonus).
|
||||
<br>
|
||||
Attention: si le module ne doit pas avoir les mêmes coefficients suivant
|
||||
le parcours, il faut en créer plusieurs versions, car dans ScoDoc chaque
|
||||
module a ses coefficients.
|
||||
""",
|
||||
"explanation": """Parcours dans lesquels est utilisé ce module.<br>
|
||||
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours,
|
||||
il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""",
|
||||
},
|
||||
)
|
||||
]
|
||||
@ -894,6 +890,23 @@ def module_edit(
|
||||
)
|
||||
|
||||
|
||||
# Edition en ligne du code Apogee
|
||||
def edit_module_set_code_apogee(id=None, value=None):
|
||||
"Set UE code apogee"
|
||||
module_id = id
|
||||
value = str(value).strip("-_ \t")
|
||||
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
|
||||
|
||||
modules = module_list(args={"module_id": module_id})
|
||||
if not modules:
|
||||
return "module invalide" # should not occur
|
||||
|
||||
do_module_edit({"module_id": module_id, "code_apogee": value})
|
||||
if not value:
|
||||
value = scu.APO_MISSING_CODE_STR
|
||||
return value
|
||||
|
||||
|
||||
def module_table(formation_id):
|
||||
"""Liste des modules de la formation
|
||||
(XXX inutile ou a revoir)
|
||||
|
@ -84,7 +84,6 @@ _ueEditor = ndb.EditableTable(
|
||||
"ects",
|
||||
"is_external",
|
||||
"code_apogee",
|
||||
"code_apogee_rcue",
|
||||
"coefficient",
|
||||
"coef_rcue",
|
||||
"color",
|
||||
@ -426,20 +425,6 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
"max_length": APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
]
|
||||
if is_apc:
|
||||
form_descr += [
|
||||
(
|
||||
"code_apogee_rcue",
|
||||
{
|
||||
"title": "Code Apogée du RCUE",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code(s) élément pédagogique Apogée du RCUE",
|
||||
"max_length": APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
]
|
||||
form_descr += [
|
||||
(
|
||||
"is_external",
|
||||
{
|
||||
@ -528,7 +513,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
|
||||
{ue_parcours_div}
|
||||
{modules_div}
|
||||
|
||||
<div id="bonus_description" class="scobox"></div>
|
||||
<div id="bonus_description"></div>
|
||||
<div id="ue_list_code" class="sco_box sco_green_bg"></div>
|
||||
|
||||
{html_sco_header.sco_footer()}
|
||||
@ -1056,10 +1041,10 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
|
||||
if current_user.has_permission(Permission.EditFormSemestre):
|
||||
H.append(
|
||||
f"""<ul>
|
||||
<li><b><a class="stdlink" href="{
|
||||
<li><a class="stdlink" href="{
|
||||
url_for('notes.formsemestre_createwithmodules', scodoc_dept=g.scodoc_dept,
|
||||
formation_id=formation_id, semestre_id=1)
|
||||
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a></b>
|
||||
}">Mettre en place un nouveau semestre de formation {formation.acronyme}</a>
|
||||
</li>
|
||||
</ul>"""
|
||||
)
|
||||
@ -1124,18 +1109,12 @@ def _ue_table_ues(
|
||||
klass = "span_apo_edit"
|
||||
else:
|
||||
klass = ""
|
||||
edit_url = url_for(
|
||||
"apiweb.ue_set_code_apogee",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=ue["ue_id"],
|
||||
ue["code_apogee_str"] = (
|
||||
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">"""
|
||||
% (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR)
|
||||
+ (ue["code_apogee"] or "")
|
||||
+ "</span>"
|
||||
)
|
||||
ue[
|
||||
"code_apogee_str"
|
||||
] = f""", Apo: <span
|
||||
class="{klass}" data-url="{edit_url}" id="{ue['ue_id']}"
|
||||
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
||||
ue["code_apogee"] or ""
|
||||
}</span>"""
|
||||
|
||||
if cur_ue_semestre_id != ue["semestre_id"]:
|
||||
cur_ue_semestre_id = ue["semestre_id"]
|
||||
@ -1369,17 +1348,16 @@ def _ue_table_modules(
|
||||
heurescoef = (
|
||||
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
|
||||
)
|
||||
edit_url = url_for(
|
||||
"apiweb.formation_module_set_code_apogee",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
module_id=mod["module_id"],
|
||||
if mod_editable:
|
||||
klass = "span_apo_edit"
|
||||
else:
|
||||
klass = ""
|
||||
heurescoef += (
|
||||
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
|
||||
% (klass, mod["module_id"], scu.APO_MISSING_CODE_STR)
|
||||
+ (mod["code_apogee"] or "")
|
||||
+ "</span>"
|
||||
)
|
||||
heurescoef += f""", Apo: <span
|
||||
class="{'span_apo_edit' if editable else ''}"
|
||||
data-url="{edit_url}" id="{mod["module_id"]}"
|
||||
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
|
||||
mod["code_apogee"] or ""
|
||||
}</span>"""
|
||||
if tag_editable:
|
||||
tag_cls = "module_tag_editor"
|
||||
else:
|
||||
@ -1515,6 +1493,28 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
||||
formation.invalidate_module_coefs()
|
||||
|
||||
|
||||
# essai edition en ligne:
|
||||
def edit_ue_set_code_apogee(id=None, value=None):
|
||||
"set UE code apogee"
|
||||
ue_id = id
|
||||
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
|
||||
|
||||
ues = ue_list(args={"ue_id": ue_id})
|
||||
if not ues:
|
||||
return "ue invalide"
|
||||
|
||||
do_ue_edit(
|
||||
{"ue_id": ue_id, "code_apogee": value},
|
||||
bypass_lock=True,
|
||||
dont_invalidate_cache=False,
|
||||
)
|
||||
if not value:
|
||||
value = scu.APO_MISSING_CODE_STR
|
||||
return value
|
||||
|
||||
|
||||
def ue_is_locked(ue_id):
|
||||
"""True if UE should not be modified
|
||||
(contains modules used in a locked formsemestre)
|
||||
|
@ -247,7 +247,9 @@ def apo_csv_check_etape(semset, set_nips, etape_apo):
|
||||
return nips_ok, apo_nips, nips_no_apo, nips_no_sco, maq_elems, sem_elems
|
||||
|
||||
|
||||
def apo_csv_semset_check(semset, allow_missing_apo=False, allow_missing_csv=False):
|
||||
def apo_csv_semset_check(
|
||||
semset, allow_missing_apo=False, allow_missing_csv=False
|
||||
): # was apo_csv_check
|
||||
"""
|
||||
check students in stored maqs vs students in semset
|
||||
Cas à détecter:
|
||||
@ -344,3 +346,120 @@ def apo_csv_retreive_etuds_by_nip(semset, nips):
|
||||
etuds[nip] = apo_etuds_by_nips.get(nip, {"nip": nip, "etape_apo": "?"})
|
||||
|
||||
return etuds
|
||||
|
||||
|
||||
"""
|
||||
Tests:
|
||||
|
||||
from debug import *
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_etape_apogee import *
|
||||
from app.scodoc.sco_apogee_csv import *
|
||||
from app.scodoc.sco_semset import *
|
||||
|
||||
app.set_sco_dept('RT')
|
||||
csv_data = open('/opt/misc/VDTRT_V1RT.TXT').read()
|
||||
annee_scolaire=2015
|
||||
sem_id=1
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=sem_id)
|
||||
print apo_data.etape_apogee
|
||||
|
||||
apo_data.setup()
|
||||
e = apo_data.etuds[0]
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco( apo_data)
|
||||
|
||||
print apo_csv_list_stored_archives()
|
||||
|
||||
|
||||
# apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
|
||||
|
||||
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos( [sco_groups.get_default_group(formsemestre_id)], formsemestre_id=formsemestre_id)
|
||||
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
#
|
||||
s = SemSet('NSS29902')
|
||||
apo_data = sco_apogee_csv.ApoData(open('/opt/scodoc/var/scodoc/archives/apo_csv/RT/2015-2/2016-07-10-11-26-15/V1RT.csv').read(), periode=1)
|
||||
|
||||
# cas Tiziri K. (inscrite en S1, démission en fin de S1, pas inscrite en S2)
|
||||
# => pas de décision, ce qui est voulu (?)
|
||||
#
|
||||
|
||||
apo_data.setup()
|
||||
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0]
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco(apo_data)
|
||||
|
||||
self=e
|
||||
col_id='apoL_c0129'
|
||||
|
||||
# --
|
||||
from app.scodoc import sco_portal_apogee
|
||||
_ = go_dept(app, 'GEA').Notes
|
||||
#csv_data = sco_portal_apogee.get_maquette_apogee(etape='V1GE', annee_scolaire=2015)
|
||||
csv_data = open('/tmp/V1GE.txt').read()
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
|
||||
|
||||
|
||||
# ------
|
||||
# les elements inconnus:
|
||||
|
||||
from debug import *
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_etape_apogee import *
|
||||
from app.scodoc.sco_apogee_csv import *
|
||||
from app.scodoc.sco_semset import *
|
||||
|
||||
_ = go_dept(app, 'RT').Notes
|
||||
csv_data = open('/opt/misc/V2RT.csv').read()
|
||||
annee_scolaire=2015
|
||||
sem_id=1
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
|
||||
print apo_data.etape_apogee
|
||||
|
||||
apo_data.setup()
|
||||
for e in apo_data.etuds:
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco(apo_data)
|
||||
|
||||
# ------
|
||||
# test export jury intermediaire
|
||||
from debug import *
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_etape_apogee import *
|
||||
from app.scodoc.sco_apogee_csv import *
|
||||
from app.scodoc.sco_semset import *
|
||||
|
||||
_ = go_dept(app, 'CJ').Notes
|
||||
csv_data = open('/opt/scodoc/var/scodoc/archives/apo_csv/CJ/2016-1/2017-03-06-21-46-32/V1CJ.csv').read()
|
||||
annee_scolaire=2016
|
||||
sem_id=1
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=1)
|
||||
print apo_data.etape_apogee
|
||||
|
||||
apo_data.setup()
|
||||
e = [ e for e in apo_data.etuds if e['nom'] == 'XYZ' ][0] #
|
||||
e.lookup_scodoc( apo_data.etape_formsemestre_ids)
|
||||
e.associate_sco(apo_data)
|
||||
|
||||
self=e
|
||||
|
||||
sco_elts = {}
|
||||
col_id='apoL_c0001'
|
||||
code = apo_data.cols[col_id]['Code'] # 'V1RT'
|
||||
|
||||
sem = apo_data.sems_periode[0] # le S1
|
||||
|
||||
"""
|
||||
|
@ -125,19 +125,14 @@ def apo_semset_maq_status(
|
||||
H.append("""<p><em>Aucune maquette chargée</em></p>""")
|
||||
# Upload fichier:
|
||||
H.append(
|
||||
f"""<form id="apo_csv_add" action="view_apo_csv_store"
|
||||
method="post" enctype="multipart/form-data"
|
||||
style="margin-bottom: 8px;"
|
||||
>
|
||||
<div style="margin-top: 12px; margin-bottom: 8px;">
|
||||
{'Charger votre fichier' if tab_archives.is_empty() else 'Ajouter un autre fichier'}
|
||||
maquette Apogée:
|
||||
</div>
|
||||
"""<form id="apo_csv_add" action="view_apo_csv_store" method="post" enctype="multipart/form-data">
|
||||
Charger votre fichier maquette Apogée:
|
||||
<input type="file" size="30" name="csvfile"/>
|
||||
<input type="hidden" name="semset_id" value="{semset_id}"/>
|
||||
<input type="hidden" name="semset_id" value="%s"/>
|
||||
<input type="submit" value="Ajouter ce fichier"/>
|
||||
<input type="checkbox" name="autodetect" checked/>autodétecter encodage</input>
|
||||
</form>"""
|
||||
% (semset_id,)
|
||||
)
|
||||
# Récupération sur portail:
|
||||
maquette_url = sco_portal_apogee.get_maquette_url()
|
||||
@ -340,7 +335,7 @@ def apo_semset_maq_status(
|
||||
missing = maq_elems - sem_elems
|
||||
H.append('<div id="apo_elements">')
|
||||
H.append(
|
||||
'<p>Élements Apogée: <span class="apo_elems">%s</span></p>'
|
||||
'<p>Elements Apogée: <span class="apo_elems">%s</span></p>'
|
||||
% ", ".join(
|
||||
[
|
||||
e if not e in missing else '<span class="missing">' + e + "</span>"
|
||||
@ -356,7 +351,7 @@ def apo_semset_maq_status(
|
||||
]
|
||||
H.append(
|
||||
f"""<div class="apo_csv_status_missing_elems">
|
||||
<span class="fontred">Élements Apogée absents dans ScoDoc: </span>
|
||||
<span class="fontred">Elements Apogée absents dans ScoDoc: </span>
|
||||
<span class="apo_elems fontred">{
|
||||
", ".join(sorted(missing))
|
||||
}</span>
|
||||
@ -447,11 +442,11 @@ def table_apo_csv_list(semset):
|
||||
annee_scolaire = semset["annee_scolaire"]
|
||||
sem_id = semset["sem_id"]
|
||||
|
||||
rows = sco_etape_apogee.apo_csv_list_stored_archives(
|
||||
T = sco_etape_apogee.apo_csv_list_stored_archives(
|
||||
annee_scolaire, sem_id, etapes=semset.list_etapes()
|
||||
)
|
||||
|
||||
for t in rows:
|
||||
for t in T:
|
||||
# Ajoute qq infos pour affichage:
|
||||
csv_data = sco_etape_apogee.apo_csv_get(t["etape_apo"], annee_scolaire, sem_id)
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
||||
@ -489,7 +484,7 @@ def table_apo_csv_list(semset):
|
||||
"date_str": "Enregistré le",
|
||||
},
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
rows=T,
|
||||
html_class="table_leftalign apo_maq_list",
|
||||
html_sortable=True,
|
||||
# base_url = '%s?formsemestre_id=%s' % (request.base_url, formsemestre_id),
|
||||
@ -545,8 +540,7 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"):
|
||||
if not isinstance(nip_list, str):
|
||||
nip_list = str(nip_list)
|
||||
nips = nip_list.split(",")
|
||||
etuds_lst = [sco_etud.get_etud_info(code_nip=nip, filled=True) for nip in nips]
|
||||
etuds = [lst[0] for lst in etuds_lst if lst]
|
||||
etuds = [sco_etud.get_etud_info(code_nip=nip, filled=True)[0] for nip in nips]
|
||||
|
||||
for e in etuds:
|
||||
tgt = url_for(
|
||||
@ -779,18 +773,12 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
||||
e["in_scodoc"] = e["nip"] not in nips_no_sco
|
||||
e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]]
|
||||
if e["in_scodoc"]:
|
||||
etud = sco_etud.get_etud_info(code_nip=e["nip"], filled=True)
|
||||
if etud:
|
||||
e.update(etud[0])
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
else:
|
||||
# race condition?
|
||||
e["in_scodoc"] = False
|
||||
e["_css_row_class"] = "apo_not_scodoc"
|
||||
e.update(sco_etud.get_etud_info(code_nip=e["nip"], filled=True)[0])
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
else:
|
||||
e["_css_row_class"] = "apo_not_scodoc"
|
||||
|
||||
|
@ -93,7 +93,7 @@ import json
|
||||
|
||||
from flask import url_for, g
|
||||
|
||||
from app.scodoc import sco_portal_apogee
|
||||
from app.scodoc.sco_portal_apogee import get_inscrits_etape
|
||||
from app import log
|
||||
from app.scodoc.sco_utils import annee_scolaire_debut
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -136,16 +136,11 @@ class DataEtudiant(object):
|
||||
self.etudid = etudid
|
||||
self.data_apogee = None
|
||||
self.data_scodoc = None
|
||||
self.etapes = set()
|
||||
"l'ensemble des étapes où il est inscrit"
|
||||
self.semestres = set()
|
||||
"l'ensemble des formsemestre_id où il est inscrit"
|
||||
self.tags = set()
|
||||
"les anomalies relevées"
|
||||
self.ind_row = "-"
|
||||
"ligne où il compte dans les effectifs"
|
||||
self.etapes = set() # l'ensemble des étapes où il est inscrit
|
||||
self.semestres = set() # l'ensemble des formsemestre_id où il est inscrit
|
||||
self.tags = set() # les anomalies relevées
|
||||
self.ind_row = "-" # là où il compte dans les effectifs (ligne et colonne)
|
||||
self.ind_col = "-"
|
||||
"colonne où il compte dans les effectifs"
|
||||
|
||||
def add_etape(self, etape):
|
||||
self.etapes.add(etape)
|
||||
@ -168,9 +163,9 @@ class DataEtudiant(object):
|
||||
def set_ind_col(self, indicatif):
|
||||
self.ind_col = indicatif
|
||||
|
||||
def get_identity(self) -> str:
|
||||
def get_identity(self):
|
||||
"""
|
||||
Calcule le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
|
||||
Calcul le nom/prénom de l'étudiant (données ScoDoc en priorité, sinon données Apogée)
|
||||
:return: L'identité calculée
|
||||
"""
|
||||
if self.data_scodoc is not None:
|
||||
@ -181,12 +176,9 @@ class DataEtudiant(object):
|
||||
|
||||
def _help() -> str:
|
||||
return """
|
||||
<div id="export_help" class="pas_help">
|
||||
<span>Explications sur les tableaux des effectifs
|
||||
et liste des étudiants</span>
|
||||
<div>
|
||||
<p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:
|
||||
</p>
|
||||
<div id="export_help" class="pas_help"> <span>Explications sur les tableaux des effectifs et liste des
|
||||
étudiants</span>
|
||||
<div> <p>Le tableau des effectifs présente le nombre d'étudiants selon deux critères:</p>
|
||||
<ul>
|
||||
<li>En colonne le statut de l'étudiant par rapport à Apogée:
|
||||
<ul>
|
||||
@ -414,8 +406,7 @@ class EtapeBilan:
|
||||
for key_etape in self.etapes:
|
||||
annee_apogee, etapestr = key_to_values(key_etape)
|
||||
self.etu_etapes[key_etape] = set()
|
||||
# get_inscrits_etape interroge portail Apo:
|
||||
for etud in sco_portal_apogee.get_inscrits_etape(etapestr, annee_apogee):
|
||||
for etud in get_inscrits_etape(etapestr, annee_apogee):
|
||||
key_etu = self.register_etud_apogee(etud, key_etape)
|
||||
self.etu_etapes[key_etape].add(key_etu)
|
||||
|
||||
@ -453,6 +444,7 @@ class EtapeBilan:
|
||||
data_etu = self.etudiants[key_etu]
|
||||
ind_col = "-"
|
||||
ind_row = "-"
|
||||
|
||||
# calcul de la colonne
|
||||
if len(data_etu.etapes) == 1:
|
||||
ind_col = self.indicatifs[list(data_etu.etapes)[0]]
|
||||
@ -486,34 +478,32 @@ class EtapeBilan:
|
||||
affichage de l'html
|
||||
:return: Le code html à afficher
|
||||
"""
|
||||
if not sco_portal_apogee.has_portal():
|
||||
return """<div id="synthese" class="semset_description">
|
||||
<em>Pas de portail Apogée configuré</em>
|
||||
</div>"""
|
||||
self.load_listes() # chargement des données
|
||||
self.dispatch() # analyse et répartition
|
||||
# calcul de la liste des colonnes et des lignes de la table des effectifs
|
||||
self.all_rows_str = "'" + ",".join(["." + r for r in self.all_rows_ind]) + "'"
|
||||
self.all_cols_str = "'" + ",".join(["." + c for c in self.all_cols_ind]) + "'"
|
||||
|
||||
return f"""
|
||||
<div id="synthese" class="semset_description">
|
||||
H = [
|
||||
"""<div id="synthese" class="semset_description">
|
||||
<details open="true">
|
||||
<summary><b>Tableau des effectifs</b>
|
||||
</summary>
|
||||
{self._diagtable()}
|
||||
</details>
|
||||
{self.display_tags()}
|
||||
<details open="true">
|
||||
<summary>
|
||||
<b id="effectifs">Liste des étudiants <span id="compte"></span></b>
|
||||
</summary>
|
||||
{entete_liste_etudiant()}
|
||||
{self.table_effectifs()}
|
||||
</details>
|
||||
{_help()}
|
||||
</div>
|
||||
"""
|
||||
<summary><b>Tableau des effectifs</b>
|
||||
</summary>
|
||||
""",
|
||||
self._diagtable(),
|
||||
"""</details>""",
|
||||
self.display_tags(),
|
||||
"""<details open="true">
|
||||
<summary><b id="effectifs">Liste des étudiants <span id="compte"></span></b>
|
||||
</summary>
|
||||
""",
|
||||
entete_liste_etudiant(),
|
||||
self.table_effectifs(),
|
||||
"""</details>""",
|
||||
_help(),
|
||||
]
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
def _inc_count(self, ind_row, ind_col):
|
||||
if (ind_row, ind_col) not in self.repartition:
|
||||
@ -702,34 +692,26 @@ class EtapeBilan:
|
||||
return "\n".join(H)
|
||||
|
||||
@staticmethod
|
||||
def link_etu(etudid, nom) -> str:
|
||||
"Lien html vers fiche de l'étudiant"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{nom}</a>"""
|
||||
def link_etu(etudid, nom):
|
||||
return '<a class="stdlink" href="%s">%s</a>' % (
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
nom,
|
||||
)
|
||||
|
||||
def link_semestre(self, semestre, short=False) -> str:
|
||||
"Lien html vers tableau de bord semestre"
|
||||
key = "session_id" if short else "titremois"
|
||||
sem = self.semestres[semestre]
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_status", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=sem['formsemestre_id']
|
||||
)}">{sem[key]}</a>
|
||||
"""
|
||||
def link_semestre(self, semestre, short=False):
|
||||
if short:
|
||||
return (
|
||||
'<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%('
|
||||
"formsemestre_id)s</a> " % self.semestres[semestre]
|
||||
)
|
||||
else:
|
||||
return (
|
||||
'<a class="stdlink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre_num)s'
|
||||
" %(mois_debut)s - %(mois_fin)s)</a>" % self.semestres[semestre]
|
||||
)
|
||||
|
||||
def table_effectifs(self) -> str:
|
||||
"Table html donnant les étudiants dans chaque semestre"
|
||||
H = [
|
||||
"""
|
||||
<style>
|
||||
table#apo-detail td.semestre {
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
]
|
||||
def table_effectifs(self):
|
||||
H = []
|
||||
|
||||
col_ids = ["tag", "etudiant", "prenom", "nip", "semestre", "apogee", "annee"]
|
||||
titles = {
|
||||
@ -784,7 +766,6 @@ class EtapeBilan:
|
||||
titles,
|
||||
html_class="table_leftalign",
|
||||
html_sortable=True,
|
||||
html_with_td_classes=True,
|
||||
table_id="apo-detail",
|
||||
).gen(fmt="html")
|
||||
)
|
||||
|
@ -37,7 +37,7 @@ from flask import url_for, g
|
||||
|
||||
from app import db, email
|
||||
from app import log
|
||||
from app.models import Admission, Identite, Scolog
|
||||
from app.models import Admission, Identite
|
||||
from app.models.etudiants import (
|
||||
check_etud_duplicate_code,
|
||||
input_civilite,
|
||||
@ -53,9 +53,10 @@ from app.scodoc.sco_utils import (
|
||||
format_prenom,
|
||||
)
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
from app.scodoc import safehtml
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.scolog import logdb
|
||||
|
||||
|
||||
def format_etud_ident(etud: dict):
|
||||
@ -510,7 +511,8 @@ def create_etud(cnx, args: dict = None):
|
||||
etudid = etud.id
|
||||
|
||||
# log
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="etudident_edit_form",
|
||||
etudid=etudid,
|
||||
msg="creation initiale",
|
||||
@ -679,6 +681,17 @@ o.close()
|
||||
"""
|
||||
|
||||
|
||||
def list_scolog(etudid):
|
||||
"liste des operations effectuees sur cet etudiant"
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"SELECT * FROM scolog WHERE etudid=%(etudid)s ORDER BY DATE DESC",
|
||||
{"etudid": etudid},
|
||||
)
|
||||
return cursor.dictfetchall()
|
||||
|
||||
|
||||
def fill_etuds_info(etuds: list[dict], add_admission=True):
|
||||
"""etuds est une liste d'etudiants (mappings)
|
||||
Pour chaque etudiant, ajoute ou formatte les champs
|
||||
|
@ -256,7 +256,7 @@ def evaluation_create_form(
|
||||
+ (
|
||||
"(pondéré par poids et ajouté aux moyennes de ce module)"
|
||||
if is_apc
|
||||
else "(ajouté à la moyenne de ce module, différent du bonus sport !)"
|
||||
else "(ajouté à la moyenne de ce module)"
|
||||
)
|
||||
),
|
||||
),
|
||||
|
@ -64,24 +64,18 @@ import sco_version
|
||||
# --------------------------------------------------------------------
|
||||
def notes_moyenne_median_mini_maxi(notes):
|
||||
"calcule moyenne et mediane d'une liste de valeurs (floats)"
|
||||
notes_num = [
|
||||
notes = [
|
||||
x
|
||||
for x in notes
|
||||
if (x is not None) and (x != scu.NOTES_NEUTRALISE) and (x != scu.NOTES_ATTENTE)
|
||||
if (x != None) and (x != scu.NOTES_NEUTRALISE) and (x != scu.NOTES_ATTENTE)
|
||||
]
|
||||
n = len(notes_num)
|
||||
n = len(notes)
|
||||
if not n:
|
||||
# Aucune note numérique
|
||||
# si elles sont toutes du même type, renvoie ce type (ABS, EXC, ATT)
|
||||
for type_note in (scu.NOTES_NEUTRALISE, scu.NOTES_ATTENTE, None):
|
||||
if all(x == type_note for x in notes):
|
||||
return (type_note, type_note, type_note, type_note)
|
||||
# sinon renvoie "???"
|
||||
return "???", None, None, None
|
||||
moy = sum(notes_num) / n
|
||||
median = list_median(notes_num)
|
||||
mini = min(notes_num)
|
||||
maxi = max(notes_num)
|
||||
return None, None, None, None
|
||||
moy = sum(notes) / n
|
||||
median = list_median(notes)
|
||||
mini = min(notes)
|
||||
maxi = max(notes)
|
||||
return moy, median, mini, maxi
|
||||
|
||||
|
||||
|
@ -26,14 +26,13 @@
|
||||
##############################################################################
|
||||
|
||||
|
||||
"""Excel file handling"""
|
||||
|
||||
""" Excel file handling
|
||||
"""
|
||||
import datetime
|
||||
import io
|
||||
import time
|
||||
from enum import Enum
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import AnyStr
|
||||
|
||||
import openpyxl.utils.datetime
|
||||
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL, FORMAT_DATE_DDMMYY
|
||||
@ -60,12 +59,12 @@ class COLORS(Enum):
|
||||
LIGHT_YELLOW = "FFFFFF99"
|
||||
|
||||
|
||||
# Un style est enregistré comme un dictionnaire avec des attributs dans la liste suivante:
|
||||
# Un style est enregistré comme un dictionnaire qui précise la valeur d'un attribut dans la liste suivante:
|
||||
# font, border, number_format, fill,...
|
||||
# (cf https://openpyxl.readthedocs.io/en/stable/styles.html#working-with-styles)
|
||||
|
||||
|
||||
def xldate_as_datetime(xldate):
|
||||
def xldate_as_datetime(xldate, datemode=0):
|
||||
"""Conversion d'une date Excel en datetime python
|
||||
Deux formats de chaîne acceptés:
|
||||
* JJ/MM/YYYY (chaîne naïve)
|
||||
@ -187,8 +186,8 @@ def excel_make_style(
|
||||
|
||||
class ScoExcelSheet:
|
||||
"""Représente une feuille qui peut être indépendante ou intégrée dans un ScoExcelBook.
|
||||
En application des directives de la bibliothèque sur l'écriture optimisée,
|
||||
l'ordre des opérations est imposé:
|
||||
En application des directives de la bibliothèque sur l'écriture optimisée, l'ordre des opérations
|
||||
est imposé:
|
||||
* instructions globales (largeur/maquage des colonnes et ligne, ...)
|
||||
* construction et ajout des cellules et ligne selon le sens de lecture (occidental)
|
||||
ligne de haut en bas et cellules de gauche à droite (i.e. A1, A2, .. B1, B2, ..)
|
||||
@ -199,7 +198,7 @@ class ScoExcelSheet:
|
||||
"""Création de la feuille. sheet_name
|
||||
-- le nom de la feuille default_style
|
||||
-- le style par défaut des cellules ws
|
||||
-- None si la feuille est autonome (elle crée son propre wb), sinon c'est la worksheet
|
||||
-- None si la feuille est autonome (dans ce cas elle crée son propre wb), sinon c'est la worksheet
|
||||
créée par le workbook propriétaire un workbook est créé et associé à cette feuille.
|
||||
"""
|
||||
# Le nom de la feuille ne peut faire plus de 31 caractères.
|
||||
@ -228,8 +227,7 @@ class ScoExcelSheet:
|
||||
fill=None,
|
||||
number_format=None,
|
||||
font=None,
|
||||
) -> dict:
|
||||
"création d'un dict"
|
||||
):
|
||||
style = {}
|
||||
if font is not None:
|
||||
style["font"] = font
|
||||
@ -246,37 +244,27 @@ class ScoExcelSheet:
|
||||
return style
|
||||
|
||||
@staticmethod
|
||||
def i2col(idx: int | str) -> str:
|
||||
"traduit un index ou lettre de colonne en lettre de colonne"
|
||||
if isinstance(idx, str):
|
||||
return idx
|
||||
def i2col(idx):
|
||||
if idx < 26: # one letter key
|
||||
return chr(idx + 65)
|
||||
# two letters AA..ZZ
|
||||
first = (idx // 26) + 64
|
||||
second = (idx % 26) + 65
|
||||
return "" + chr(first) + chr(second)
|
||||
else: # two letters AA..ZZ
|
||||
first = (idx // 26) + 66
|
||||
second = (idx % 26) + 65
|
||||
return "" + chr(first) + chr(second)
|
||||
|
||||
def set_column_dimension_width(self, cle=None, value: int | str | list = 21):
|
||||
"""Détermine la largeur d'une colonne.
|
||||
cle -- identifie la colonne (lettre ou indice à partir de 0),
|
||||
Si cle is None, affecte toutes les colonnes.
|
||||
|
||||
value est soit la liste des largeurs de colonnes [ (0, width0), (1, width1), ...]
|
||||
soit la largeur de la colonne indiquée par cle, soit "auto".
|
||||
Largeurs en unité : 7 pixels comme affiché dans Excel)
|
||||
ou value == "auto", ajuste la largeur au contenu
|
||||
def set_column_dimension_width(self, cle=None, value=21):
|
||||
"""Détermine la largeur d'une colonne. cle -- identifie la colonne ("A" "B", ... ou 0, 1, 2, ...) si None,
|
||||
value donne la liste des largeurs de colonnes depuis A, B, C, ... value -- la dimension (unité : 7 pixels
|
||||
comme affiché dans Excel)
|
||||
"""
|
||||
if cle is None:
|
||||
cols_widths = enumerate(value)
|
||||
for i, val in enumerate(value):
|
||||
self.ws.column_dimensions[self.i2col(i)].width = val
|
||||
# No keys: value is a list of widths
|
||||
elif isinstance(cle, str): # accepts set_column_with("D", ...)
|
||||
self.ws.column_dimensions[cle].width = value
|
||||
else:
|
||||
cols_widths = [(cle, value)]
|
||||
|
||||
for idx, width in cols_widths:
|
||||
if width == "auto":
|
||||
self.adjust_column_widths(column_letter=self.i2col(idx))
|
||||
else:
|
||||
self.ws.column_dimensions[self.i2col(idx)].width = width
|
||||
self.ws.column_dimensions[self.i2col(cle)].width = value
|
||||
|
||||
def set_row_dimension_height(self, cle=None, value=21):
|
||||
"""Détermine la hauteur d'une ligne. cle -- identifie la ligne (1, 2, ...) si None,
|
||||
@ -297,58 +285,16 @@ class ScoExcelSheet:
|
||||
self.ws.row_dimensions[cle].hidden = value
|
||||
|
||||
def set_column_dimension_hidden(self, cle, value):
|
||||
"""Masque ou affiche une colonne.
|
||||
"""Masque ou affiche une ligne.
|
||||
cle -- identifie la colonne (1...)
|
||||
value -- boolean (vrai = colonne cachée)
|
||||
"""
|
||||
self.ws.column_dimensions[cle].hidden = value
|
||||
|
||||
def set_auto_filter(self, filter_range):
|
||||
"met en place un auto-filter excel: le range désigne les cellules de titres"
|
||||
self.auto_filter = filter_range
|
||||
def set_auto_filter(self, range):
|
||||
self.auto_filter = range
|
||||
|
||||
def adjust_column_widths(
|
||||
self, column_letter=None, min_row=None, max_row=None, min_col=None, max_col=None
|
||||
):
|
||||
"""Adjust columns widths to fit their content.
|
||||
If column_letter, adjust only this column, else adjust all.
|
||||
(min_row, max_row, min_col, max_col) can be used to restrinct the area to consider
|
||||
while determining the widths.
|
||||
"""
|
||||
# Create a dictionary to store the maximum width of each column
|
||||
col_widths = {}
|
||||
|
||||
if column_letter is None:
|
||||
# Iterate over each row and cell in the worksheet
|
||||
for row in self.ws.iter_rows(
|
||||
min_row=min_row, max_row=max_row, min_col=min_col, max_col=max_col
|
||||
):
|
||||
for cell in row:
|
||||
# Get the length of the cell value (converted to string)
|
||||
cell_value = str(cell.value)
|
||||
# Update the maximum width for the column
|
||||
col_widths[cell.column_letter] = max(
|
||||
col_widths.get(cell.column_letter, 0), len(cell_value)
|
||||
)
|
||||
else:
|
||||
min_row = self.ws.min_row if min_row is None else min_row
|
||||
max_row = self.ws.max_row if max_row is None else max_row
|
||||
|
||||
for row in range(min_row, max_row + 1):
|
||||
cell = self.ws[f"{column_letter}{row}"]
|
||||
cell_value = str(cell.value)
|
||||
col_widths[cell.column_letter] = max(
|
||||
col_widths.get(cell.column_letter, 0), len(cell_value)
|
||||
)
|
||||
|
||||
# Set the column widths based on the maximum length found
|
||||
# (nb: the width is expressed in characters, in the default font)
|
||||
for col, width in col_widths.items():
|
||||
self.ws.column_dimensions[col].width = width
|
||||
|
||||
def make_cell(
|
||||
self, value: any = None, style: dict = None, comment=None
|
||||
) -> WriteOnlyCell:
|
||||
def make_cell(self, value: any = None, style=None, comment=None):
|
||||
"""Construit une cellule.
|
||||
value -- contenu de la cellule (texte, numérique, booléen ou date)
|
||||
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
||||
@ -361,8 +307,9 @@ class ScoExcelSheet:
|
||||
elif value is False:
|
||||
value = 0
|
||||
elif isinstance(value, datetime.datetime):
|
||||
# make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
|
||||
value = value.replace(tzinfo=None)
|
||||
value = value.replace(
|
||||
tzinfo=None
|
||||
) # make date naive (cf https://openpyxl.readthedocs.io/en/latest/datetime.html#timezones)
|
||||
|
||||
# création de la cellule
|
||||
cell = WriteOnlyCell(self.ws, value)
|
||||
@ -394,7 +341,7 @@ class ScoExcelSheet:
|
||||
if isinstance(value, datetime.date):
|
||||
cell.data_type = "d"
|
||||
cell.number_format = FORMAT_DATE_DDMMYY
|
||||
elif isinstance(value, (int, float)):
|
||||
elif isinstance(value, int) or isinstance(value, float):
|
||||
cell.data_type = "n"
|
||||
else:
|
||||
cell.data_type = "s"
|
||||
@ -411,14 +358,13 @@ class ScoExcelSheet:
|
||||
for value, comment in zip(values, comments)
|
||||
]
|
||||
|
||||
def append_single_cell_row(self, value: any, style=None, prefix=None):
|
||||
def append_single_cell_row(self, value: any, style=None):
|
||||
"""construit une ligne composée d'une seule cellule et l'ajoute à la feuille.
|
||||
mêmes paramètres que make_cell:
|
||||
value -- contenu de la cellule (texte ou numérique)
|
||||
style -- style par défaut de la feuille si non spécifié
|
||||
prefix -- cellules ajoutées au début de la ligne
|
||||
"""
|
||||
self.append_row((prefix or []) + [self.make_cell(value, style)])
|
||||
self.append_row([self.make_cell(value, style)])
|
||||
|
||||
def append_blank_row(self):
|
||||
"""construit une ligne vide et l'ajoute à la feuille."""
|
||||
@ -433,29 +379,21 @@ class ScoExcelSheet:
|
||||
Ce flux pourra ensuite être repris dans send_excel_file (classeur mono feille)
|
||||
ou pour la génération d'un classeur multi-feuilles
|
||||
"""
|
||||
for k, v in self.column_dimensions.items():
|
||||
self.ws.column_dimensions[k] = v
|
||||
|
||||
for k, v in self.row_dimensions.items():
|
||||
self.ws.row_dimensions[k] = self.row_dimensions[v]
|
||||
for row in self.column_dimensions.keys():
|
||||
self.ws.column_dimensions[row] = self.column_dimensions[row]
|
||||
for row in self.row_dimensions.keys():
|
||||
self.ws.row_dimensions[row] = self.row_dimensions[row]
|
||||
for row in self.rows:
|
||||
self.ws.append(row)
|
||||
|
||||
def generate(self, column_widths=None) -> AnyStr:
|
||||
def generate(self):
|
||||
"""génération d'un classeur mono-feuille"""
|
||||
# this method makes sense for standalone worksheet (else call workbook.generate())
|
||||
# this method makes sense only if it is a standalone worksheet (else call workbook.generate()
|
||||
if self.wb is None: # embeded sheet
|
||||
raise ScoValueError("can't generate a single sheet from a ScoWorkbook")
|
||||
|
||||
# construction d'un flux
|
||||
# https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream
|
||||
# construction d'un flux (https://openpyxl.readthedocs.io/en/stable/tutorial.html#saving-as-a-stream)
|
||||
self.prepare()
|
||||
|
||||
# largeur des colonnes
|
||||
if column_widths:
|
||||
for k, v in column_widths.items():
|
||||
self.set_column_dimension_width(k, v)
|
||||
|
||||
if self.auto_filter is not None:
|
||||
self.ws.auto_filter.ref = self.auto_filter
|
||||
with NamedTemporaryFile() as tmp:
|
||||
@ -508,26 +446,211 @@ def excel_simple_table(
|
||||
return ws.generate()
|
||||
|
||||
|
||||
def excel_bytes_to_list(bytes_content) -> tuple[list, list[list]]:
|
||||
"Lecture d'un flux xlsx"
|
||||
def excel_feuille_saisie(evaluation: "Evaluation", titreannee, description, lines):
|
||||
"""Genere feuille excel pour saisie des notes.
|
||||
E: evaluation (dict)
|
||||
lines: liste de tuples
|
||||
(etudid, nom, prenom, etat, groupe, val, explanation)
|
||||
"""
|
||||
sheet_name = "Saisie notes"
|
||||
ws = ScoExcelSheet(sheet_name)
|
||||
|
||||
# ajuste largeurs colonnes (unite inconnue, empirique)
|
||||
ws.set_column_dimension_width("A", 11.0 / 7) # codes
|
||||
# ws.set_column_dimension_hidden("A", True) # codes
|
||||
ws.set_column_dimension_width("B", 164.00 / 7) # noms
|
||||
ws.set_column_dimension_width("C", 109.0 / 7) # prenoms
|
||||
ws.set_column_dimension_width("D", 164.0 / 7) # groupes
|
||||
ws.set_column_dimension_width("E", 115.0 / 7) # notes
|
||||
ws.set_column_dimension_width("F", 355.0 / 7) # remarques
|
||||
ws.set_column_dimension_width("G", 72.0 / 7) # colonne NIP
|
||||
ws.set_column_dimension_hidden("G", True) # colonne NIP cachée
|
||||
|
||||
# fontes
|
||||
font_base = Font(name="Arial", size=12)
|
||||
font_bold = Font(name="Arial", bold=True)
|
||||
font_italic = Font(name="Arial", size=12, italic=True, color=COLORS.RED.value)
|
||||
font_titre = Font(name="Arial", bold=True, size=14)
|
||||
font_purple = Font(name="Arial", color=COLORS.PURPLE.value)
|
||||
font_brown = Font(name="Arial", color=COLORS.BROWN.value)
|
||||
font_blue = Font(name="Arial", size=9, color=COLORS.BLUE.value)
|
||||
|
||||
# bordures
|
||||
side_thin = Side(border_style="thin", color=COLORS.BLACK.value)
|
||||
border_top = Border(top=side_thin)
|
||||
border_right = Border(right=side_thin)
|
||||
|
||||
# fonds
|
||||
fill_light_yellow = PatternFill(
|
||||
patternType="solid", fgColor=COLORS.LIGHT_YELLOW.value
|
||||
)
|
||||
|
||||
# styles
|
||||
style = {"font": font_base}
|
||||
style_titres = {"font": font_titre}
|
||||
style_expl = {"font": font_italic}
|
||||
|
||||
style_ro = { # cells read-only
|
||||
"font": font_purple,
|
||||
"border": border_right,
|
||||
}
|
||||
style_dem = {
|
||||
"font": font_brown,
|
||||
"border": border_top,
|
||||
}
|
||||
style_nom = { # style pour nom, prenom, groupe
|
||||
"font": font_base,
|
||||
"border": border_top,
|
||||
}
|
||||
style_notes = {
|
||||
"font": font_bold,
|
||||
"number_format": FORMAT_GENERAL,
|
||||
"fill": fill_light_yellow,
|
||||
"border": border_top,
|
||||
}
|
||||
style_comment = {
|
||||
"font": font_blue,
|
||||
"border": border_top,
|
||||
}
|
||||
|
||||
# filtre
|
||||
filter_top = 8
|
||||
filter_bottom = 8 + len(lines)
|
||||
filter_left = "A"
|
||||
filter_right = "G"
|
||||
ws.set_auto_filter(f"${filter_left}${filter_top}:${filter_right}${filter_bottom}")
|
||||
|
||||
# ligne de titres
|
||||
ws.append_single_cell_row(
|
||||
"Feuille saisie note (à enregistrer au format excel)", style_titres
|
||||
)
|
||||
# lignes d'instructions
|
||||
ws.append_single_cell_row(
|
||||
"Saisir les notes dans la colonne E (cases jaunes)", style_expl
|
||||
)
|
||||
ws.append_single_cell_row("Ne pas modifier les cases en mauve !", style_expl)
|
||||
# Nom du semestre
|
||||
ws.append_single_cell_row(scu.unescape_html(titreannee), style_titres)
|
||||
# description evaluation
|
||||
ws.append_single_cell_row(scu.unescape_html(description), style_titres)
|
||||
ws.append_single_cell_row(
|
||||
f"Evaluation {evaluation.descr_date()} (coef. {(evaluation.coefficient):g})",
|
||||
style,
|
||||
)
|
||||
# ligne blanche
|
||||
ws.append_blank_row()
|
||||
# code et titres colonnes
|
||||
ws.append_row(
|
||||
[
|
||||
ws.make_cell("!%s" % evaluation.id, style_ro),
|
||||
ws.make_cell("Nom", style_titres),
|
||||
ws.make_cell("Prénom", style_titres),
|
||||
ws.make_cell("Groupe", style_titres),
|
||||
ws.make_cell("Note sur %g" % (evaluation.note_max or 0.0), style_titres),
|
||||
ws.make_cell("Remarque", style_titres),
|
||||
ws.make_cell("NIP", style_titres),
|
||||
]
|
||||
)
|
||||
|
||||
# etudiants
|
||||
for line in lines:
|
||||
st = style_nom
|
||||
if line[3] != "I":
|
||||
st = style_dem
|
||||
if line[3] == "D": # demissionnaire
|
||||
s = "DEM"
|
||||
else:
|
||||
s = line[3] # etat autre
|
||||
else:
|
||||
s = line[4] # groupes TD/TP/...
|
||||
try:
|
||||
val = float(line[5])
|
||||
except ValueError:
|
||||
val = line[5]
|
||||
ws.append_row(
|
||||
[
|
||||
ws.make_cell("!" + line[0], style_ro), # code
|
||||
ws.make_cell(line[1], st),
|
||||
ws.make_cell(line[2], st),
|
||||
ws.make_cell(s, st),
|
||||
ws.make_cell(val, style_notes), # note
|
||||
ws.make_cell(line[6], style_comment), # comment
|
||||
ws.make_cell(line[7], style_ro), # NIP
|
||||
]
|
||||
)
|
||||
|
||||
# ligne blanche
|
||||
ws.append_blank_row()
|
||||
|
||||
# explication en bas
|
||||
ws.append_row([None, ws.make_cell("Code notes", style_titres)])
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("ABS", style_expl),
|
||||
ws.make_cell("absent (0)", style_expl),
|
||||
]
|
||||
)
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("EXC", style_expl),
|
||||
ws.make_cell("pas prise en compte", style_expl),
|
||||
]
|
||||
)
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("ATT", style_expl),
|
||||
ws.make_cell("en attente", style_expl),
|
||||
]
|
||||
)
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("SUPR", style_expl),
|
||||
ws.make_cell("pour supprimer note déjà entrée", style_expl),
|
||||
]
|
||||
)
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("", style_expl),
|
||||
ws.make_cell("cellule vide -> note non modifiée", style_expl),
|
||||
]
|
||||
)
|
||||
return ws.generate()
|
||||
|
||||
|
||||
def excel_bytes_to_list(bytes_content):
|
||||
try:
|
||||
filelike = io.BytesIO(bytes_content)
|
||||
return _excel_to_list(filelike)
|
||||
except Exception as exc:
|
||||
raise ScoValueError(
|
||||
"""Le fichier xlsx attendu n'est pas lisible ! (1)
|
||||
"""Le fichier xlsx attendu n'est pas lisible !
|
||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..)
|
||||
"""
|
||||
) from exc
|
||||
return _excel_to_list(filelike)
|
||||
|
||||
|
||||
def excel_file_to_list(filename):
|
||||
try:
|
||||
return _excel_to_list(filename)
|
||||
except Exception as exc:
|
||||
raise ScoValueError(
|
||||
"""Le fichier xlsx attendu n'est pas lisible !
|
||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
||||
"""
|
||||
) from exc
|
||||
|
||||
|
||||
def excel_file_to_list(filelike) -> tuple[list, list[list]]:
|
||||
"Lecture d'un flux xlsx"
|
||||
def excel_workbook_to_list(filename):
|
||||
try:
|
||||
return _excel_to_list(filelike)
|
||||
return _excel_workbook_to_list(filename)
|
||||
except Exception as exc:
|
||||
raise ScoValueError(
|
||||
"""Le fichier xlsx attendu n'est pas lisible ! (2)
|
||||
"""Le fichier xlsx attendu n'est pas lisible !
|
||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
||||
"""
|
||||
) from exc
|
||||
@ -552,7 +675,7 @@ def _open_workbook(filelike, dump_debug=False) -> Workbook:
|
||||
return workbook
|
||||
|
||||
|
||||
def _excel_to_list(filelike) -> tuple[list, list[list]]:
|
||||
def _excel_to_list(filelike):
|
||||
"""returns list of list"""
|
||||
workbook = _open_workbook(filelike)
|
||||
diag = [] # liste de chaines pour former message d'erreur
|
||||
@ -569,7 +692,7 @@ def _excel_to_list(filelike) -> tuple[list, list[list]]:
|
||||
return diag, matrix
|
||||
|
||||
|
||||
def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list[list]]:
|
||||
def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list]:
|
||||
"""read a spreadsheet sheet, and returns:
|
||||
- diag : a list of strings (error messages aimed at helping the user)
|
||||
- a list of lists: the spreadsheet cells
|
||||
@ -602,21 +725,14 @@ def _excel_sheet_to_list(sheet: Worksheet, sheet_name: str) -> tuple[list, list[
|
||||
return diag, matrix
|
||||
|
||||
|
||||
def excel_workbook_to_list(filelike):
|
||||
def _excel_workbook_to_list(filelike):
|
||||
"""Lit un classeur (workbook): chaque feuille est lue
|
||||
et est convertie en une liste de listes.
|
||||
Returns:
|
||||
- diag : a list of strings (error messages aimed at helping the user)
|
||||
- a list of lists: the spreadsheet cells
|
||||
"""
|
||||
try:
|
||||
workbook = _open_workbook(filelike)
|
||||
except Exception as exc:
|
||||
raise ScoValueError(
|
||||
"""Le fichier xlsx attendu n'est pas lisible ! (3)
|
||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
||||
"""
|
||||
) from exc
|
||||
workbook = _open_workbook(filelike)
|
||||
diag = [] # liste de chaines pour former message d'erreur
|
||||
if len(workbook.sheetnames) < 1:
|
||||
diag.append("Aucune feuille trouvée dans le classeur !")
|
||||
@ -631,7 +747,6 @@ def excel_workbook_to_list(filelike):
|
||||
return diag, matrix_list
|
||||
|
||||
|
||||
# TODO déplacer dans un autre fichier
|
||||
def excel_feuille_listeappel(
|
||||
sem,
|
||||
groupname,
|
||||
@ -640,23 +755,8 @@ def excel_feuille_listeappel(
|
||||
with_codes=False,
|
||||
with_paiement=False,
|
||||
server_name=None,
|
||||
edt_params: dict = None,
|
||||
):
|
||||
"""generation feuille appel
|
||||
|
||||
edt_params :
|
||||
- "discipline" : Discipline
|
||||
- "ens" : Enseignant
|
||||
- "date" : Date (format JJ/MM/AAAA)
|
||||
- "heure" : Heure (format HH:MM)
|
||||
"""
|
||||
# Obligatoire sinon import circulaire
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from app.scodoc.sco_groups import listgroups_abbrev, get_etud_groups
|
||||
|
||||
if edt_params is None:
|
||||
edt_params = {}
|
||||
|
||||
"""generation feuille appel"""
|
||||
if partitions is None:
|
||||
partitions = []
|
||||
formsemestre_id = sem["formsemestre_id"]
|
||||
@ -664,8 +764,8 @@ def excel_feuille_listeappel(
|
||||
|
||||
ws = ScoExcelSheet(sheet_name)
|
||||
ws.set_column_dimension_width("A", 3)
|
||||
max_name_width: int = 35
|
||||
letter_int: int = ord("B")
|
||||
ws.set_column_dimension_width("B", 35)
|
||||
ws.set_column_dimension_width("C", 12)
|
||||
|
||||
font1 = Font(name="Arial", size=11)
|
||||
font1i = Font(name="Arial", size=10, italic=True)
|
||||
@ -691,6 +791,11 @@ def excel_feuille_listeappel(
|
||||
"font": Font(name="Arial", size=14),
|
||||
}
|
||||
|
||||
style2b = {
|
||||
"font": font1i,
|
||||
"border": border_tblr,
|
||||
}
|
||||
|
||||
style2t3 = {
|
||||
"border": border_tblr,
|
||||
}
|
||||
@ -704,6 +809,8 @@ def excel_feuille_listeappel(
|
||||
"font": Font(name="Arial", bold=True, size=14),
|
||||
}
|
||||
|
||||
nb_weeks = 4 # nombre de colonnes pour remplir absences
|
||||
|
||||
# ligne 1
|
||||
title = "%s %s (%s - %s)" % (
|
||||
sco_preferences.get_preference("DeptName", formsemestre_id),
|
||||
@ -715,67 +822,35 @@ def excel_feuille_listeappel(
|
||||
ws.append_row([None, ws.make_cell(title, style2)])
|
||||
|
||||
# ligne 2
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("Discipline :", style2),
|
||||
ws.make_cell(edt_params.get("discipline", ""), style3),
|
||||
]
|
||||
)
|
||||
ws.append_row([None, ws.make_cell("Discipline :", style2)])
|
||||
|
||||
# ligne 3
|
||||
cell_2 = ws.make_cell("Enseignant :", style2)
|
||||
ws.append_row([None, cell_2, ws.make_cell(edt_params.get("ens", ""), style3)])
|
||||
cell_6 = ws.make_cell(f"Groupe {groupname}", style3)
|
||||
ws.append_row([None, cell_2, None, None, None, None, cell_6])
|
||||
|
||||
# ligne 4: Avertissement pour ne pas confondre avec listes notes + Date
|
||||
cell_1 = ws.make_cell("Date :", style2)
|
||||
# ligne 4: Avertissement pour ne pas confondre avec listes notes
|
||||
cell_2 = ws.make_cell(
|
||||
"Ne pas utiliser cette feuille pour saisir les notes !", style1i
|
||||
)
|
||||
ws.append_row([None, cell_1, ws.make_cell(edt_params.get("date", ""))])
|
||||
|
||||
# ligne 5 : Heure
|
||||
ws.append_row(
|
||||
[
|
||||
None,
|
||||
ws.make_cell("Heure :", style2),
|
||||
ws.make_cell(edt_params.get("heure", "")),
|
||||
]
|
||||
)
|
||||
|
||||
# ligne 6: groupe
|
||||
ws.append_row([None, ws.make_cell(f"Groupe {groupname}", style3)])
|
||||
ws.append_row([None, None, cell_2])
|
||||
|
||||
ws.append_blank_row()
|
||||
ws.append_row([None, cell_2])
|
||||
ws.append_blank_row()
|
||||
|
||||
# ligne 9: Entête (contruction dans une liste cells)
|
||||
# ligne 7: Entête (contruction dans une liste cells)
|
||||
cell_2 = ws.make_cell("Nom", style3)
|
||||
cells = [None, cell_2]
|
||||
letter_int += 1
|
||||
p_name: list = []
|
||||
for partition in partitions:
|
||||
p_name.append(partition["partition_name"])
|
||||
|
||||
p_name: str = " / ".join(p_name)
|
||||
ws.set_column_dimension_width(chr(letter_int), len(p_name))
|
||||
cells.append(ws.make_cell(partition["partition_name"], style3))
|
||||
if with_codes:
|
||||
cells.append(ws.make_cell("etudid", style3))
|
||||
cells.append(ws.make_cell("code_nip", style3))
|
||||
cells.append(ws.make_cell("code_ine", style3))
|
||||
|
||||
# case Groupes
|
||||
cells.append(ws.make_cell("Groupes", style3))
|
||||
letter_int += 1
|
||||
ws.set_column_dimension_width(chr(letter_int), 30)
|
||||
|
||||
# case émargement
|
||||
cells.append(ws.make_cell("Émargement", style3))
|
||||
letter_int += 1
|
||||
ws.set_column_dimension_width(chr(letter_int), 30)
|
||||
for i in range(nb_weeks):
|
||||
cells.append(ws.make_cell("", style2b))
|
||||
ws.append_row(cells)
|
||||
|
||||
row_id: int = len(ws.rows) + 1
|
||||
n = 0
|
||||
# pour chaque étudiant
|
||||
for t in lines:
|
||||
@ -783,8 +858,6 @@ def excel_feuille_listeappel(
|
||||
nomprenom = (
|
||||
t["civilite_str"] + " " + t["nom"] + " " + t["prenom"].lower().capitalize()
|
||||
)
|
||||
name_width = min(max_name_width, (len(nomprenom) + 2.0) * 1.25)
|
||||
ws.set_column_dimension_width("B", name_width)
|
||||
style_nom = style2t3
|
||||
if with_paiement:
|
||||
paie = t.get("paiementinscription", None)
|
||||
@ -797,19 +870,22 @@ def excel_feuille_listeappel(
|
||||
cell_1 = ws.make_cell(n, style1b)
|
||||
cell_2 = ws.make_cell(nomprenom, style_nom)
|
||||
cells = [cell_1, cell_2]
|
||||
group = get_etud_groups(t["etudid"], formsemestre_id=formsemestre_id)
|
||||
cells.append(ws.make_cell(listgroups_abbrev(group), style2t3))
|
||||
|
||||
for partition in partitions:
|
||||
if partition["partition_name"]:
|
||||
cells.append(
|
||||
ws.make_cell(t.get(partition["partition_id"], ""), style2t3)
|
||||
)
|
||||
if with_codes:
|
||||
cells.append(ws.make_cell(t["etudid"], style2t3))
|
||||
code_nip = t.get("code_nip", "")
|
||||
cells.append(ws.make_cell(code_nip, style2t3))
|
||||
code_ine = t.get("code_ine", "")
|
||||
cells.append(ws.make_cell(code_ine, style2t3))
|
||||
|
||||
cells.append(ws.make_cell(style=style2t3))
|
||||
cells.append(ws.make_cell(t.get("etath", ""), style2b))
|
||||
for i in range(1, nb_weeks):
|
||||
cells.append(ws.make_cell(style=style2t3))
|
||||
ws.append_row(cells)
|
||||
ws.set_row_dimension_height(row_id, 30)
|
||||
row_id += 1
|
||||
|
||||
ws.append_blank_row()
|
||||
|
||||
|
@ -61,12 +61,12 @@ class ScoValueError(ScoException):
|
||||
class ScoPermissionDenied(ScoValueError):
|
||||
"""Permission non accordée (appli web)"""
|
||||
|
||||
def __init__(self, msg=None, dest_url=None, safe=False):
|
||||
def __init__(self, msg=None, dest_url=None):
|
||||
if msg is None:
|
||||
msg = f"""Opération non autorisée pour {
|
||||
current_user.get_nomcomplet() if current_user else "?"
|
||||
}. Pas la permission, ou objet verrouillé."""
|
||||
super().__init__(msg, dest_url=dest_url, safe=safe)
|
||||
super().__init__(msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoBugCatcher(ScoException):
|
||||
@ -84,8 +84,8 @@ class InvalidEtudId(NoteProcessError):
|
||||
class ScoFormatError(ScoValueError):
|
||||
"Erreur lecture d'un fichier fourni par l'utilisateur"
|
||||
|
||||
def __init__(self, msg, filename="", dest_url=None, safe=False):
|
||||
super().__init__(msg, dest_url=dest_url, safe=safe)
|
||||
def __init__(self, msg, filename="", dest_url=None):
|
||||
super().__init__(msg, dest_url=dest_url)
|
||||
self.filename = filename
|
||||
|
||||
|
||||
@ -95,15 +95,15 @@ class ScoInvalidParamError(ScoValueError):
|
||||
(id strings, ...)
|
||||
"""
|
||||
|
||||
def __init__(self, msg=None, dest_url=None, safe=False):
|
||||
def __init__(self, msg=None, dest_url=None):
|
||||
msg = msg or "Adresse invalide. Vérifiez vos signets."
|
||||
super().__init__(msg, dest_url=dest_url, safe=safe)
|
||||
super().__init__(msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoPDFFormatError(ScoValueError):
|
||||
"erreur génération PDF (templates platypus, ...)"
|
||||
|
||||
def __init__(self, msg, dest_url=None, safe=False):
|
||||
def __init__(self, msg, dest_url=None):
|
||||
super().__init__(
|
||||
f"""Erreur dans un format pdf:
|
||||
<p>{msg}</p>
|
||||
@ -112,7 +112,6 @@ class ScoPDFFormatError(ScoValueError):
|
||||
</p>
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
safe=safe,
|
||||
)
|
||||
|
||||
|
||||
@ -131,33 +130,33 @@ class ScoConfigurationError(ScoValueError):
|
||||
class ScoLockedFormError(ScoValueError):
|
||||
"Modification d'une formation verrouillée"
|
||||
|
||||
def __init__(self, msg="", dest_url=None, safe=False):
|
||||
def __init__(self, msg="", dest_url=None):
|
||||
msg = (
|
||||
"Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
|
||||
+ str(msg)
|
||||
)
|
||||
super().__init__(msg=msg, dest_url=dest_url, safe=safe)
|
||||
super().__init__(msg=msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoLockedSemError(ScoValueError):
|
||||
"Modification d'un formsemestre verrouillé"
|
||||
|
||||
def __init__(self, msg="", dest_url=None, safe=False):
|
||||
def __init__(self, msg="", dest_url=None):
|
||||
msg = "Ce semestre est verrouillé ! " + str(msg)
|
||||
super().__init__(msg=msg, dest_url=dest_url, safe=safe)
|
||||
super().__init__(msg=msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoNonEmptyFormationObject(ScoValueError):
|
||||
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
|
||||
|
||||
def __init__(self, type_objet="objet'", msg="", dest_url=None, safe=False):
|
||||
def __init__(self, type_objet="objet'", msg="", dest_url=None):
|
||||
msg = f"""<h3>{type_objet} "{msg}" utilisé(e) dans des semestres: suppression impossible.</h3>
|
||||
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}).
|
||||
Mais il est peut-être préférable de laisser ce programme intact et d'en créer une
|
||||
nouvelle version pour la modifier sans affecter les semestres déjà en place.
|
||||
</p>
|
||||
"""
|
||||
super().__init__(msg=msg, dest_url=dest_url, safe=safe)
|
||||
super().__init__(msg=msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoInvalidIdType(ScoValueError):
|
||||
@ -200,11 +199,11 @@ class ScoNoReferentielCompetences(ScoValueError):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ScoGenError(ScoValueError):
|
||||
class ScoGenError(ScoException):
|
||||
"exception avec affichage d'une page explicative ad-hoc"
|
||||
|
||||
def __init__(self, msg="", safe=False):
|
||||
super().__init__(msg, safe=safe)
|
||||
def __init__(self, msg=""):
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class AccessDenied(ScoGenError):
|
||||
|
@ -30,9 +30,8 @@
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db
|
||||
import app
|
||||
from app.models import Departement, Identite
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -102,12 +101,9 @@ def form_search_etud(
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def search_etuds_infos_from_exp(
|
||||
expnom: str = "", dept_id: int | None = None
|
||||
) -> list[Identite]:
|
||||
def search_etuds_infos_from_exp(expnom: str = "") -> list[Identite]:
|
||||
"""Cherche étudiants, expnom peut être, dans cet ordre:
|
||||
un etudid (int), un code NIP, ou une partie d'un nom (case insensitive).
|
||||
Si dept_id est None, cherche dans le dept courant, sinon cherche dans le dept indiqué.
|
||||
"""
|
||||
if not isinstance(expnom, int) and len(expnom) <= 1:
|
||||
return [] # si expnom est trop court, n'affiche rien
|
||||
@ -115,31 +111,23 @@ def search_etuds_infos_from_exp(
|
||||
etudid = int(expnom)
|
||||
except ValueError:
|
||||
etudid = None
|
||||
dept_id = g.scodoc_dept_id if dept_id is None else dept_id
|
||||
if etudid is not None:
|
||||
etud = Identite.query.filter_by(dept_id=dept_id, id=etudid).first()
|
||||
etud = Identite.query.filter_by(dept_id=g.scodoc_dept_id, id=etudid).first()
|
||||
if etud:
|
||||
return [etud]
|
||||
expnom_str = str(expnom)
|
||||
if scu.is_valid_code_nip(expnom_str):
|
||||
etuds = sorted(
|
||||
Identite.query.filter_by(dept_id=dept_id, code_nip=expnom_str).all(),
|
||||
key=lambda e: e.sort_key,
|
||||
)
|
||||
etuds = Identite.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id, code_nip=expnom_str
|
||||
).all()
|
||||
if etuds:
|
||||
return etuds
|
||||
try:
|
||||
return sorted(
|
||||
Identite.query.filter_by(dept_id=dept_id)
|
||||
.filter(
|
||||
Identite.nom.op("~*")(expnom_str)
|
||||
) # ~* matches regular expression, case-insensitive
|
||||
.all(),
|
||||
key=lambda e: e.sort_key,
|
||||
)
|
||||
except sa.exc.DataError:
|
||||
db.session.rollback()
|
||||
return []
|
||||
|
||||
return (
|
||||
Identite.query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
.filter(Identite.nom.op("~*")(expnom_str))
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
def search_etud_in_dept(expnom=""):
|
||||
@ -203,7 +191,7 @@ def search_etud_in_dept(expnom=""):
|
||||
# Choix dans la liste des résultats:
|
||||
rows = []
|
||||
e: Identite
|
||||
for e in sorted(etuds, key=lambda e: e.sort_key):
|
||||
for e in etuds:
|
||||
url_args["etudid"] = e.id
|
||||
target = url_for(endpoint, **url_args)
|
||||
cur_inscription = e.inscription_courante()
|
||||
@ -231,7 +219,6 @@ def search_etud_in_dept(expnom=""):
|
||||
"inscription_target": target,
|
||||
"groupes": groupes,
|
||||
"nomprenom": e.nomprenom,
|
||||
"_nomprenom_order": e.sort_key,
|
||||
"_nomprenom_target": target,
|
||||
"_nomprenom_td_attrs": f'id="{e.id}" class="etudinfo"',
|
||||
}
|
||||
@ -270,6 +257,7 @@ def search_etud_in_dept(expnom=""):
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
# Was chercheEtudsInfo()
|
||||
def search_etuds_infos(expnom=None, code_nip=None) -> list[dict]:
|
||||
"""recherche les étudiants correspondants à expnom ou au code_nip
|
||||
et ramene liste de mappings utilisables en DTML.
|
||||
@ -299,36 +287,71 @@ def search_etud_by_name(term: str) -> list:
|
||||
{ "label" : "<nip> <nom> <prenom>", "value" : etudid }
|
||||
"""
|
||||
may_be_nip = scu.is_valid_code_nip(term)
|
||||
etuds = search_etuds_infos_from_exp(term)
|
||||
# term = term.upper() # conserve les accents
|
||||
term = term.upper()
|
||||
if (
|
||||
not scu.ALPHANUM_EXP.match(term) # n'autorise pas les caractères spéciaux
|
||||
and not may_be_nip
|
||||
):
|
||||
data = []
|
||||
else:
|
||||
if may_be_nip:
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT nom, prenom, code_nip
|
||||
FROM identite
|
||||
WHERE
|
||||
dept_id = %(dept_id)s
|
||||
AND code_nip ILIKE %(beginning)s
|
||||
ORDER BY nom
|
||||
""",
|
||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||
)
|
||||
data = [
|
||||
{
|
||||
"label": "%s %s %s"
|
||||
% (x["code_nip"], x["nom"], scu.format_prenom(x["prenom"])),
|
||||
"value": x["code_nip"],
|
||||
}
|
||||
for x in r
|
||||
]
|
||||
else:
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT id AS etudid, nom, prenom
|
||||
FROM identite
|
||||
WHERE
|
||||
dept_id = %(dept_id)s
|
||||
AND nom ILIKE %(beginning)s
|
||||
ORDER BY nom
|
||||
""",
|
||||
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"label": f"""{(etud.code_nip+' ') if (etud.code_nip and may_be_nip) else ""}{
|
||||
etud.nom_prenom()}""",
|
||||
"value": etud.id,
|
||||
}
|
||||
for etud in etuds
|
||||
]
|
||||
data = [
|
||||
{
|
||||
"label": "%s %s" % (x["nom"], scu.format_prenom(x["prenom"])),
|
||||
"value": x["etudid"],
|
||||
}
|
||||
for x in r
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
# ---------- Recherche sur plusieurs département
|
||||
|
||||
|
||||
def search_etud_in_accessible_depts(
|
||||
expnom=None,
|
||||
) -> tuple[list[list[Identite]], list[str]]:
|
||||
def search_etud_in_accessible_depts(expnom=None, code_nip=None):
|
||||
"""
|
||||
result: list of (sorted) etuds, one list per dept.
|
||||
accessible_depts: list of dept acronyms
|
||||
result is a list of (sorted) etuds, one list per dept.
|
||||
"""
|
||||
result = []
|
||||
accessible_depts = []
|
||||
depts = Departement.query.filter_by(visible=True).all()
|
||||
for dept in depts:
|
||||
if current_user.has_permission(Permission.ScoView, dept=dept.acronym):
|
||||
if expnom:
|
||||
if expnom or code_nip:
|
||||
accessible_depts.append(dept.acronym)
|
||||
etuds = search_etuds_infos_from_exp(expnom=expnom, dept_id=dept.id)
|
||||
app.set_sco_dept(dept.acronym)
|
||||
etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip)
|
||||
else:
|
||||
etuds = []
|
||||
result.append(etuds)
|
||||
@ -348,26 +371,21 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||
]
|
||||
for etuds in result:
|
||||
if etuds:
|
||||
dept = etuds[0].departement
|
||||
rows = [
|
||||
{
|
||||
"nomprenom": etud.nom_prenom(),
|
||||
"_nomprenom_target": url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=dept.acronym, etudid=etud.id
|
||||
),
|
||||
"_nomprenom_td_attrs": f"""id="{etud.id}" class="etudinfo" """,
|
||||
"_nomprenom_order": etud.sort_key,
|
||||
}
|
||||
for etud in etuds
|
||||
]
|
||||
dept_id = etuds[0]["dept"]
|
||||
# H.append('<h3>Département %s</h3>' % DeptId)
|
||||
for e in etuds:
|
||||
e["_nomprenom_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=dept_id, etudid=e["etudid"]
|
||||
)
|
||||
e["_nomprenom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
|
||||
|
||||
tab = GenTable(
|
||||
titles={"nomprenom": "Étudiants en " + dept.acronym},
|
||||
titles={"nomprenom": "Étudiants en " + dept_id},
|
||||
columns_ids=("nomprenom",),
|
||||
rows=rows,
|
||||
rows=etuds,
|
||||
html_sortable=True,
|
||||
html_class="table_leftalign",
|
||||
# table_id="etud_in_accessible_depts",
|
||||
table_id="etud_in_accessible_depts",
|
||||
)
|
||||
|
||||
H.append('<div class="table_etud_in_dept">')
|
||||
@ -392,3 +410,48 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||
+ "\n".join(H)
|
||||
+ html_sco_header.standard_html_footer()
|
||||
)
|
||||
|
||||
|
||||
def search_inscr_etud_by_nip(code_nip, fmt="json"):
|
||||
"""Recherche multi-departement d'un étudiant par son code NIP
|
||||
Seuls les départements accessibles par l'utilisateur sont cherchés.
|
||||
|
||||
Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
|
||||
code_nip, nom, prenom, civilite_str, dept, formsemestre_id, date_debut_sem, date_fin_sem
|
||||
"""
|
||||
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
|
||||
|
||||
rows = []
|
||||
for etuds in result:
|
||||
if etuds:
|
||||
dept_id = etuds[0]["dept"]
|
||||
for e in etuds:
|
||||
for sem in e["sems"]:
|
||||
rows.append(
|
||||
{
|
||||
"dept": dept_id,
|
||||
"etudid": e["etudid"],
|
||||
"code_nip": e["code_nip"],
|
||||
"civilite_str": e["civilite_str"],
|
||||
"nom": e["nom"],
|
||||
"prenom": e["prenom"],
|
||||
"formsemestre_id": sem["formsemestre_id"],
|
||||
"date_debut_iso": sem["date_debut_iso"],
|
||||
"date_fin_iso": sem["date_fin_iso"],
|
||||
}
|
||||
)
|
||||
|
||||
columns_ids = (
|
||||
"dept",
|
||||
"etudid",
|
||||
"code_nip",
|
||||
"civilite_str",
|
||||
"nom",
|
||||
"prenom",
|
||||
"formsemestre_id",
|
||||
"date_debut_iso",
|
||||
"date_fin_iso",
|
||||
)
|
||||
tab = GenTable(columns_ids=columns_ids, rows=rows, table_id="inscr_etud_by_nip")
|
||||
|
||||
return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)
|
||||
|
@ -45,29 +45,28 @@ import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
# ---- Table recap formation
|
||||
def formation_table_recap(formation: Formation, fmt="html") -> Response:
|
||||
def formation_table_recap(formation_id, fmt="html") -> Response:
|
||||
"""Table recapitulant formation."""
|
||||
rows = []
|
||||
T = []
|
||||
formation = Formation.query.get_or_404(formation_id)
|
||||
ues = formation.ues.order_by(UniteEns.semestre_idx, UniteEns.numero)
|
||||
can_edit = current_user.has_permission(Permission.EditFormation)
|
||||
li = 0
|
||||
for ue in ues:
|
||||
# L'UE
|
||||
rows.append(
|
||||
T.append(
|
||||
{
|
||||
"sem": f"S{ue.semestre_idx}" if ue.semestre_idx is not None else "-",
|
||||
"_sem_order": f"{li:04d}",
|
||||
"code": ue.acronyme,
|
||||
"titre": ue.titre or "",
|
||||
"_titre_target": (
|
||||
url_for(
|
||||
"notes.ue_edit",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=ue.id,
|
||||
)
|
||||
if can_edit
|
||||
else None
|
||||
),
|
||||
"_titre_target": url_for(
|
||||
"notes.ue_edit",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
ue_id=ue.id,
|
||||
)
|
||||
if can_edit
|
||||
else None,
|
||||
"apo": ue.code_apogee or "",
|
||||
"_apo_td_attrs": f""" data-oid="{ue.id}" data-value="{ue.code_apogee or ''}" """,
|
||||
"coef": ue.coefficient or "",
|
||||
@ -82,25 +81,21 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
|
||||
for mod in modules:
|
||||
nb_moduleimpls = mod.modimpls.count()
|
||||
# le module (ou ressource ou sae)
|
||||
rows.append(
|
||||
T.append(
|
||||
{
|
||||
"sem": (
|
||||
f"S{mod.semestre_id}"
|
||||
if mod.semestre_id is not None
|
||||
else "-"
|
||||
),
|
||||
"sem": f"S{mod.semestre_id}"
|
||||
if mod.semestre_id is not None
|
||||
else "-",
|
||||
"_sem_order": f"{li:04d}",
|
||||
"code": mod.code,
|
||||
"titre": mod.abbrev or mod.titre,
|
||||
"_titre_target": (
|
||||
url_for(
|
||||
"notes.module_edit",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
module_id=mod.id,
|
||||
)
|
||||
if can_edit
|
||||
else None
|
||||
),
|
||||
"_titre_target": url_for(
|
||||
"notes.module_edit",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
module_id=mod.id,
|
||||
)
|
||||
if can_edit
|
||||
else None,
|
||||
"apo": mod.code_apogee,
|
||||
"_apo_td_attrs": f""" data-oid="{mod.id}" data-value="{mod.code_apogee or ''}" """,
|
||||
"coef": mod.coefficient,
|
||||
@ -151,7 +146,7 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
|
||||
|
||||
tab = GenTable(
|
||||
columns_ids=columns_ids,
|
||||
rows=rows,
|
||||
rows=T,
|
||||
titles=titles,
|
||||
origin=f"Généré par {scu.sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
caption=title,
|
||||
@ -159,15 +154,11 @@ def formation_table_recap(formation: Formation, fmt="html") -> Response:
|
||||
html_class=html_class,
|
||||
html_class_ignore_default=True,
|
||||
html_table_attrs=f"""
|
||||
data-apo_ue_save_url="{
|
||||
url_for('apiweb.ue_set_code_apogee', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-apo_mod_save_url="{
|
||||
url_for('apiweb.formation_module_set_code_apogee', scodoc_dept=g.scodoc_dept)
|
||||
}"
|
||||
data-apo_ue_save_url="{url_for('notes.ue_set_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
data-apo_mod_save_url="{url_for('notes.module_set_apo', scodoc_dept=g.scodoc_dept)}"
|
||||
""",
|
||||
html_with_td_classes=True,
|
||||
base_url=f"{request.base_url}",
|
||||
base_url=f"{request.base_url}?formation_id={formation_id}",
|
||||
page_title=title,
|
||||
html_title=f"<h2>{title}</h2>",
|
||||
pdf_title=title,
|
||||
@ -191,7 +182,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
|
||||
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
|
||||
for formation_id in formation_ids:
|
||||
formation = db.session.get(Formation, formation_id)
|
||||
xls = formation_table_recap(formation, fmt="xlsx").data
|
||||
xls = formation_table_recap(formation_id, fmt="xlsx").data
|
||||
filename = (
|
||||
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
|
||||
)
|
||||
|
@ -125,9 +125,6 @@ def formation_export_dict(
|
||||
if formation.is_apc():
|
||||
# BUT: indique niveau de compétence associé à l'UE
|
||||
if ue.niveau_competence:
|
||||
ue_dict["apc_niveau_competence_titre"] = (
|
||||
ue.niveau_competence.competence.titre
|
||||
)
|
||||
ue_dict["apc_niveau_libelle"] = ue.niveau_competence.libelle
|
||||
ue_dict["apc_niveau_annee"] = ue.niveau_competence.annee
|
||||
ue_dict["apc_niveau_ordre"] = ue.niveau_competence.ordre
|
||||
@ -146,7 +143,6 @@ def formation_export_dict(
|
||||
|
||||
if not export_codes_apo:
|
||||
ue_dict.pop("code_apogee", None)
|
||||
ue_dict.pop("code_apogee_rcue", None)
|
||||
if ue_dict.get("ects") is None:
|
||||
ue_dict.pop("ects", None)
|
||||
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
|
||||
@ -222,7 +218,7 @@ def formation_export(
|
||||
"""Get a formation, with UE, matieres, modules
|
||||
in desired format
|
||||
"""
|
||||
formation = Formation.get_formation(formation_id)
|
||||
formation: Formation = Formation.query.get_or_404(formation_id)
|
||||
f_dict = formation_export_dict(
|
||||
formation,
|
||||
export_ids=export_ids,
|
||||
@ -261,39 +257,29 @@ def _formation_retreive_refcomp(f_dict: dict) -> int:
|
||||
return refcomp.id
|
||||
else:
|
||||
flash(
|
||||
f"""Impossible de trouver le référentiel de compétence pour {
|
||||
refcomp_specialite} : est-il chargé ?"""
|
||||
f"Impossible de trouver le référentiel de compétence pour {refcomp_specialite} : est-il chargé ?"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _formation_retreive_apc_niveau(
|
||||
referentiel_competence_id: int, ue_dict: dict
|
||||
) -> int | None:
|
||||
) -> int:
|
||||
"""Recherche dans le ref. de comp. un niveau pour cette UE.
|
||||
Utilise (libelle, annee, ordre) comme clé, ou
|
||||
(competence_titre, libelle, annee, ordre) si présent.
|
||||
Utilise (libelle, annee, ordre) comme clé.
|
||||
"""
|
||||
libelle = ue_dict.get("apc_niveau_libelle")
|
||||
annee = ue_dict.get("apc_niveau_annee")
|
||||
ordre = ue_dict.get("apc_niveau_ordre")
|
||||
competence_titre = ue_dict.get("apc_niveau_competence_titre")
|
||||
niveau = None
|
||||
if all((competence_titre, libelle, annee, ordre)):
|
||||
niveau = (
|
||||
ApcNiveau.query.filter_by(libelle=libelle, annee=annee, ordre=ordre)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id, titre=competence_titre)
|
||||
).first()
|
||||
|
||||
elif all((libelle, annee, ordre)):
|
||||
if all((libelle, annee, ordre)):
|
||||
niveau = (
|
||||
ApcNiveau.query.filter_by(libelle=libelle, annee=annee, ordre=ordre)
|
||||
.join(ApcCompetence)
|
||||
.filter_by(referentiel_id=referentiel_competence_id)
|
||||
).first()
|
||||
|
||||
return niveau.id if niveau is not None else None
|
||||
if niveau is not None:
|
||||
return niveau.id
|
||||
return None
|
||||
|
||||
|
||||
def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
|
||||
|
@ -39,7 +39,7 @@ import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.models import Departement
|
||||
from app.models import Formation, FormSemestre
|
||||
from app.scodoc import sco_cache, codes_cursus, sco_preferences
|
||||
from app.scodoc import sco_cache, codes_cursus, sco_formations, sco_preferences
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.codes_cursus import NO_SEMESTRE_ID
|
||||
from app.scodoc.sco_exceptions import ScoInvalidIdType, ScoValueError
|
||||
@ -68,7 +68,6 @@ _formsemestreEditor = ndb.EditableTable(
|
||||
"ens_can_edit_eval",
|
||||
"elt_sem_apo",
|
||||
"elt_annee_apo",
|
||||
"elt_passage_apo",
|
||||
"edt_id",
|
||||
),
|
||||
filter_dept=True,
|
||||
@ -230,14 +229,11 @@ def etapes_apo_str(etapes):
|
||||
return ", ".join([str(x) for x in etapes])
|
||||
|
||||
|
||||
def do_formsemestre_create( # DEPRECATED, use FormSemestre.create_formsemestre()
|
||||
args, silent=False
|
||||
):
|
||||
def do_formsemestre_create(args, silent=False):
|
||||
"create a formsemestre"
|
||||
from app.models import ScolarNews
|
||||
from app.scodoc import sco_groups
|
||||
|
||||
log("Warning: do_formsemestre_create is deprecated")
|
||||
cnx = ndb.GetDBConnexion()
|
||||
formsemestre_id = _formsemestreEditor.create(cnx, args)
|
||||
if args["etapes"]:
|
||||
@ -423,23 +419,49 @@ def sem_set_responsable_name(sem):
|
||||
)
|
||||
|
||||
|
||||
def sem_in_annee_scolaire(sem: dict, year=False): # OBSOLETE
|
||||
def sem_in_semestre_scolaire(
|
||||
sem,
|
||||
year=False,
|
||||
periode=None,
|
||||
mois_pivot_annee=scu.MONTH_DEBUT_ANNEE_SCOLAIRE,
|
||||
mois_pivot_periode=scu.MONTH_DEBUT_PERIODE2,
|
||||
) -> bool:
|
||||
"""Vrai si la date du début du semestre est dans la période indiquée (1,2,0)
|
||||
du semestre `periode` de l'année scolaire indiquée
|
||||
(ou, à défaut, de celle en cours).
|
||||
|
||||
La période utilise les même conventions que semset["sem_id"];
|
||||
* 1 : première période
|
||||
* 2 : deuxième période
|
||||
* 0 ou période non précisée: annualisé (donc inclut toutes les périodes)
|
||||
)
|
||||
"""
|
||||
if not year:
|
||||
year = scu.annee_scolaire()
|
||||
# n'utilise pas le jour pivot
|
||||
jour_pivot_annee = jour_pivot_periode = 1
|
||||
# calcule l'année universitaire et la période
|
||||
sem_annee, sem_periode = FormSemestre.comp_periode(
|
||||
datetime.datetime.fromisoformat(sem["date_debut_iso"]),
|
||||
mois_pivot_annee,
|
||||
mois_pivot_periode,
|
||||
jour_pivot_annee,
|
||||
jour_pivot_periode,
|
||||
)
|
||||
if periode is None or periode == 0:
|
||||
return sem_annee == year
|
||||
return sem_annee == year and sem_periode == periode
|
||||
|
||||
|
||||
def sem_in_annee_scolaire(sem, year=False):
|
||||
"""Test si sem appartient à l'année scolaire year (int).
|
||||
N'utilise que la date de début, pivot au 1er août.
|
||||
Si année non specifiée, année scolaire courante
|
||||
"""
|
||||
return FormSemestre.est_in_semestre_scolaire(
|
||||
datetime.date.fromisoformat(sem["date_debut_iso"]), year, periode=0
|
||||
)
|
||||
return sem_in_semestre_scolaire(sem, year, periode=0)
|
||||
|
||||
|
||||
def sem_in_semestre_scolaire(sem, year=False, periode=None): # OBSOLETE
|
||||
return FormSemestre.est_in_semestre_scolaire(
|
||||
datetime.date.fromisoformat(sem["date_debut_iso"]), year, periode=periode
|
||||
)
|
||||
|
||||
|
||||
def sem_est_courant(sem: dict): # -> FormSemestre.est_courant
|
||||
def sem_est_courant(sem): # -> FormSemestre.est_courant
|
||||
"""Vrai si la date actuelle (now) est dans le semestre (les dates de début et fin sont incluses)"""
|
||||
now = time.strftime("%Y-%m-%d")
|
||||
debut = ndb.DateDMYtoISO(sem["date_debut"])
|
||||
|
@ -37,17 +37,16 @@ from app import db
|
||||
from app.auth.models import User
|
||||
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import (
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
Evaluation,
|
||||
FormSemestreUECoef,
|
||||
Module,
|
||||
ModuleImpl,
|
||||
ScoDocSiteConfig,
|
||||
ScolarAutorisationInscription,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarNews,
|
||||
Evaluation,
|
||||
UniteEns,
|
||||
ScoDocSiteConfig,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
ApcValidationAnnee,
|
||||
ApcValidationRCUE,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.models.formations import Formation
|
||||
from app.models.formsemestre import FormSemestre
|
||||
@ -63,6 +62,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_compute_moy
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups_copy
|
||||
@ -76,7 +76,7 @@ from app.scodoc import sco_users
|
||||
|
||||
def _default_sem_title(formation):
|
||||
"""Default title for a semestre in formation"""
|
||||
return formation.acronyme
|
||||
return formation.titre
|
||||
|
||||
|
||||
def formsemestre_createwithmodules():
|
||||
@ -438,24 +438,8 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
"elt_sem_apo",
|
||||
{
|
||||
"size": 32,
|
||||
"title": "Element(s) Apogée sem.:",
|
||||
"explanation": """associé(s) au résultat du semestre (ex: VRTW1).
|
||||
Inutile en BUT. Séparés par des virgules.""",
|
||||
"allow_null": (
|
||||
not sco_preferences.get_preference("always_require_apo_sem_codes")
|
||||
or (formsemestre and formsemestre.modalite == "EXT")
|
||||
or (formsemestre and formsemestre.formation.is_apc())
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
modform.append(
|
||||
(
|
||||
"elt_annee_apo",
|
||||
{
|
||||
"size": 32,
|
||||
"title": "Element(s) Apogée année:",
|
||||
"explanation": "associé(s) au résultat de l'année (ex: VRT1A). Séparés par des virgules.",
|
||||
"title": "Element(s) Apogée:",
|
||||
"explanation": "associé(s) au résultat du semestre (ex: VRTW1). Inutile en BUT. Séparés par des virgules.",
|
||||
"allow_null": not sco_preferences.get_preference(
|
||||
"always_require_apo_sem_codes"
|
||||
)
|
||||
@ -465,12 +449,15 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
|
||||
)
|
||||
modform.append(
|
||||
(
|
||||
"elt_passage_apo",
|
||||
"elt_annee_apo",
|
||||
{
|
||||
"size": 32,
|
||||
"title": "Element(s) Apogée passage:",
|
||||
"explanation": "associé(s) au passage. Séparés par des virgules.",
|
||||
"allow_null": True, # toujours optionnel car rarement utilisé
|
||||
"title": "Element(s) Apogée:",
|
||||
"explanation": "associé(s) au résultat de l'année (ex: VRT1A). Séparés par des virgules.",
|
||||
"allow_null": not sco_preferences.get_preference(
|
||||
"always_require_apo_sem_codes"
|
||||
)
|
||||
or (formsemestre and formsemestre.modalite == "EXT"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@ -1262,7 +1249,7 @@ def formsemestre_clone(formsemestre_id):
|
||||
raise ScoValueError("id responsable invalide")
|
||||
new_formsemestre_id = do_formsemestre_clone(
|
||||
formsemestre_id,
|
||||
resp,
|
||||
resp.id,
|
||||
tf[2]["date_debut"],
|
||||
tf[2]["date_fin"],
|
||||
clone_evaluations=tf[2]["clone_evaluations"],
|
||||
@ -1280,7 +1267,7 @@ def formsemestre_clone(formsemestre_id):
|
||||
|
||||
def do_formsemestre_clone(
|
||||
orig_formsemestre_id,
|
||||
responsable: User, # new resp.
|
||||
responsable_id, # new resp.
|
||||
date_debut,
|
||||
date_fin, # 'dd/mm/yyyy'
|
||||
clone_evaluations=False,
|
||||
@ -1293,68 +1280,49 @@ def do_formsemestre_clone(
|
||||
formsemestre_orig: FormSemestre = FormSemestre.query.get_or_404(
|
||||
orig_formsemestre_id
|
||||
)
|
||||
orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
|
||||
cnx = ndb.GetDBConnexion()
|
||||
# 1- create sem
|
||||
args = formsemestre_orig.to_dict()
|
||||
args = orig_sem.copy()
|
||||
del args["formsemestre_id"]
|
||||
del args["id"]
|
||||
del args["parcours"] # copiés ensuite
|
||||
args["responsables"] = [responsable]
|
||||
args["responsables"] = [responsable_id]
|
||||
args["date_debut"] = date_debut
|
||||
args["date_fin"] = date_fin
|
||||
args["etat"] = 1 # non verrouillé
|
||||
|
||||
formsemestre = FormSemestre.create_formsemestre(args)
|
||||
log(f"created formsemestre {formsemestre}")
|
||||
formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
|
||||
log(f"created formsemestre {formsemestre_id}")
|
||||
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
|
||||
# 2- create moduleimpls
|
||||
modimpl_orig: ModuleImpl
|
||||
for modimpl_orig in formsemestre_orig.modimpls:
|
||||
assert isinstance(modimpl_orig, ModuleImpl)
|
||||
assert isinstance(modimpl_orig.id, int)
|
||||
log(f"cloning {modimpl_orig}")
|
||||
args = modimpl_orig.to_dict(with_module=False)
|
||||
args["formsemestre_id"] = formsemestre.id
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
modimpl_new = ModuleImpl.create_from_dict(args)
|
||||
log(f"created ModuleImpl from {args}")
|
||||
db.session.flush()
|
||||
# copy enseignants
|
||||
for ens in modimpl_orig.enseignants:
|
||||
modimpl_new.enseignants.append(ens)
|
||||
db.session.add(modimpl_new)
|
||||
db.session.flush()
|
||||
log(f"new moduleimpl.id = {modimpl_new.id}")
|
||||
# optionally, copy evaluations
|
||||
if clone_evaluations:
|
||||
e: Evaluation
|
||||
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
|
||||
log(f"cloning evaluation {e.id}")
|
||||
# copie en enlevant la date
|
||||
args = dict(e.__dict__)
|
||||
args.pop("_sa_instance_state")
|
||||
args.pop("id")
|
||||
args.pop("date_debut", None)
|
||||
args.pop("date_fin", None)
|
||||
args["moduleimpl_id"] = modimpl_new.id
|
||||
new_eval = Evaluation(**args)
|
||||
db.session.add(new_eval)
|
||||
db.session.commit()
|
||||
new_eval = e.clone(
|
||||
not_copying=("date_debut", "date_fin", "moduleimpl_id")
|
||||
)
|
||||
new_eval.moduleimpl_id = modimpl_new.id
|
||||
# Copie les poids APC de l'évaluation
|
||||
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
|
||||
db.session.commit()
|
||||
if clone_evaluations:
|
||||
flash(
|
||||
"Attention: les évaluations n'ont plus de dates: n'oubliez pas de les indiquer"
|
||||
)
|
||||
|
||||
# 3- copy uecoefs
|
||||
for ue_coef in FormSemestreUECoef.query.filter_by(
|
||||
formsemestre_id=formsemestre_orig.id
|
||||
):
|
||||
new_ue_coef = FormSemestreUECoef(
|
||||
formsemestre_id=formsemestre.id,
|
||||
ue_id=ue_coef.ue_id,
|
||||
coefficient=ue_coef.coefficient,
|
||||
)
|
||||
db.session.add(new_ue_coef)
|
||||
db.session.flush()
|
||||
objs = sco_formsemestre.formsemestre_uecoef_list(
|
||||
cnx, args={"formsemestre_id": orig_formsemestre_id}
|
||||
)
|
||||
for obj in objs:
|
||||
args = obj.copy()
|
||||
args["formsemestre_id"] = formsemestre_id
|
||||
_ = sco_formsemestre.formsemestre_uecoef_create(cnx, args)
|
||||
|
||||
# NB: don't copy notes_formsemestre_custommenu (usually specific)
|
||||
|
||||
@ -1366,11 +1334,11 @@ def do_formsemestre_clone(
|
||||
if not prefs.is_global(pname):
|
||||
pvalue = prefs[pname]
|
||||
try:
|
||||
prefs.base_prefs.set(formsemestre.id, pname, pvalue)
|
||||
prefs.base_prefs.set(formsemestre_id, pname, pvalue)
|
||||
except ValueError:
|
||||
log(
|
||||
f"""do_formsemestre_clone: ignoring old preference {
|
||||
pname}={pvalue} for {formsemestre}"""
|
||||
"do_formsemestre_clone: ignoring old preference %s=%s for %s"
|
||||
% (pname, pvalue, formsemestre_id)
|
||||
)
|
||||
|
||||
# 5- Copie les parcours
|
||||
@ -1381,10 +1349,10 @@ def do_formsemestre_clone(
|
||||
# 6- Copy partitions and groups
|
||||
if clone_partitions:
|
||||
sco_groups_copy.clone_partitions_and_groups(
|
||||
orig_formsemestre_id, formsemestre.id
|
||||
orig_formsemestre_id, formsemestre_id
|
||||
)
|
||||
|
||||
return formsemestre.id
|
||||
return formsemestre_id
|
||||
|
||||
|
||||
def formsemestre_delete(formsemestre_id: int) -> str | flask.Response:
|
||||
|
@ -42,6 +42,7 @@ from app.models.groups import Partition, GroupDescr
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.codes_cursus import UE_STANDARD, UE_SPORT, UE_TYPE_NAME
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -111,11 +112,12 @@ def do_formsemestre_inscription_create(args, method=None):
|
||||
},
|
||||
)
|
||||
# Log etudiant
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method=method,
|
||||
etudid=args["etudid"],
|
||||
msg=f"inscription en semestre {args['formsemestre_id']}",
|
||||
commit=True,
|
||||
commit=False,
|
||||
)
|
||||
#
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=args["formsemestre_id"])
|
||||
@ -263,11 +265,12 @@ def do_formsemestre_desinscription(
|
||||
db.session.commit()
|
||||
flash(f"Semestre extérieur supprimé: {formsemestre.titre_annee()}")
|
||||
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="formsemestre_desinscription",
|
||||
etudid=etudid,
|
||||
msg=f"desinscription semestre {formsemestre_id}",
|
||||
commit=True,
|
||||
commit=False,
|
||||
)
|
||||
|
||||
|
||||
|
@ -58,13 +58,12 @@ from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_archives_formsemestre
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_compute_moy
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre
|
||||
@ -75,7 +74,6 @@ from app.scodoc import sco_users
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
|
||||
from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
|
||||
|
||||
import sco_version
|
||||
|
||||
|
||||
@ -428,12 +426,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre | None) -> str:
|
||||
"endpoint": "notes.formsemestre_list_saisies_notes",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
},
|
||||
{
|
||||
"title": "Importer les notes",
|
||||
"endpoint": "notes.formsemestre_import_notes",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": formsemestre.est_chef_or_diretud(),
|
||||
},
|
||||
]
|
||||
menu_jury = [
|
||||
{
|
||||
@ -791,10 +783,6 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
)
|
||||
#
|
||||
H.append('<div class="sem-groups-abs">')
|
||||
|
||||
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
|
||||
show_abs: str = "hidden" if disable_abs else ""
|
||||
|
||||
# Genere liste pour chaque partition (categorie de groupes)
|
||||
for partition in formsemestre.get_partitions_list():
|
||||
groups = partition.groups.all()
|
||||
@ -806,14 +794,13 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
<div class="sem-groups-partition-titre">{
|
||||
'Groupes de ' + partition.partition_name
|
||||
if partition.partition_name else
|
||||
('aucun étudiant inscrit' if partition_is_empty else 'Tous les étudiants')}
|
||||
'Tous les étudiants'}
|
||||
</div>
|
||||
<div class="sem-groups-partition-titre">{
|
||||
"Assiduité" if not partition_is_empty and not show_abs else ""
|
||||
"Assiduité" if not partition_is_empty else ""
|
||||
}</div>
|
||||
"""
|
||||
)
|
||||
|
||||
if groups:
|
||||
for group in groups:
|
||||
n_members = effectifs[group.id]
|
||||
@ -834,7 +821,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
- {n_members} étudiants</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sem-groups-assi {show_abs}">
|
||||
<div class="sem-groups-assi">
|
||||
|
||||
"""
|
||||
)
|
||||
if can_edit_abs:
|
||||
@ -897,7 +885,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
)
|
||||
|
||||
H.append("</div>") # /sem-groups-assi
|
||||
if partition_is_empty and not partition.is_default():
|
||||
if partition_is_empty:
|
||||
H.append(
|
||||
'<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
|
||||
)
|
||||
@ -922,45 +910,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
}">Ajouter une partition</a></h4>"""
|
||||
)
|
||||
|
||||
# --- Formulaire importation Assiduité excel (si autorisé)
|
||||
if current_user.has_permission(Permission.AbsChange) and not disable_abs:
|
||||
H.append(
|
||||
f"""<p>
|
||||
<a class="stdlink" href="{url_for('assiduites.feuille_abs_formsemestre',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)}">
|
||||
Importation de l'assiduité depuis un fichier excel</a>
|
||||
</p>"""
|
||||
)
|
||||
|
||||
# --- Lien Traitement Justificatifs:
|
||||
|
||||
if (
|
||||
current_user.has_permission(Permission.AbsJustifView)
|
||||
and current_user.has_permission(Permission.JustifValidate)
|
||||
and not disable_abs
|
||||
):
|
||||
H.append(
|
||||
f"""<p>
|
||||
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)}">
|
||||
Traitement des justificatifs d'absence</a>
|
||||
</p>"""
|
||||
)
|
||||
|
||||
H.append("</div>")
|
||||
|
||||
if disable_abs:
|
||||
H.append(
|
||||
f"""
|
||||
<div class="scobox" style="width:fit-content; font-style: italic;">
|
||||
La gestion des absences est désactivée dans ScoDoc pour ce semestre:
|
||||
{disable_abs}
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
@ -1068,6 +1018,9 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
use_ue_coefs = sco_preferences.get_preference("use_ue_coefs", formsemestre_id)
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}"
|
||||
),
|
||||
'<div class="formsemestre_status">',
|
||||
formsemestre_status_head(
|
||||
formsemestre_id=formsemestre_id, page_title="Tableau de bord"
|
||||
@ -1181,6 +1134,18 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
"</div>",
|
||||
]
|
||||
|
||||
# --- Lien Traitement Justificatifs:
|
||||
|
||||
if current_user.has_permission(Permission.AbsJustifView):
|
||||
H.append(
|
||||
f"""<p>
|
||||
<a class="stdlink" href="{url_for('assiduites.traitement_justificatifs',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id)}">
|
||||
Traitement des justificatifs d'absence</a>
|
||||
</p>"""
|
||||
)
|
||||
|
||||
# --- Lien mail enseignants:
|
||||
adrlist = list(mails_enseignants - {None, ""})
|
||||
if adrlist:
|
||||
@ -1190,11 +1155,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
|
||||
len(adrlist)} enseignants du semestre</a>
|
||||
</p>"""
|
||||
)
|
||||
return render_template(
|
||||
"sco_page.j2",
|
||||
content="".join(H),
|
||||
title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}",
|
||||
)
|
||||
return "".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
_TABLEAU_MODULES_HEAD = """
|
||||
@ -1257,6 +1218,18 @@ def formsemestre_tableau_modules(
|
||||
<td colspan="2">"""
|
||||
)
|
||||
|
||||
expr = sco_compute_moy.get_ue_expression(
|
||||
formsemestre.id, ue.id, html_quote=True
|
||||
)
|
||||
if expr:
|
||||
H.append(
|
||||
f""" <span class="formula" title="mode de calcul de la moyenne d'UE">{expr}</span>
|
||||
<span class="warning">formule inutilisée en ScoDoc 9: <a href="{
|
||||
url_for("notes.delete_ue_expr", scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id, ue_id=ue.id )
|
||||
}
|
||||
">supprimer</a></span>"""
|
||||
)
|
||||
|
||||
H.append("</td></tr>")
|
||||
|
||||
if ue.type != codes_cursus.UE_STANDARD:
|
||||
@ -1455,10 +1428,7 @@ def formsemestre_note_etuds_sans_notes(
|
||||
):
|
||||
"""Affichage et saisie des étudiants sans notes
|
||||
|
||||
Si etudid est spécifié, traite un seul étudiant.
|
||||
"""
|
||||
from app.views import ScoData
|
||||
|
||||
Si etudid est spécifié, traite un seul étudiant."""
|
||||
formsemestre: FormSemestre = FormSemestre.query.filter_by(
|
||||
id=formsemestre_id, dept_id=g.scodoc_dept_id
|
||||
).first_or_404()
|
||||
@ -1473,9 +1443,8 @@ def formsemestre_note_etuds_sans_notes(
|
||||
if request.method == "POST":
|
||||
if not code in ("ATT", "EXC", "ABS"):
|
||||
raise ScoValueError("code invalide: doit être ATT, ABS ou EXC")
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etud in etuds:
|
||||
formsemestre.etud_set_all_missing_notes(etud, code)
|
||||
for etud in etuds:
|
||||
formsemestre.etud_set_all_missing_notes(etud, code)
|
||||
flash(f"Notes de {len(etuds)} étudiants affectées à {code}")
|
||||
return redirect(
|
||||
url_for(
|
||||
@ -1484,19 +1453,61 @@ def formsemestre_note_etuds_sans_notes(
|
||||
formsemestre_id=formsemestre.id,
|
||||
)
|
||||
)
|
||||
if not etuds and etudid is not None:
|
||||
flash(
|
||||
f"""{Identite.get_etud(etudid).nomprenom}
|
||||
if not etuds:
|
||||
if etudid is None:
|
||||
message = """<h3>aucun étudiant sans notes</h3>"""
|
||||
else:
|
||||
flash(
|
||||
f"""{Identite.get_etud(etudid).nomprenom}
|
||||
a déjà des notes"""
|
||||
)
|
||||
return redirect(
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
noms = "</li><li>".join(
|
||||
[
|
||||
f"""<a href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}" class="discretelink">{etud.nomprenom}</a>"""
|
||||
for etud in etuds
|
||||
]
|
||||
)
|
||||
return redirect(
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
etud = Identite.get_etud(etudid) if etudid is not None else None
|
||||
return render_template(
|
||||
"formsemestre/etuds_sans_notes.j2",
|
||||
etudid=etudid,
|
||||
etuds=etuds,
|
||||
sco=ScoData(formsemestre=formsemestre, etud=etud),
|
||||
title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}",
|
||||
)
|
||||
message = f"""
|
||||
<h3>Étudiants sans notes:</h3>
|
||||
<ul>
|
||||
<li>{noms}</li>
|
||||
</ul>
|
||||
"""
|
||||
|
||||
return f"""
|
||||
{html_sco_header.sco_header(
|
||||
page_title=f"{formsemestre.sem_modalite()} {formsemestre.titre_annee()}"
|
||||
)}
|
||||
<div class="formsemestre_status">
|
||||
{formsemestre_status_head(
|
||||
formsemestre_id=formsemestre_id, page_title="Étudiants sans notes"
|
||||
)}
|
||||
</div>
|
||||
{message}
|
||||
|
||||
<style>
|
||||
.sco-std-form select, .sco-std-form input[type="submit"] {{
|
||||
height: 24px;
|
||||
}}
|
||||
</style>
|
||||
<form class="sco-std-form" method="post">
|
||||
<input type="hidden" name="formsemestre_id" value="{formsemestre.id}">
|
||||
<input type="hidden" name="etudid" value="{etudid or ""}">
|
||||
|
||||
Mettre toutes les notes de {"ces étudiants" if len(etuds)> 1 else "cet étudiant"}
|
||||
à :
|
||||
<select name="code">
|
||||
<option value="ABS">ABS (absent, compte zéro)</option>
|
||||
<option value="ATT" selected>ATT (en attente)</option>
|
||||
<option value="EXC">EXC (neutralisée)</option>
|
||||
</select>
|
||||
<input type="submit" value="Enregistrer">
|
||||
</form>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
@ -31,7 +31,7 @@ import time
|
||||
|
||||
import flask
|
||||
from flask import url_for, flash, g, request
|
||||
from flask.templating import render_template
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app.models import Identite, Evaluation
|
||||
@ -41,7 +41,7 @@ from app import db, log
|
||||
|
||||
from app.comp import res_sem
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Formation, FormSemestre, UniteEns, ScolarNews, Scolog
|
||||
from app.models import Formation, FormSemestre, UniteEns, ScolarNews
|
||||
from app.models.notes import etud_has_notes_attente
|
||||
from app.models.validations import (
|
||||
ScolarAutorisationInscription,
|
||||
@ -49,6 +49,7 @@ from app.models.validations import (
|
||||
)
|
||||
from app.models.but_validations import ApcValidationRCUE, ApcValidationAnnee
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc.codes_cursus import *
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
|
||||
|
||||
@ -64,6 +65,7 @@ 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
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------------
|
||||
@ -114,7 +116,7 @@ def formsemestre_validation_etud_form(
|
||||
check = True
|
||||
|
||||
etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud_d, formsemestre_id)
|
||||
if not Se.sem["etat"]:
|
||||
raise ScoValueError("validation: semestre verrouille")
|
||||
|
||||
@ -260,8 +262,8 @@ def formsemestre_validation_etud_form(
|
||||
return "\n".join(H + footer)
|
||||
|
||||
# Infos si pas de semestre précédent
|
||||
if not Se.prev_formsemestre:
|
||||
if Se.cur_sem.semestre_id == 1:
|
||||
if not Se.prev:
|
||||
if Se.sem["semestre_id"] == 1:
|
||||
H.append("<p>Premier semestre (pas de précédent)</p>")
|
||||
else:
|
||||
H.append("<p>Pas de semestre précédent !</p>")
|
||||
@ -272,7 +274,7 @@ def formsemestre_validation_etud_form(
|
||||
f"""Le jury n'a pas statué sur le semestre précédent ! (<a href="{
|
||||
url_for("notes.formsemestre_validation_etud_form",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=Se.prev_formsemestre.id,
|
||||
formsemestre_id=Se.prev["formsemestre_id"],
|
||||
etudid=etudid)
|
||||
}">le faire maintenant</a>)
|
||||
"""
|
||||
@ -308,9 +310,9 @@ def formsemestre_validation_etud_form(
|
||||
H.append("</p>")
|
||||
|
||||
# Cas particulier pour ATJ: corriger precedent avant de continuer
|
||||
if Se.prev_formsemestre and Se.prev_decision and Se.prev_decision["code"] == ATJ:
|
||||
if Se.prev_decision and Se.prev_decision["code"] == ATJ:
|
||||
H.append(
|
||||
f"""<div class="sfv_warning"><p>La décision du semestre précédent est en
|
||||
"""<div class="sfv_warning"><p>La décision du semestre précédent est en
|
||||
<b>attente</b> à cause d\'un <b>problème d\'assiduité<b>.</p>
|
||||
<p>Vous devez la corriger avant de continuer ce jury. Soit vous considérez que le
|
||||
problème d'assiduité n'est pas réglé et choisissez de ne pas valider le semestre
|
||||
@ -318,16 +320,14 @@ def formsemestre_validation_etud_form(
|
||||
l'assiduité.</p>
|
||||
<form method="get" action="formsemestre_validation_etud_form">
|
||||
<input type="submit" value="Statuer sur le semestre précédent"/>
|
||||
<input type="hidden" name="formsemestre_id" value="{Se.prev_formsemestre.id}"/>
|
||||
<input type="hidden" name="etudid" value="{etudid}"/>
|
||||
<input type="hidden" name="desturl" value="{
|
||||
url_for("notes.formsemestre_validation_etud_form",
|
||||
etudid=etudid, formsemestre_id=formsemestre_id, scodoc_dept=g.scodoc_dept
|
||||
)}"/>
|
||||
<input type="hidden" name="formsemestre_id" value="%s"/>
|
||||
<input type="hidden" name="etudid" value="%s"/>
|
||||
<input type="hidden" name="desturl" value="formsemestre_validation_etud_form?etudid=%s&formsemestre_id=%s"/>
|
||||
"""
|
||||
% (Se.prev["formsemestre_id"], etudid, etudid, formsemestre_id)
|
||||
)
|
||||
if sortcol:
|
||||
H.append(f"""<input type="hidden" name="sortcol" value="{sortcol}"/>""")
|
||||
H.append('<input type="hidden" name="sortcol" value="%s"/>' % sortcol)
|
||||
H.append("</form></div>")
|
||||
|
||||
H.append(html_sco_header.sco_footer())
|
||||
@ -405,7 +405,7 @@ def formsemestre_validation_etud(
|
||||
sortcol=None,
|
||||
):
|
||||
"""Enregistre validation"""
|
||||
etud = Identite.get_etud(etudid)
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
# retrouve la decision correspondant au code:
|
||||
choices = Se.get_possible_choices(assiduite=True)
|
||||
@ -438,7 +438,7 @@ def formsemestre_validation_etud_manu(
|
||||
"""Enregistre validation"""
|
||||
if assidu:
|
||||
assidu = True
|
||||
etud = Identite.get_etud(etudid)
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if code_etat in Se.parcours.UNUSED_CODES:
|
||||
raise ScoValueError("code decision invalide dans ce parcours")
|
||||
@ -494,35 +494,32 @@ def decisions_possible_rows(Se, assiduite, subtitle="", trclass=""):
|
||||
choices = Se.get_possible_choices(assiduite=assiduite)
|
||||
if not choices:
|
||||
return ""
|
||||
prev_title = ""
|
||||
if Se.prev_formsemestre:
|
||||
if Se.prev_formsemestre.semestre_id >= 0:
|
||||
prev_title = "%s%d" % (
|
||||
Se.parcours.SESSION_ABBRV,
|
||||
Se.prev_formsemestre.semestre_id,
|
||||
)
|
||||
TitlePrev = ""
|
||||
if Se.prev:
|
||||
if Se.prev["semestre_id"] >= 0:
|
||||
TitlePrev = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.prev["semestre_id"])
|
||||
else:
|
||||
prev_title = "Prec."
|
||||
TitlePrev = "Prec."
|
||||
|
||||
if Se.cur_sem.semestre_id >= 0:
|
||||
cur_title = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.cur_sem.semestre_id)
|
||||
if Se.sem["semestre_id"] >= 0:
|
||||
TitleCur = "%s%d" % (Se.parcours.SESSION_ABBRV, Se.sem["semestre_id"])
|
||||
else:
|
||||
cur_title = Se.parcours.SESSION_NAME
|
||||
TitleCur = Se.parcours.SESSION_NAME
|
||||
|
||||
H = [
|
||||
'<tr class="%s titles"><th class="sfv_subtitle">%s</em></th>'
|
||||
% (trclass, subtitle)
|
||||
]
|
||||
if Se.prev_formsemestre:
|
||||
H.append(f"<th>Code {prev_title}</th>")
|
||||
H.append(f"<th>Code {cur_title}</th><th>Devenir</th></tr>")
|
||||
if Se.prev:
|
||||
H.append("<th>Code %s</th>" % TitlePrev)
|
||||
H.append("<th>Code %s</th><th>Devenir</th></tr>" % TitleCur)
|
||||
for ch in choices:
|
||||
H.append(
|
||||
"""<tr class="%s"><td title="règle %s"><input type="radio" name="codechoice" value="%s" onClick="document.getElementById('subut').disabled=false;">"""
|
||||
% (trclass, ch.rule_id, ch.codechoice)
|
||||
)
|
||||
H.append("%s </input></td>" % ch.explication)
|
||||
if Se.prev_formsemestre:
|
||||
if Se.prev:
|
||||
H.append('<td class="centercell">%s</td>' % _dispcode(ch.new_code_prev))
|
||||
H.append(
|
||||
'<td class="centercell">%s</td><td>%s</td>'
|
||||
@ -538,6 +535,7 @@ def formsemestre_recap_parcours_table(
|
||||
etudid,
|
||||
with_links=False,
|
||||
with_all_columns=True,
|
||||
a_url="",
|
||||
sem_info=None,
|
||||
show_details=False,
|
||||
):
|
||||
@ -578,14 +576,14 @@ def formsemestre_recap_parcours_table(
|
||||
H.append("<th></th></tr>")
|
||||
|
||||
num_sem = 0
|
||||
for formsemestre in situation_etud_cursus.formsemestres:
|
||||
is_prev = situation_etud_cursus.prev_formsemestre and (
|
||||
situation_etud_cursus.prev_formsemestre.id == formsemestre.id
|
||||
for sem in situation_etud_cursus.get_semestres():
|
||||
is_prev = situation_etud_cursus.prev and (
|
||||
situation_etud_cursus.prev["formsemestre_id"] == sem["formsemestre_id"]
|
||||
)
|
||||
is_cur = situation_etud_cursus.formsemestre_id == formsemestre.id
|
||||
is_cur = situation_etud_cursus.formsemestre_id == sem["formsemestre_id"]
|
||||
num_sem += 1
|
||||
|
||||
dpv = sco_pv_dict.dict_pvjury(formsemestre.id, etudids=[etudid])
|
||||
dpv = sco_pv_dict.dict_pvjury(sem["formsemestre_id"], etudids=[etudid])
|
||||
pv = dpv["decisions"][0]
|
||||
decision_sem = pv["decision_sem"]
|
||||
decisions_ue = pv["decisions_ue"]
|
||||
@ -594,6 +592,7 @@ def formsemestre_recap_parcours_table(
|
||||
else:
|
||||
ass = ""
|
||||
|
||||
formsemestre = db.session.get(FormSemestre, sem["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
if is_cur:
|
||||
type_sem = "*" # now unused
|
||||
@ -604,24 +603,20 @@ def formsemestre_recap_parcours_table(
|
||||
else:
|
||||
type_sem = ""
|
||||
class_sem = "sem_autre"
|
||||
if (
|
||||
formsemestre.formation.formation_code
|
||||
!= situation_etud_cursus.formation.formation_code
|
||||
):
|
||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
||||
class_sem += " sem_autre_formation"
|
||||
bgcolor = (
|
||||
formsemestre.bul_bgcolor
|
||||
if formsemestre.bul_bgcolor
|
||||
else "background-color: rgb(255,255,240)"
|
||||
)
|
||||
if sem["bul_bgcolor"]:
|
||||
bgcolor = sem["bul_bgcolor"]
|
||||
else:
|
||||
bgcolor = "background-color: rgb(255,255,240)"
|
||||
# 1ere ligne: titre sem, decision, acronymes UE
|
||||
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, formsemestre.id))
|
||||
H.append('<tr class="%s rcp_l1 sem_%s">' % (class_sem, sem["formsemestre_id"]))
|
||||
if is_cur:
|
||||
pm = ""
|
||||
elif is_prev:
|
||||
pm = minuslink % formsemestre.id
|
||||
pm = minuslink % sem["formsemestre_id"]
|
||||
else:
|
||||
pm = plusminus % formsemestre.id
|
||||
pm = plusminus % sem["formsemestre_id"]
|
||||
|
||||
inscr = formsemestre.etuds_inscriptions.get(etudid)
|
||||
parcours_name = ""
|
||||
@ -643,12 +638,9 @@ def formsemestre_recap_parcours_table(
|
||||
H.append(
|
||||
f"""
|
||||
<td class="rcp_type_sem" style="background-color:{bgcolor};">{num_sem}{pm}</td>
|
||||
<td class="datedebut">{formsemestre.mois_debut()}</td>
|
||||
<td class="datedebut">{sem['mois_debut']}</td>
|
||||
<td class="rcp_titre_sem"><a class="formsemestre_status_link"
|
||||
href="{
|
||||
url_for("notes.formsemestre_bulletinetud", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id, etudid=etudid
|
||||
)}"
|
||||
href="{a_url}formsemestre_bulletinetud?formsemestre_id={formsemestre.id}&etudid={etudid}"
|
||||
title="Bulletin de notes">{formsemestre.titre_annee()}{parcours_name}</a>
|
||||
"""
|
||||
)
|
||||
@ -683,7 +675,7 @@ def formsemestre_recap_parcours_table(
|
||||
ues = [
|
||||
ue
|
||||
for ue in ues
|
||||
if etud_est_inscrit_ue(cnx, etudid, formsemestre.id, ue.id)
|
||||
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue.id)
|
||||
or etud_ue_status[ue.id]["is_capitalized"]
|
||||
]
|
||||
|
||||
@ -705,7 +697,7 @@ def formsemestre_recap_parcours_table(
|
||||
H.append("<td></td>")
|
||||
H.append("</tr>")
|
||||
# 2eme ligne: notes
|
||||
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{formsemestre.id}">""")
|
||||
H.append(f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">""")
|
||||
H.append(
|
||||
f"""<td class="rcp_type_sem"
|
||||
style="background-color:{bgcolor};"> </td>"""
|
||||
@ -714,28 +706,21 @@ def formsemestre_recap_parcours_table(
|
||||
default_sem_info = '<span class="fontred">[sem. précédent]</span>'
|
||||
else:
|
||||
default_sem_info = ""
|
||||
if not formsemestre.etat: # locked
|
||||
if not sem["etat"]: # locked
|
||||
lockicon = scu.icontag("lock32_img", title="verrouillé", border="0")
|
||||
default_sem_info += lockicon
|
||||
if (
|
||||
formsemestre.formation.formation_code
|
||||
!= situation_etud_cursus.formation.formation_code
|
||||
):
|
||||
default_sem_info += (
|
||||
f"""Autre formation: {formsemestre.formation.formation_code}"""
|
||||
)
|
||||
if sem["formation_code"] != situation_etud_cursus.formation.formation_code:
|
||||
default_sem_info += f"""Autre formation: {sem["formation_code"]}"""
|
||||
H.append(
|
||||
'<td class="datefin">%s</td><td class="sem_info">%s</td>'
|
||||
% (formsemestre.mois_fin(), sem_info.get(formsemestre.id, default_sem_info))
|
||||
% (sem["mois_fin"], sem_info.get(sem["formsemestre_id"], default_sem_info))
|
||||
)
|
||||
# Moy Gen (sous le code decision)
|
||||
H.append(
|
||||
f"""<td class="rcp_moy">{scu.fmt_note(nt.get_etud_moy_gen(etudid))}</td>"""
|
||||
)
|
||||
# Absences (nb d'abs non just. dans ce semestre)
|
||||
nbabsnj = sco_assiduites.formsemestre_get_assiduites_count(
|
||||
etudid, formsemestre
|
||||
)[0]
|
||||
nbabsnj = sco_assiduites.get_assiduites_count(etudid, sem)[0]
|
||||
H.append(f"""<td class="rcp_abs">{nbabsnj}</td>""")
|
||||
|
||||
# UEs
|
||||
@ -782,30 +767,26 @@ def formsemestre_recap_parcours_table(
|
||||
H.append("<td></td>")
|
||||
if with_links:
|
||||
H.append(
|
||||
f"""<td><a class="stdlink" href="{
|
||||
url_for("notes.formsemestre_validation_etud_form", scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id, etudid=etudid
|
||||
)}">modifier</a></td>"""
|
||||
'<td><a href="%sformsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s">modifier</a></td>'
|
||||
% (a_url, sem["formsemestre_id"], etudid)
|
||||
)
|
||||
|
||||
H.append("</tr>")
|
||||
# 3eme ligne: ECTS
|
||||
if (
|
||||
sco_preferences.get_preference("bul_show_ects", formsemestre.id)
|
||||
sco_preferences.get_preference("bul_show_ects", sem["formsemestre_id"])
|
||||
or nt.parcours.ECTS_ONLY
|
||||
):
|
||||
etud_ects_infos = nt.get_etud_ects_pot(etudid) # ECTS potentiels
|
||||
H.append(
|
||||
f"""<tr class="{class_sem} rcp_l2 sem_{formsemestre.id}">
|
||||
f"""<tr class="{class_sem} rcp_l2 sem_{sem["formsemestre_id"]}">
|
||||
<td class="rcp_type_sem" style="background-color:{bgcolor};"> </td>
|
||||
<td></td>"""
|
||||
)
|
||||
# Total ECTS (affiché sous la moyenne générale)
|
||||
H.append(
|
||||
f"""<td class="sem_ects_tit"><a title="crédit acquis">ECTS:</a></td>
|
||||
<td class="sem_ects">{
|
||||
pv.get("sum_ects",0):2.3g} / {etud_ects_infos["ects_total"]:2.3g}
|
||||
</td>
|
||||
<td class="sem_ects">{pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}</td>
|
||||
<td class="rcp_abs"></td>
|
||||
"""
|
||||
)
|
||||
@ -817,7 +798,7 @@ def formsemestre_recap_parcours_table(
|
||||
ects_pot = ue_status["ects_pot"]
|
||||
H.append(
|
||||
f"""<td class="ue"
|
||||
title="{ects:2.3g}/{ects_pot:2.3g} ECTS">{ects:2.3g}</td>"""
|
||||
title="{ects:2.2g}/{ects_pot:2.2g} ECTS">{ects:2.2g}</td>"""
|
||||
)
|
||||
else:
|
||||
H.append("""<td class="ue"></td>""")
|
||||
@ -884,7 +865,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
|
||||
# précédent n'est pas géré dans ScoDoc (code ADC_)
|
||||
# log(str(Se.sems))
|
||||
for sem in Se.sems:
|
||||
if sem["formsemestre_id"] in Se.can_compensate:
|
||||
if sem["can_compensate"]:
|
||||
H.append(
|
||||
'<option value="%s_%s">Admis par compensation avec S%s (%s)</option>'
|
||||
% (
|
||||
@ -901,7 +882,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
|
||||
H.append("</select></td></tr>")
|
||||
|
||||
# Choix code semestre precedent:
|
||||
if Se.prev_formsemestre:
|
||||
if Se.prev:
|
||||
H.append(
|
||||
'<tr><td>Code semestre précédent: </td><td><select name="new_code_prev"><option value="">Choisir une décision...</option>'
|
||||
)
|
||||
@ -994,7 +975,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
||||
conflicts = [] # liste des etudiants avec decision differente déjà saisie
|
||||
with sco_cache.DeferredSemCacheManager():
|
||||
for etudid in etudids:
|
||||
etud = Identite.get_etud(etudid)
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
ins = sco_formsemestre_inscriptions.do_formsemestre_inscription_list(
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id}
|
||||
@ -1003,7 +984,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
||||
# Conditions pour validation automatique:
|
||||
if ins["etat"] == scu.INSCRIT and (
|
||||
(
|
||||
(not Se.prev_formsemestre)
|
||||
(not Se.prev)
|
||||
or (
|
||||
Se.prev_decision and Se.prev_decision["code"] in (ADM, ADC, ADJ)
|
||||
)
|
||||
@ -1074,8 +1055,8 @@ def do_formsemestre_validation_auto(formsemestre_id):
|
||||
f"""<li><a href="{
|
||||
url_for('notes.formsemestre_validation_etud_form',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id,
|
||||
etudid=etud.id, check=1)
|
||||
}">{etud.nom_prenom()}</li>"""
|
||||
etudid=etud["etudid"], check=1)
|
||||
}">{etud["nomprenom"]}</li>"""
|
||||
)
|
||||
H.append("</ul>")
|
||||
H.append(
|
||||
@ -1248,7 +1229,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_validations_html(etud, formsemestre)}
|
||||
{_get_etud_ue_cap_html(etud, formsemestre)}
|
||||
|
||||
<div class="scobox">
|
||||
<div class="scobox-title">
|
||||
@ -1299,7 +1280,7 @@ def formsemestre_validate_previous_ue(formsemestre: FormSemestre, etud: Identite
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def _get_etud_ue_validations_html(etud: Identite, formsemestre: FormSemestre) -> str:
|
||||
def _get_etud_ue_cap_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é.
|
||||
"""
|
||||
@ -1318,13 +1299,39 @@ def _get_etud_ue_validations_html(etud: Identite, formsemestre: FormSemestre) ->
|
||||
|
||||
if not validations:
|
||||
return ""
|
||||
return render_template(
|
||||
"jury/ue_list_etud_validations.j2",
|
||||
edit_mode=True,
|
||||
etud=etud,
|
||||
titre_boite="Validations d'UEs dans cette formation",
|
||||
validations=validations,
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
def do_formsemestre_validate_previous_ue(
|
||||
@ -1364,11 +1371,12 @@ def do_formsemestre_validate_previous_ue(
|
||||
is_external=True,
|
||||
)
|
||||
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="formsemestre_validate_previous_ue",
|
||||
etudid=etudid,
|
||||
msg=f"Validation UE prec. {ue_id} {ue.acronyme}: {code}",
|
||||
commit=True,
|
||||
commit=False,
|
||||
)
|
||||
_invalidate_etud_formation_caches(etudid, formsemestre.formation_id)
|
||||
cnx.commit()
|
||||
|
@ -44,7 +44,9 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre, Identite, Scolog
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.groups import GroupDescr, Partition
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
@ -464,9 +466,9 @@ def etud_add_group_infos(
|
||||
etud['groupes'] = "TDB, Gr2, TPB1"
|
||||
etud['partitionsgroupes'] = "Groupes TD:TDB, Groupes TP:Gr2 (...)"
|
||||
"""
|
||||
etud["partitions"] = (
|
||||
collections.OrderedDict()
|
||||
) # partition_id : group + partition_name
|
||||
etud[
|
||||
"partitions"
|
||||
] = collections.OrderedDict() # partition_id : group + partition_name
|
||||
if not formsemestre_id:
|
||||
etud["groupes"] = ""
|
||||
return etud
|
||||
@ -767,12 +769,12 @@ groupsToDelete={groupsToDelete}
|
||||
{"etudid": etudid, "group_id": group_id},
|
||||
cursor=cursor,
|
||||
)
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="removeFromGroup",
|
||||
etudid=etudid,
|
||||
msg=f"""formsemestre_id={partition.formsemestre.id},partition_name={
|
||||
partition.partition_name}, group_name={group.group_name}""",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# Supprime les groupes indiqués comme supprimés:
|
||||
@ -1407,17 +1409,21 @@ def groups_auto_repartition(partition: Partition):
|
||||
return flask.redirect(dest_url)
|
||||
|
||||
|
||||
def _get_prev_moy(etudid: int, formsemestre_id: int) -> float | str:
|
||||
def _get_prev_moy(etudid, formsemestre_id):
|
||||
"""Donne la derniere moyenne generale calculee pour cette étudiant,
|
||||
ou 0 si on n'en trouve pas (nouvel inscrit,...).
|
||||
"""
|
||||
etud = Identite.get_etud(etudid)
|
||||
info = sco_etud.get_etud_info(etudid=etudid, filled=True)
|
||||
if not info:
|
||||
raise ScoValueError("etudiant invalide: etudid=%s" % etudid)
|
||||
etud = info[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
if Se.prev_formsemestre:
|
||||
prev_sem = db.session.get(FormSemestre, Se.prev_formsemestre.id)
|
||||
if Se.prev:
|
||||
prev_sem = db.session.get(FormSemestre, Se.prev["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(prev_sem)
|
||||
return nt.get_etud_moy_gen(etud.id)
|
||||
return 0.0
|
||||
return nt.get_etud_moy_gen(etudid)
|
||||
else:
|
||||
return 0.0
|
||||
|
||||
|
||||
def create_etapes_partition(formsemestre_id, partition_name="apo_etapes"):
|
||||
@ -1470,7 +1476,7 @@ def do_evaluation_listeetuds_groups(
|
||||
include_demdef: bool = False,
|
||||
) -> list[tuple[int, str]]:
|
||||
"""Donne la liste non triée des etudids inscrits à cette évaluation dans les
|
||||
groupes indiqués (donc inscrits au modimpl ET au formsemestre).
|
||||
groupes indiqués.
|
||||
Si getallstudents==True, donne tous les étudiants inscrits à cette
|
||||
evaluation.
|
||||
Si include_demdef, compte aussi les etudiants démissionnaires et défaillants
|
||||
|
@ -27,8 +27,10 @@
|
||||
|
||||
"""Exports groupes
|
||||
"""
|
||||
from flask import request
|
||||
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_groups_view
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
@ -81,7 +83,9 @@ def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
|
||||
"date_str": "Date",
|
||||
"comment": "Annotation",
|
||||
},
|
||||
origin=f"Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}",
|
||||
origin="Généré par %s le " % sco_version.SCONAME
|
||||
+ scu.timedate_human_repr()
|
||||
+ "",
|
||||
page_title=f"Annotations sur les étudiants de {groups_infos.groups_titles}",
|
||||
caption="Annotations",
|
||||
base_url=groups_infos.base_url,
|
||||
|
@ -39,10 +39,9 @@ from flask import url_for, g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_assiduites as scass
|
||||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
@ -748,6 +747,8 @@ def groups_table(
|
||||
tab.html(),
|
||||
f"""
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=xlsappel">Feuille d'appel Excel</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a>
|
||||
@ -861,25 +862,21 @@ def groups_table(
|
||||
# et ajoute infos inscription
|
||||
for m in groups_infos.members:
|
||||
etud_info = sco_etud.get_etud_info(m["etudid"], filled=True)[0]
|
||||
# TODO utiliser Identite
|
||||
etud = Identite.get_etud(m["etudid"])
|
||||
m.update(etud_info)
|
||||
sco_etud.etud_add_lycee_infos(etud_info)
|
||||
# et ajoute le parcours
|
||||
Se = sco_cursus.get_situation_etud_cursus(
|
||||
etud, groups_infos.formsemestre_id
|
||||
etud_info, groups_infos.formsemestre_id
|
||||
)
|
||||
m["parcours"] = Se.get_cursus_descr()
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(
|
||||
etud.id, formsemestres=etud.get_formsemestres()
|
||||
etud_info["etudid"], sems=etud_info["sems"]
|
||||
)
|
||||
# TODO utiliser Identite:
|
||||
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
|
||||
title = f"etudiants_{groups_infos.groups_filename}"
|
||||
title = "etudiants_%s" % groups_infos.groups_filename
|
||||
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
|
||||
return scu.send_file(
|
||||
xls, filename=title, suffix=scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE
|
||||
)
|
||||
filename = title
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
|
||||
else:
|
||||
raise ScoValueError("unsupported format")
|
||||
|
||||
@ -893,51 +890,29 @@ def tab_absences_html(groups_infos, etat=None):
|
||||
|
||||
group_ids: str = ",".join(map(str, groups_infos.group_ids))
|
||||
formsemestre: FormSemestre = groups_infos.get_formsemestre()
|
||||
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
|
||||
|
||||
liens_abs: list = [
|
||||
'<ul class="ul_abs">',
|
||||
"<li>",
|
||||
form_choix_saisie_semaine(groups_infos), # Ajout Le Havre
|
||||
"</li>",
|
||||
"<li>",
|
||||
form_choix_jour_saisie_hebdo(groups_infos),
|
||||
"</li>",
|
||||
f"""<li><a class="stdlink" href="{
|
||||
H.extend(
|
||||
[
|
||||
"<h3>Assiduité</h3>",
|
||||
'<ul class="ul_abs">',
|
||||
"<li>",
|
||||
form_choix_saisie_semaine(groups_infos), # Ajout Le Havre
|
||||
"</li>",
|
||||
"<li>",
|
||||
form_choix_jour_saisie_hebdo(groups_infos),
|
||||
"</li>",
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept,
|
||||
group_ids=group_ids,
|
||||
date_debut=formsemestre.date_debut.isoformat(),
|
||||
date_fin=formsemestre.date_fin.isoformat()
|
||||
)
|
||||
}">État de l'assiduité du groupe</a></li>""",
|
||||
"</ul>",
|
||||
]
|
||||
|
||||
if disable_abs:
|
||||
liens_abs = [
|
||||
f"""
|
||||
<div class="scobox" style="width:fit-content; font-style:italic;">
|
||||
La gestion des absences est désactivée dans ScoDoc pour ce semestre:
|
||||
{disable_abs}
|
||||
</div>
|
||||
"""
|
||||
]
|
||||
|
||||
url_feuille_appel: str = url_for(
|
||||
"scolar.formulaire_feuille_appel",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=groups_infos.formsemestre_id,
|
||||
group_ids=group_ids,
|
||||
)
|
||||
|
||||
H.extend(
|
||||
[
|
||||
"<h3>Assiduité</h3>",
|
||||
*liens_abs,
|
||||
"</ul>",
|
||||
"<h3>Feuilles</h3>",
|
||||
'<ul class="ul_feuilles">',
|
||||
"""<li><a class="stdlink" href="%s">Feuille d'émargement %s (Excel)</a></li>"""
|
||||
% (url_feuille_appel, groups_infos.groups_titles),
|
||||
"""<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d'émargement %s (Excel)</a></li>"""
|
||||
% (groups_infos.base_url, groups_infos.groups_titles),
|
||||
"""<li><a class="stdlink" href="trombino?%s&fmt=pdf">Trombinoscope en PDF</a></li>"""
|
||||
% groups_infos.groups_query_args,
|
||||
"""<li><a class="stdlink" href="pdf_trombino_tours?%s&fmt=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>"""
|
||||
|
@ -28,6 +28,7 @@
|
||||
""" Importation des étudiants à partir de fichiers CSV
|
||||
"""
|
||||
|
||||
import collections
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
@ -63,7 +64,6 @@ import app.scodoc.sco_utils as scu
|
||||
FORMAT_FILE = "format_import_etudiants.txt"
|
||||
|
||||
# Champs modifiables via "Import données admission"
|
||||
# (nom/prénom modifiables en mode "avec etudid")
|
||||
ADMISSION_MODIFIABLE_FIELDS = (
|
||||
"code_nip",
|
||||
"code_ine",
|
||||
@ -132,27 +132,19 @@ def sco_import_format(with_codesemestre=True):
|
||||
return r
|
||||
|
||||
|
||||
def sco_import_format_dict(with_codesemestre=True, use_etudid: bool = False):
|
||||
def sco_import_format_dict(with_codesemestre=True):
|
||||
"""Attribut: { 'type': , 'table', 'allow_nulls' , 'description' }"""
|
||||
fmt = sco_import_format(with_codesemestre=with_codesemestre)
|
||||
formats = {}
|
||||
R = collections.OrderedDict()
|
||||
for l in fmt:
|
||||
formats[l[0]] = {
|
||||
R[l[0]] = {
|
||||
"type": l[1],
|
||||
"table": l[2],
|
||||
"allow_nulls": l[3],
|
||||
"description": l[4],
|
||||
"aliases": l[5],
|
||||
}
|
||||
if use_etudid:
|
||||
formats["etudid"] = {
|
||||
"type": "int",
|
||||
"table": "identite",
|
||||
"allow_nulls": False,
|
||||
"description": "",
|
||||
"aliases": ["etudid", "id"],
|
||||
}
|
||||
return formats
|
||||
return R
|
||||
|
||||
|
||||
def sco_import_generate_excel_sample(
|
||||
@ -196,7 +188,8 @@ def sco_import_generate_excel_sample(
|
||||
groups_infos = sco_groups_view.DisplayedGroupsInfos(group_ids)
|
||||
members = groups_infos.members
|
||||
log(
|
||||
f"sco_import_generate_excel_sample: group_ids={group_ids}, {len(members)} members"
|
||||
"sco_import_generate_excel_sample: group_ids=%s %d members"
|
||||
% (group_ids, len(members))
|
||||
)
|
||||
titles = ["etudid"] + titles
|
||||
titles_styles = [style] + titles_styles
|
||||
@ -241,26 +234,21 @@ def students_import_excel(
|
||||
exclude_cols=["photo_filename"],
|
||||
)
|
||||
if return_html:
|
||||
dest_url = (
|
||||
url_for(
|
||||
if formsemestre_id:
|
||||
dest = url_for(
|
||||
"notes.formsemestre_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
if formsemestre_id
|
||||
else url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
)
|
||||
else:
|
||||
dest = url_for("notes.index_html", scodoc_dept=g.scodoc_dept)
|
||||
H = [html_sco_header.sco_header(page_title="Import etudiants")]
|
||||
H.append("<ul>")
|
||||
for d in diag:
|
||||
H.append(f"<li>{d}</li>")
|
||||
H.append(
|
||||
f"""
|
||||
</ul>)
|
||||
<p>Import terminé !</p>
|
||||
<p><a class="stdlink" href="{dest_url}">Continuer</a></p>
|
||||
"""
|
||||
)
|
||||
H.append("<li>%s</li>" % d)
|
||||
H.append("</ul>")
|
||||
H.append("<p>Import terminé !</p>")
|
||||
H.append('<p><a class="stdlink" href="%s">Continuer</a></p>' % dest)
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
@ -320,13 +308,13 @@ def scolars_import_excel_file(
|
||||
titleslist = []
|
||||
for t in fs:
|
||||
if t not in titles:
|
||||
raise ScoValueError(f'Colonne invalide: "{t}"')
|
||||
raise ScoValueError('Colonne invalide: "%s"' % t)
|
||||
titleslist.append(t) #
|
||||
# ok, same titles
|
||||
# Start inserting data, abort whole transaction in case of error
|
||||
created_etudids = []
|
||||
np_imported_homonyms = 0
|
||||
group_id_inferer = {}
|
||||
GroupIdInferers = {}
|
||||
try: # --- begin DB transaction
|
||||
linenum = 0
|
||||
for line in data[1:]:
|
||||
@ -441,7 +429,7 @@ def scolars_import_excel_file(
|
||||
_import_one_student(
|
||||
formsemestre_id,
|
||||
values,
|
||||
group_id_inferer,
|
||||
GroupIdInferers,
|
||||
annee_courante,
|
||||
created_etudids,
|
||||
linenum,
|
||||
@ -508,14 +496,13 @@ def scolars_import_excel_file(
|
||||
|
||||
|
||||
def students_import_admission(
|
||||
csvfile, type_admission="", formsemestre_id=None, return_html=True, use_etudid=False
|
||||
) -> str:
|
||||
csvfile, type_admission="", formsemestre_id=None, return_html=True
|
||||
):
|
||||
"import donnees admission from Excel file (v2016)"
|
||||
diag = scolars_import_admission(
|
||||
csvfile,
|
||||
formsemestre_id=formsemestre_id,
|
||||
type_admission=type_admission,
|
||||
use_etudid=use_etudid,
|
||||
)
|
||||
if return_html:
|
||||
H = [html_sco_header.sco_header(page_title="Import données admissions")]
|
||||
@ -537,7 +524,6 @@ def students_import_admission(
|
||||
)
|
||||
|
||||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
return ""
|
||||
|
||||
|
||||
def _import_one_student(
|
||||
@ -613,15 +599,13 @@ def _is_new_ine(cnx, code_ine):
|
||||
|
||||
|
||||
# ------ Fonction ré-écrite en nov 2016 pour lire des fichiers sans etudid (fichiers APB)
|
||||
def scolars_import_admission(
|
||||
datafile, formsemestre_id=None, type_admission=None, use_etudid=False
|
||||
):
|
||||
def scolars_import_admission(datafile, formsemestre_id=None, type_admission=None):
|
||||
"""Importe données admission depuis un fichier Excel quelconque
|
||||
par exemple ceux utilisés avec APB, avec ou sans etudid
|
||||
par exemple ceux utilisés avec APB
|
||||
|
||||
Cherche dans ce fichier les étudiants qui correspondent à des inscrits du
|
||||
semestre formsemestre_id.
|
||||
Si le fichier n'a pas d'etudid (use_etudid faux), la correspondance se fait
|
||||
Le fichier n'a pas l'INE ni le NIP ni l'etudid, la correspondance se fait
|
||||
via les noms/prénoms qui doivent être égaux (la casse, les accents et caractères spéciaux
|
||||
étant ignorés).
|
||||
|
||||
@ -633,24 +617,23 @@ def scolars_import_admission(
|
||||
dans le fichier importé) du champ type_admission.
|
||||
Si une valeur existe ou est présente dans le fichier importé, ce paramètre est ignoré.
|
||||
|
||||
TODO:
|
||||
- choix onglet du classeur
|
||||
"""
|
||||
|
||||
log(f"scolars_import_admission: formsemestre_id={formsemestre_id}")
|
||||
diag: list[str] = []
|
||||
members = sco_groups.get_group_members(
|
||||
sco_groups.get_default_group(formsemestre_id)
|
||||
)
|
||||
etuds_by_nomprenom = {} # { nomprenom : etud }
|
||||
etuds_by_etudid = {} # { etudid : etud }
|
||||
if use_etudid:
|
||||
etuds_by_etudid = {m["etudid"]: m for m in members}
|
||||
else:
|
||||
for m in members:
|
||||
np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
|
||||
if np in etuds_by_nomprenom:
|
||||
msg = f"""Attention: hononymie pour {m["nom"]} {m["prenom"]}"""
|
||||
log(msg)
|
||||
diag.append(msg)
|
||||
etuds_by_nomprenom[np] = m
|
||||
diag = []
|
||||
for m in members:
|
||||
np = (adm_normalize_string(m["nom"]), adm_normalize_string(m["prenom"]))
|
||||
if np in etuds_by_nomprenom:
|
||||
msg = "Attention: hononymie pour %s %s" % (m["nom"], m["prenom"])
|
||||
log(msg)
|
||||
diag.append(msg)
|
||||
etuds_by_nomprenom[np] = m
|
||||
|
||||
exceldata = datafile.read()
|
||||
diag2, data = sco_excel.excel_bytes_to_list(exceldata)
|
||||
@ -661,29 +644,19 @@ def scolars_import_admission(
|
||||
|
||||
titles = data[0]
|
||||
# idx -> ('field', convertor)
|
||||
fields = adm_get_fields(titles, formsemestre_id, use_etudid=use_etudid)
|
||||
idx_nom = idx_prenom = idx_etudid = None
|
||||
fields = adm_get_fields(titles, formsemestre_id)
|
||||
idx_nom = None
|
||||
idx_prenom = None
|
||||
for idx, field in fields.items():
|
||||
match field[0]:
|
||||
case "nom":
|
||||
idx_nom = idx
|
||||
case "prenom":
|
||||
idx_prenom = idx
|
||||
case "etudid":
|
||||
idx_etudid = idx
|
||||
|
||||
if (not use_etudid and ((idx_nom is None) or (idx_prenom is None))) or (
|
||||
use_etudid and idx_etudid is None
|
||||
):
|
||||
if field[0] == "nom":
|
||||
idx_nom = idx
|
||||
if field[0] == "prenom":
|
||||
idx_prenom = idx
|
||||
if (idx_nom is None) or (idx_prenom is None):
|
||||
log("fields indices=" + ", ".join([str(x) for x in fields]))
|
||||
log("fields titles =" + ", ".join([x[0] for x in fields.values()]))
|
||||
log("fields titles =" + ", ".join([fields[x][0] for x in fields]))
|
||||
raise ScoFormatError(
|
||||
(
|
||||
"""colonne etudid requise
|
||||
(si l'option "Utiliser l'identifiant d'étudiant ScoDoc" est cochée)"""
|
||||
if use_etudid
|
||||
else "colonnes nom et prenom requises"
|
||||
),
|
||||
"scolars_import_admission: colonnes nom et prenom requises",
|
||||
dest_url=url_for(
|
||||
"scolar.form_students_import_infos_admissions",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
@ -692,31 +665,18 @@ def scolars_import_admission(
|
||||
)
|
||||
|
||||
modifiable_fields = set(ADMISSION_MODIFIABLE_FIELDS)
|
||||
if use_etudid:
|
||||
modifiable_fields |= {"nom", "prenom"}
|
||||
|
||||
nline = 2 # la premiere ligne de donnees du fichier excel est 2
|
||||
n_import = 0
|
||||
for line in data[1:]:
|
||||
if use_etudid:
|
||||
try:
|
||||
etud = etuds_by_etudid.get(int(line[idx_etudid]))
|
||||
except ValueError:
|
||||
etud = None
|
||||
if not etud:
|
||||
msg = f"""Étudiant avec code etudid=<b>{line[idx_etudid]}</b> inexistant"""
|
||||
diag.append(msg)
|
||||
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
|
||||
nom = adm_normalize_string(line[idx_nom])
|
||||
prenom = adm_normalize_string(line[idx_prenom])
|
||||
if (nom, prenom) not in etuds_by_nomprenom:
|
||||
msg = f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]} inexistant</b>"""
|
||||
diag.append(msg)
|
||||
else:
|
||||
# Retrouve l'étudiant parmi ceux du semestre par (nom, prenom)
|
||||
nom = adm_normalize_string(line[idx_nom])
|
||||
prenom = adm_normalize_string(line[idx_prenom])
|
||||
etud = etuds_by_nomprenom.get((nom, prenom))
|
||||
if not etud:
|
||||
msg = (
|
||||
f"""Étudiant <b>{line[idx_nom]} {line[idx_prenom]}</b> inexistant"""
|
||||
)
|
||||
diag.append(msg)
|
||||
if etud:
|
||||
etud = etuds_by_nomprenom[(nom, prenom)]
|
||||
cur_adm = sco_etud.admission_list(cnx, args={"id": etud["admission_id"]})[0]
|
||||
# peuple les champs presents dans le tableau
|
||||
args = {}
|
||||
@ -767,16 +727,15 @@ def scolars_import_admission(
|
||||
)
|
||||
|
||||
for group_id in group_ids:
|
||||
group: GroupDescr = GroupDescr.get_instance(group_id)
|
||||
group = db.session.get(GroupDescr, group_id)
|
||||
if group.partition.groups_editable:
|
||||
sco_groups.change_etud_group_in_partition(
|
||||
args["etudid"], group
|
||||
)
|
||||
elif not group.partition.is_parcours:
|
||||
else:
|
||||
log("scolars_import_admission: partition non editable")
|
||||
diag.append(
|
||||
f"""Attention: partition {
|
||||
group.partition} (g{group.id}) non editable et ignorée"""
|
||||
f"Attention: partition {group.partition} non editable (ignorée)"
|
||||
)
|
||||
|
||||
#
|
||||
@ -799,19 +758,19 @@ def adm_normalize_string(s):
|
||||
)
|
||||
|
||||
|
||||
def adm_get_fields(titles, formsemestre_id: int, use_etudid: bool = False):
|
||||
def adm_get_fields(titles, formsemestre_id):
|
||||
"""Cherche les colonnes importables dans les titres (ligne 1) du fichier excel
|
||||
return: { idx : (field_name, convertor) }
|
||||
"""
|
||||
format_dict = sco_import_format_dict(use_etudid=use_etudid)
|
||||
format_dict = sco_import_format_dict()
|
||||
fields = {}
|
||||
idx = 0
|
||||
for title in titles:
|
||||
title_n = adm_normalize_string(title)
|
||||
for k, fmt in format_dict.items():
|
||||
for v in fmt["aliases"]:
|
||||
for k in format_dict:
|
||||
for v in format_dict[k]["aliases"]:
|
||||
if adm_normalize_string(v) == title_n:
|
||||
typ = fmt["type"]
|
||||
typ = format_dict[k]["type"]
|
||||
if typ == "real":
|
||||
convertor = adm_convert_real
|
||||
elif typ == "integer" or typ == "int":
|
||||
|
@ -140,7 +140,7 @@ def read_users_excel_file(datafile, titles=TITLES) -> list[dict]:
|
||||
for line in data[1:]:
|
||||
d = {}
|
||||
for i, field in enumerate(xls_titles):
|
||||
d[field] = (line[i] or "").strip()
|
||||
d[field] = line[i]
|
||||
users.append(d)
|
||||
return users
|
||||
|
||||
|
@ -754,4 +754,4 @@ def etuds_select_box_xls(src_cat):
|
||||
table_id="etuds_select_box_xls",
|
||||
titles=titles,
|
||||
)
|
||||
return tab.excel()
|
||||
return tab.excel() # tab.make_page(filename=src_cat["infos"]["filename"])
|
||||
|
@ -30,7 +30,10 @@
|
||||
|
||||
import psycopg2
|
||||
|
||||
from app.models import Scolog
|
||||
from app import db
|
||||
|
||||
from app.models import Formation
|
||||
from app.scodoc import scolog
|
||||
from app.scodoc import sco_cache
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
@ -53,8 +56,7 @@ _moduleimplEditor = ndb.EditableTable(
|
||||
|
||||
def do_moduleimpl_create(args):
|
||||
"create a moduleimpl"
|
||||
# TODO remplacer par une methode de ModuleImpl qui appelle
|
||||
# super().create_from_dict() puis invalide le formsemestre
|
||||
# TODO remplacer par une methode de ModuleImpl qui appelle super().create_from_dict() puis invalide le formsemestre
|
||||
cnx = ndb.GetDBConnexion()
|
||||
r = _moduleimplEditor.create(cnx, args)
|
||||
sco_cache.invalidate_formsemestre(
|
||||
@ -182,11 +184,12 @@ def do_moduleimpl_inscription_create(args, formsemestre_id=None, cnx=None):
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
) # > moduleimpl_inscription
|
||||
Scolog.logdb(
|
||||
scolog.logdb(
|
||||
cnx,
|
||||
method="moduleimpl_inscription",
|
||||
etudid=args["etudid"],
|
||||
msg=f"inscription module {args['moduleimpl_id']}",
|
||||
commit=True,
|
||||
commit=False,
|
||||
)
|
||||
return r
|
||||
|
||||
|
@ -44,8 +44,8 @@ from app.models import (
|
||||
Partition,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
Scolog,
|
||||
)
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_cache
|
||||
@ -79,9 +79,9 @@ def moduleimpl_inscriptions_edit(
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
module = modimpl.module
|
||||
formsemestre = modimpl.formsemestre
|
||||
# -- check permission (and lock)
|
||||
if not modimpl.can_change_inscriptions():
|
||||
return # can_change_inscriptions raises exception
|
||||
# -- check lock
|
||||
if not formsemestre.etat:
|
||||
raise ScoValueError("opération impossible: semestre verrouille")
|
||||
header = html_sco_header.sco_header(
|
||||
page_title="Inscription au module",
|
||||
init_qtip=True,
|
||||
@ -774,11 +774,12 @@ def do_etud_desinscrit_ue_classic(etudid, formsemestre_id, ue_id):
|
||||
""",
|
||||
{"etudid": etudid, "formsemestre_id": formsemestre_id, "ue_id": ue_id},
|
||||
)
|
||||
Scolog.logdb(
|
||||
logdb(
|
||||
cnx,
|
||||
method="etud_desinscrit_ue",
|
||||
etudid=etudid,
|
||||
msg=f"desinscription UE {ue_id}",
|
||||
commit=True,
|
||||
commit=False,
|
||||
)
|
||||
sco_cache.invalidate_formsemestre(
|
||||
formsemestre_id=formsemestre_id
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user