Compare commits

..

3 Commits

Author SHA1 Message Date
Iziram
72d705d90d Merge branch 'offSco' into assiduites_fixes 2024-06-19 19:51:52 +02:00
Iziram
b6d4bbbf98 signal_assiduites_group : fix bug actualisation "jourSuivant" 2024-06-17 06:43:27 +02:00
Iziram
dc3456fc0e signal_assiduites_hebdo : fix typo html 2024-06-16 18:30:36 +02:00
185 changed files with 3999 additions and 10214 deletions

View File

@ -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)

View File

@ -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 &rightarrow; 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 &rightarrow; 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 &rightarrow; 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 &rightarrow; 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 &rightarrow; 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)

View File

@ -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")

View File

@ -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]

View File

@ -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:

View File

@ -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:

View File

@ -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&amp;T",
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 1
}
```
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;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&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;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&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;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&apos;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&apos;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

View File

@ -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:

View File

@ -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"

View File

@ -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 &rightarrow; 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 &rightarrow; 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 &rightarrow; 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é

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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 !

View File

@ -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"

View File

@ -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"))

View File

@ -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>"""
)

View File

@ -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"] = (

View File

@ -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

View File

@ -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>"

View File

@ -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

View File

@ -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
@ -209,10 +207,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 ] }
@ -384,59 +378,27 @@ class FormSemestreCursusBUT:
# "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(

View File

@ -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]:
@ -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)

View File

@ -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,7 @@ 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)
has_diplome = deca.valide_diplome()
row = {
"nom_pv": (
etud.code_ine or etud.code_nip or etud.id
@ -181,12 +173,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": (
@ -202,7 +190,7 @@ def pvjury_table_but(
else ""
)
),
"diplome": diplome_str,
"diplome": "ADM" if has_diplome else "",
# pour exports excel seulement:
"civilite": etud.civilite_etat_civil_str,
"nom": etud.nom,
@ -211,7 +199,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"])

View File

@ -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

View File

@ -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,
)

View File

@ -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),
)

View File

@ -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

View File

@ -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
]

View File

@ -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.

View File

@ -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.

View File

@ -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):

View File

@ -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)

View File

@ -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")

View File

@ -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],

View File

@ -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")

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"),
)

View File

@ -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}>"""
@ -125,7 +120,7 @@ class ApcValidationRCUE(ScoDocModel):
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 +151,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,7 +208,16 @@ 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"]}&nbsp;{niveau["ordre"]}:&nbsp;{
dec_rcue["code"]}"""
)
decisions["descr_decisions_rcue"] = ", ".join(titres_rcues)
decisions["descr_decisions_rcue_list"] = titres_rcues
decisions["descr_decisions_niveaux"] = (
@ -241,112 +243,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}
"""

View File

@ -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

View File

@ -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,

View File

@ -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.
@ -830,7 +818,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 +833,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 +1068,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 +1081,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 +1102,7 @@ itemsuivi_tags_assoc = db.Table(
)
class EtudAnnotation(models.ScoDocModel):
class EtudAnnotation(db.Model):
"""Annotation sur un étudiant"""
__tablename__ = "etud_annotations"
@ -1128,8 +1113,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__)

View File

@ -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)}"
)

View File

@ -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(

View File

@ -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={

View File

@ -123,11 +123,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)
@ -258,7 +256,7 @@ class FormSemestre(models.ScoDocModel):
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"])
args["date_fin"] = scu.convert_fr_date(args["date_debut"])
if "etat" in args:
args["etat"] = bool(args["etat"])
if "bul_bgcolor" in args:
@ -845,11 +843,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 +865,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 +993,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 +1002,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]:

View File

@ -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)
@ -226,11 +225,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 +336,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(

View File

@ -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)}>"
@ -276,39 +274,21 @@ class ModuleImpl(ScoDocModel):
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

View File

@ -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:

View File

@ -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

View File

@ -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}>"

View File

@ -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
@ -123,16 +123,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 +255,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()
@ -396,21 +362,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 +370,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 +378,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 (

View File

@ -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,

View File

@ -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

View File

@ -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
@ -315,11 +313,11 @@ class GenTable:
T.append(l + [self.bottom_titles.get(cid, "") for cid in self.columns_ids])
return T
def get_titles_list(self):
def get_titles_list(self, with_lines_titles=True):
"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"])
if with_lines_titles:
titles.insert(0, "")
return titles
def gen(self, fmt="html", columns_ids=None):
@ -683,15 +681,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 +698,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()

View File

@ -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>"""

View File

@ -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):

View File

@ -221,8 +221,7 @@ class ApoEtud(dict):
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,
col_id}</tt> non déclarée ?"""
) from exc
else:
try:
@ -327,15 +326,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()
@ -356,7 +375,7 @@ class ApoEtud(dict):
if module_code_found:
return VOID_APO_RES
# RCUE du BUT (validations enregistrées seulement, pas avant jury)
# RCUE du BUT
if res.is_apc:
for val_rcue in ApcValidationRCUE.query.filter_by(
etudid=etudid, formsemestre_id=sem["formsemestre_id"]
@ -372,62 +391,6 @@ class ApoEtud(dict):
#
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.
@ -728,8 +691,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 +783,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 +848,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())
@ -950,7 +909,7 @@ class ApoData:
# 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 +931,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

View File

@ -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))

View File

@ -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:

View File

@ -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:

View File

@ -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"]

View 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

View File

@ -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, Identite, ScolarAutorisationInscription, UniteEns
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,6 +109,7 @@ class DecisionSem(object):
class SituationEtudCursus:
"Semestre dans un cursus"
pass
class SituationEtudCursusClassic(SituationEtudCursus):
@ -197,7 +193,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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(
@ -223,9 +219,9 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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 +229,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_idx - 1))
elif devenir == REDOSEM:
return f"Redouble semestre (recommence en {sess_abrv}{s_idx})"
return "Redouble semestre (recommence en %s%s)" % (SA, s_idx)
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_idx - 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_idx,
SA,
s_idx - 1,
)
elif devenir == RS_OR_NEXT:
return f"{passage}, ou semestre {sess_abrv}{s_idx}"
return passage + ", ou semestre %s%s" % (SA, s_idx)
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_idx + 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_idx + 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):
@ -357,7 +359,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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.
@ -368,7 +370,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
for formsemestre in self.formsemestres:
inscription = formsemestre.etuds_inscriptions.get(self.etud.id)
if inscription is None:
return "non inscrit" # !!!
raise ValueError("Etudiant non inscrit au semestre") # bug
if inscription.etat == scu.DEMISSION:
dem = " (dem.)"
else:
@ -454,8 +456,7 @@ class SituationEtudCursusClassic(SituationEtudCursus):
break
if not cur or cur.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
@ -585,11 +586,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(
@ -613,19 +616,20 @@ class SituationEtudCursusClassic(SituationEtudCursus):
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.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(
@ -755,6 +759,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 +937,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()

View File

@ -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":

View File

@ -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

View File

@ -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,
},
),

View File

@ -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)

View File

@ -528,7 +528,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()}
@ -1124,18 +1124,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 +1363,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 +1508,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(f"edit_ue_set_code_apogee: ue_id={ue_id} code_apogee={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)

View File

@ -545,8 +545,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 +778,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"

View File

@ -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

View File

@ -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)"
)
),
),

View File

@ -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

View File

@ -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()

View File

@ -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):

View File

@ -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)

View File

@ -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
)

View File

@ -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
@ -222,7 +219,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 +258,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):

View File

@ -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,

View File

@ -63,6 +63,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 +77,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():
@ -463,17 +464,6 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
},
)
)
modform.append(
(
"elt_passage_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é
},
)
)
if ScoDocSiteConfig.get("edt_ics_path"):
modform.append(
(
@ -1331,8 +1321,6 @@ def do_formsemestre_clone(
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)
@ -1340,10 +1328,7 @@ def do_formsemestre_clone(
# 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

View File

@ -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,
)

View File

@ -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()
@ -809,11 +797,10 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
('aucun étudiant inscrit' if partition_is_empty else '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:
@ -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"}
à&nbsp;:
<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()}
"""

View File

@ -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
# ------------------------------------------------------------------------------------
@ -804,7 +806,7 @@ def formsemestre_recap_parcours_table(
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}
pv.get("sum_ects",0):2.2g} / {etud_ects_infos["ects_total"]:2.2g}
</td>
<td class="rcp_abs"></td>
"""
@ -817,7 +819,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>""")
@ -1075,7 +1077,7 @@ def do_formsemestre_validation_auto(formsemestre_id):
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>"""
}">{etud_d["nomprenom"]}</li>"""
)
H.append("</ul>")
H.append(
@ -1248,7 +1250,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 +1301,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 +1320,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 +1392,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()

View File

@ -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
@ -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:
@ -1470,7 +1472,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

View File

@ -42,7 +42,6 @@ from app import db
from app.models import FormSemestre, Identite
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
@ -586,8 +585,8 @@ def groups_table(
etud_info["_nom_disp_order"] = etud_sort_key(etud_info)
etud_info["_prenom_target"] = fiche_url
etud_info["_nom_disp_td_attrs"] = (
'id="%s" class="etudinfo"' % (etud_info["etudid"])
etud_info["_nom_disp_td_attrs"] = 'id="%s" class="etudinfo"' % (
etud_info["etudid"]
)
etud_info["bourse_str"] = "oui" if etud_info["boursier"] else "non"
if etud_info["etat"] == "D":
@ -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>
@ -893,51 +894,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>"""

View File

@ -767,16 +767,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)"
)
#

View File

@ -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"])

View File

@ -30,7 +30,7 @@
import psycopg2
from app.models import Scolog
from app.scodoc import scolog
from app.scodoc import sco_cache
import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoValueError
@ -182,11 +182,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

View File

@ -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
@ -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

View File

@ -30,7 +30,7 @@
import math
import datetime
from flask import g, render_template, url_for
from flask import g, url_for
from flask_login import current_user
from app import db
@ -40,12 +40,14 @@ from app.comp.res_common import ResultatsSemestre
from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre, Module, ModuleImpl, UniteEns
import app.scodoc.sco_utils as scu
from app.scodoc import sco_assiduites as scass
from app.scodoc.codes_cursus import UE_SPORT
from app.scodoc.sco_exceptions import ScoInvalidIdType
from app.scodoc.sco_cursus_dut import formsemestre_has_decisions
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import htmlutils
from app.scodoc import sco_compute_moy
from app.scodoc import sco_evaluations
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
@ -66,9 +68,6 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
else:
sup_label = "Supprimer évaluation"
formsemestre: FormSemestre = FormSemestre.get_formsemestre(modimpl.formsemestre_id)
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
menu_eval = [
{
"title": "Saisir les notes",
@ -140,8 +139,7 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
),
},
"enabled": evaluation.date_debut is not None
and evaluation.date_fin is not None
and not disable_abs,
and evaluation.date_fin is not None,
},
{
"title": "Vérifier notes vs absents",
@ -149,9 +147,7 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
"args": {
"evaluation_id": evaluation_id,
},
"enabled": nbnotes > 0
and evaluation.date_debut is not None
and not disable_abs,
"enabled": nbnotes > 0 and evaluation.date_debut is not None,
},
]
@ -252,6 +248,11 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
module_resp = db.session.get(User, modimpl.responsable_id)
mod_type_name = scu.MODULE_TYPE_NAMES[module.module_type]
H = [
html_sco_header.sco_header(
page_title=f"{mod_type_name} {module.code} {module.titre}",
javascripts=["js/etud_info.js"],
init_qtip=True,
),
f"""<h2 class="formsemestre">{mod_type_name}
<tt>{module.code}</tt> {module.titre}
{"dans l'UE " + modimpl.module.ue.acronyme
@ -325,28 +326,30 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id
)}">modifier</a>"""
)
H.append(
"""</td></tr>
<tr><td colspan="4"></td></tr>
"""
)
disable_abs: str | bool = scass.has_assiduites_disable_pref(formsemestre)
if not disable_abs:
H.append("</td></tr>")
# Ligne: règle de calcul
has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl)
if has_expression:
H.append(
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
"""<tr>
<td class="fichetitre2" colspan="4">Règle de calcul:
<span class="warning">inutilisée dans cette version de ScoDoc</span>
</td>
</tr>
"""
)
else:
H.append('<tr><td colspan="4">')
H.append("</td></tr>")
H.append(
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
href="{
url_for("notes.view_module_abs", scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">Absences dans ce module</a></span>"""
)
)
# Adapté à partir d'une suggestion de DS (Le Havre)
# Liens saisies absences seulement si permission et date courante dans le semestre
if (
current_user.has_permission(Permission.AbsChange)
and formsemestre.est_courant()
and not disable_abs
):
if current_user.has_permission(Permission.AbsChange) and formsemestre.est_courant():
group_id = sco_groups.get_default_group(formsemestre_id)
H.append(
f"""
@ -472,17 +475,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_evaluation_renumber",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}" title="Ordonner les évaluations par date">Trier par date</a>
}">Trier par date</a>
"""
bot_table_links = (
top_table_links
+ f"""
<a class="stdlink" style="margin-left:2em;" href="{
url_for("notes.moduleimpl_import_notes",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}" title="Charger toutles les notes via tableur">Importer les notes</a>
"""
)
if nb_evaluations > 0:
H.append(
'<div class="moduleimpl_evaluations_top_links">'
@ -517,9 +511,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
if sem_locked:
H.append(f"""{scu.icontag("lock32_img")} semestre verrouillé""")
elif can_edit_evals:
H.append(
f"""<div class="moduleimpl_evaluations_table_bot">{bot_table_links}</div>"""
)
H.append(top_table_links)
H.append(
f"""</td></tr>
@ -558,11 +550,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
</p>
"""
)
return render_template(
"sco_page.j2",
content="".join(H),
title=f"{mod_type_name} {module.code} {module.titre}",
)
H.append(html_sco_header.sco_footer())
return "".join(H)
def _ligne_evaluation(
@ -812,7 +801,7 @@ def _ligne_evaluation(
<td class="rightcell" colspan="2">"""
% etat
)
if etat["nb_notes"]:
if etat["moy"]:
H.append(
f"""<b>{etat["moy"]} / 20</b>
&nbsp; (<a class="stdlink" href="{
@ -862,7 +851,7 @@ def _ligne_evaluation(
)
if gr_moyenne["gr_nb_notes"] > 0:
H.append(
f"""{gr_moyenne["gr_moy"]}&nbsp; (<a class="stdlink" href="{
f"""{gr_moyenne["gr_moy"]}&nbsp; (<a href="{
url_for('notes.evaluation_listenotes',
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id,
tf_submitted=1, **{'group_ids:list': gr_moyenne["group_id"]})
@ -881,7 +870,7 @@ def _ligne_evaluation(
f"""<a class="redlink" href="{url_for('notes.saisie_notes',
scodoc_dept=g.scodoc_dept, evaluation_id=evaluation.id,
**{'group_ids:list': gr_moyenne["group_id"]})
}">incomplet&nbsp;: terminer saisie</a></font>]"""
}">incomplet</a></font>]"""
)
else:
H.append("""incomplet</font>]""")

View File

@ -37,14 +37,7 @@ import sqlalchemy as sa
from app import log
from app.auth.models import User
from app.but import cursus_but, validations_view
from app.models import (
Adresse,
EtudAnnotation,
FormSemestre,
Identite,
ScoDocSiteConfig,
ValidationDUT120,
)
from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import (
codes_cursus,
html_sco_header,
@ -121,14 +114,14 @@ def _menu_scolarite(
"enabled": def_enabled,
},
{
"title": "Désinscrire (en cas d'erreur)",
"endpoint": "notes.formsemestre_desinscription",
"title": "Inscrire à un module optionnel (ou au sport)",
"endpoint": "notes.formsemestre_inscription_option",
"args": args,
"enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
},
{
"title": "Inscrire à un module optionnel (ou au sport)",
"endpoint": "notes.formsemestre_inscription_option",
"title": "Désinscrire (en cas d'erreur)",
"endpoint": "notes.formsemestre_desinscription",
"args": args,
"enabled": authuser.has_permission(Permission.EtudInscrit) and not locked,
},
@ -138,6 +131,12 @@ def _menu_scolarite(
"args": args,
"enabled": formsemestre.can_edit_jury(),
},
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
"args": {"etudid": etudid},
"enabled": authuser.has_permission(Permission.EtudInscrit),
},
{
"title": "Enregistrer un semestre effectué ailleurs",
"endpoint": "notes.formsemestre_ext_create_form",
@ -150,12 +149,6 @@ def _menu_scolarite(
"args": args,
"enabled": authuser.has_permission(Permission.EditAllNotes),
},
{
"title": "Inscrire à un autre semestre",
"endpoint": "notes.formsemestre_inscription_with_modules_form",
"args": {"etudid": etudid},
"enabled": authuser.has_permission(Permission.EtudInscrit),
},
]
return htmlutils.make_menu(
@ -302,32 +295,21 @@ def fiche_etud(etudid=None):
}">Visualiser les compétences BUT</a>
</span>
"""
info["link_inscrire_ailleurs"] = (
f"""<span class="link_bul_pdf"><a class="stdlink" href="{
if current_user.has_permission(Permission.EtudInscrit):
info[
"link_inscrire_ailleurs"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.formsemestre_inscription_with_modules_form",
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Inscrire à un autre semestre</a></span>
"""
if current_user.has_permission(Permission.EtudInscrit)
else ""
)
can_edit_jury = current_user.has_permission(Permission.EtudInscrit)
info[
"link_inscrire_ailleurs"
] += f"""
<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.jury_delete_manual",
scodoc_dept=g.scodoc_dept, etudid=etudid,
read_only=not can_edit_jury)
}">{'Éditer' if can_edit_jury else 'Détail de'} toutes décisions de jury</a></span>
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Éditer toutes décisions de jury</a></span>
"""
info[
"link_bilan_ects"
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
url_for("notes.etud_bilan_ects",
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">ECTS</a></span>"""
else:
info["link_inscrire_ailleurs"] = ""
else:
# non inscrit
l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
@ -342,7 +324,6 @@ def fiche_etud(etudid=None):
info["liste_inscriptions"] = "\n".join(l)
info["link_bul_pdf"] = ""
info["link_inscrire_ailleurs"] = ""
info["link_bilan_ects"] = ""
# Liste des annotations
html_annotations_list = "\n".join(
@ -445,9 +426,7 @@ def fiche_etud(etudid=None):
"inscriptions_mkup"
] = f"""<div class="ficheinscriptions" id="ficheinscriptions">
<div class="fichetitre">Cursus</div>{info["liste_inscriptions"]}
{info["link_bul_pdf"]}
{info["link_inscrire_ailleurs"]}
{info["link_bilan_ects"]}
{info["link_bul_pdf"]} {info["link_inscrire_ailleurs"]}
</div>"""
#
@ -467,10 +446,7 @@ def fiche_etud(etudid=None):
# Liens vers compétences BUT
if last_formsemestre and last_formsemestre.formation.is_apc():
try:
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
except ScoValueError:
but_cursus = None
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
refcomp = last_formsemestre.formation.referentiel_competence
if refcomp:
ue_validation_by_niveau = validations_view.get_ue_validation_by_niveau(
@ -479,20 +455,6 @@ def fiche_etud(etudid=None):
ects_total = sum((v.ects() for v in ue_validation_by_niveau.values()))
else:
ects_total = ""
validation_dut120 = ValidationDUT120.query.filter_by(etudid=etudid).first()
validation_dut120_html = (
f"""Diplôme DUT décerné
en&nbsp; <a class="stdlink" href="{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=validation_dut120.formsemestre.id)
}">S{validation_dut120.formsemestre.semestre_id}</a>
"""
if validation_dut120
else ""
)
info[
"but_cursus_mkup"
] = f"""
@ -501,8 +463,7 @@ def fiche_etud(etudid=None):
"but/cursus_etud.j2",
cursus=but_cursus,
scu=scu,
validation_dut120_html=validation_dut120_html,
) if but_cursus else '<span class="pb-config">problème configuration formation BUT</span>'}
)}
<div class="fiche_but_col2">
<div class="link_validation_rcues">
<a class="stdlink" href="{url_for("notes.validation_rcues",
@ -510,7 +471,7 @@ def fiche_etud(etudid=None):
formsemestre_id=last_formsemestre.id)}"
title="Visualiser les compétences BUT"
>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="132px"/>
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
<div style="text-align: center;">Compétences BUT</div>
</a>
</div>
@ -603,7 +564,7 @@ def fiche_etud(etudid=None):
%(etat_civil)s
<span>%(email_link)s</span>
</td><td class="photocell">
<a href="etud_photo_orig_page/%(etudid)s">%(etudfoto)s</a>
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
</td></tr></table>
"""
+ situation_template
@ -817,7 +778,7 @@ def menus_etud(etudid):
},
{
"title": "Voir le journal...",
"endpoint": "scolar.show_etud_log",
"endpoint": "scolar.showEtudLog",
"args": {"etudid": etud["etudid"]},
"enabled": True,
},

View File

@ -68,11 +68,6 @@ _SCO_PERMISSIONS = (
"AbsJustifView",
"Visualisation du détail des justificatifs (motif, fichiers)",
),
(
1 << 51,
"JustifValidate",
"Définir la validité d'un justificatif (valide, invalide, modifié)",
),
# Attention: les permissions sont codées sur 64 bits.
)

View File

@ -64,6 +64,8 @@ from app.scodoc import sco_etud
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_preferences
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.scolog import logdb
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from config import Config
@ -302,9 +304,10 @@ def suppress_photo(etud: Identite) -> None:
for filename in filenames:
log(f"removing file {filename}")
os.remove(filename)
# 3- log
Scolog.logdb(method="changePhoto", msg="suppression", etudid=etud.id)
db.session.commit()
# 3- log
cnx = ndb.GetDBConnexion()
logdb(cnx, method="changePhoto", msg="suppression", etudid=etud.id)
# ---------------------------------------------------------------------------

View File

@ -188,8 +188,7 @@ def get_inscrits_etape(
break
if not doc:
raise ScoValueError(
f"pas de réponse du portail ! <br>(timeout={portal_timeout}, requête: <tt>{req}</tt>)",
safe=True,
f"pas de réponse du portail ! <br>(timeout={portal_timeout}, requête: <tt>{req}</tt>)"
)
etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))

View File

@ -233,6 +233,7 @@ def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
tab.base_url = "%s?formsemestre_id=%s" % (request.base_url, formsemestre_id)
return tab.make_page(
title="""<h2 class="formsemestre">Poursuite d'études</h2>""",
init_qtip=True,
javascripts=["js/etud_info.js"],
fmt=fmt,
with_html_headers=True,

View File

@ -246,7 +246,7 @@ PREF_CATEGORIES = (
"bul_margins",
{
"title": "Marges additionnelles des bulletins, en millimètres",
"subtitle": """Le bulletin de notes classique (pas BUT) est toujours redimensionné
"subtitle": """Le bulletin de notes notes est toujours redimensionné
pour occuper l'espace disponible entre les marges.
""",
"related": ("bul", "bul_mail", "pdf"),
@ -685,19 +685,6 @@ class BasePreferences:
"only_global": True,
},
),
(
"assiduites_disable",
{
"initvalue": "",
"title": "Désactiver le module d'assiduité",
"size": 40,
"category": "assi",
"explanation": """Désactive complètement le suivi de l'assiduité sur ScoDoc.
Indiquer un message à afficher pour orienter l'utilisateur.
Laisser ce champ est vide pour utiliser l'assiduité dans ScoDoc.
""",
},
),
(
"abs_notify_max_freq",
{
@ -1363,7 +1350,7 @@ class BasePreferences:
"bul_show_decision",
{
"initvalue": 0,
"title": "Faire figurer les décisions de jury sur les bulletins",
"title": "Faire figurer les décisions sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],

Some files were not shown because too many files have changed in this diff Show More