Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into revamp

This commit is contained in:
Emmanuel Viennet 2024-07-27 15:38:04 +02:00
commit 7ff0fd39fb
45 changed files with 1970 additions and 1276 deletions

View File

@ -19,7 +19,8 @@ import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.api import api_bp as bp 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_web_bp, get_model_api_object, tools
from app.decorators import permission_required, scodoc from app.api import api_permission_required as permission_required
from app.decorators import scodoc
from app.models import ( from app.models import (
Assiduite, Assiduite,
Evaluation, Evaluation,
@ -47,6 +48,8 @@ def assiduite(assiduite_id: int = None):
"""Retourne un objet assiduité à partir de son id """Retourne un objet assiduité à partir de son id
Exemple de résultat: Exemple de résultat:
```json
{ {
"assiduite_id": 1, "assiduite_id": 1,
"etudid": 2, "etudid": 2,
@ -55,11 +58,17 @@ def assiduite(assiduite_id: int = None):
"date_fin": "2022-10-31T10:00+01:00", "date_fin": "2022-10-31T10:00+01:00",
"etat": "retard", "etat": "retard",
"desc": "une description", "desc": "une description",
"user_id: 1 or null, "user_id": 1 or null,
"user_name" : login scodoc or null "user_name" : login scodoc or null,
"user_nom_complet": "Marie Dupont" "user_nom_complet": "Marie Dupont",
"est_just": False or True, "est_just": False or True,
} }
```
SAMPLES
-------
/assiduite/1;
""" """
return get_model_api_object(Assiduite, assiduite_id, Identite) return get_model_api_object(Assiduite, assiduite_id, Identite)
@ -77,15 +86,23 @@ def assiduite(assiduite_id: int = None):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def assiduite_justificatifs(assiduite_id: int = None, long: bool = False): def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
"""Retourne la liste des justificatifs qui justifie cette assiduitée """Retourne la liste des justificatifs qui justifient cette assiduité.
Exemple de résultat: Exemple de résultat:
```json
[ [
1, 1,
2, 2,
3, 3,
... ...
] ]
```
SAMPLES
-------
/assiduite/1/justificatifs;
/assiduite/1/justificatifs/long;
""" """
return get_assiduites_justif(assiduite_id, long) return get_assiduites_justif(assiduite_id, long)
@ -123,45 +140,7 @@ def assiduites_count(
etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False 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 QUERY
----- -----
@ -175,6 +154,25 @@ def assiduites_count(
metric:<array[string]:metric> metric:<array[string]:metric>
split:<bool:split> 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;
""" """
# Récupération de l'étudiant # Récupération de l'étudiant
@ -230,39 +228,6 @@ def assiduites_count(
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False): def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
""" """
Retourne toutes les assiduités d'un étudiant Retourne toutes les assiduités d'un étudiant
chemin : /assiduites/<int:etudid>
Un filtrage peut être donné avec une query
chemin : /assiduites/<int:etudid>/query?
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
QUERY QUERY
----- -----
@ -273,6 +238,25 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
date_fin:<string:date_fin_iso> date_fin:<string:date_fin_iso>
etat:<array[string]:etat> etat:<array[string]:etat>
formsemestre_id:<int:formsemestre_id> formsemestre_id:<int:formsemestre_id>
with_justifs:<bool:with_justifs>
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é
SAMPLES
-------
/assiduites/1;
/assiduites/1/query?etat=retard;
/assiduites/1/query?moduleimpl_id=1;
/assiduites/1/query?with_justifs=;
""" """
@ -328,7 +312,9 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
Pour chaque évaluation, retourne la liste des objets assiduités Pour chaque évaluation, retourne la liste des objets assiduités
sur la plage de l'évaluation sur la plage de l'évaluation
Présentation du retour : Exemple de résultat:
```json
[ [
{ {
"evaluation_id": 1234, "evaluation_id": 1234,
@ -341,6 +327,12 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
} }
] ]
SAMPLES
-------
/assiduites/1/evaluations;
```
""" """
# Récupération de l'étudiant # Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine) etud: Identite = tools.get_etud(etudid, nip, ine)
@ -367,7 +359,10 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
def evaluation_assiduites(evaluation_id): def evaluation_assiduites(evaluation_id):
""" """
Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation
Présentation du retour :
Exemple de résultat:
```json
{ {
"<etudid>" : [ "<etudid>" : [
{ {
@ -376,6 +371,11 @@ def evaluation_assiduites(evaluation_id):
}, },
] ]
} }
```
CATEGORY
--------
Évaluations
""" """
# Récupération de l'évaluation # Récupération de l'évaluation
try: try:
@ -404,37 +404,6 @@ def assiduites_group(with_query: bool = False):
Retourne toutes les assiduités d'un groupe d'étudiants Retourne toutes les assiduités d'un groupe d'étudiants
chemin : /assiduites/group/query?etudids=1,2,3 chemin : /assiduites/group/query?etudids=1,2,3
Un filtrage peut être donné avec une query
chemin : /assiduites/group/query?etudids=1,2,3
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
QUERY QUERY
----- -----
@ -444,8 +413,25 @@ def assiduites_group(with_query: bool = False):
date_debut:<string:date_debut_iso> date_debut:<string:date_debut_iso>
date_fin:<string:date_fin_iso> date_fin:<string:date_fin_iso>
etat:<array[string]:etat> etat:<array[string]:etat>
etudids:<array[int]:etudids etudids:<array[int]:etudids>
formsemestre_id:<int:formsemestre_id> formsemestre_id:<int:formsemestre_id>
with_justif:<bool:with_justif>
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;
""" """
@ -506,6 +492,7 @@ def assiduites_group(with_query: bool = False):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne toutes les assiduités du formsemestre """Retourne toutes les assiduités du formsemestre
QUERY QUERY
----- -----
user_id:<int:user_id> user_id:<int:user_id>
@ -514,6 +501,23 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
date_debut:<string:date_debut_iso> date_debut:<string:date_debut_iso>
date_fin:<string:date_fin_iso> date_fin:<string:date_fin_iso>
etat:<array[string]:etat> 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;
""" """
# Récupération du formsemestre à partir du formsemestre_id # Récupération du formsemestre à partir du formsemestre_id
@ -577,6 +581,25 @@ def assiduites_formsemestre_count(
formsemestre_id:<int:formsemestre_id> formsemestre_id:<int:formsemestre_id>
metric:<array[string]:metric> metric:<array[string]:metric>
split:<bool:split> 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;
""" """
# Récupération du formsemestre à partir du formsemestre_id # Récupération du formsemestre à partir du formsemestre_id
@ -628,7 +651,10 @@ def assiduites_formsemestre_count(
def assiduite_create(etudid: int = None, nip=None, ine=None): def assiduite_create(etudid: int = None, nip=None, ine=None):
""" """
Enregistrement d'assiduités pour un étudiant (etudid) Enregistrement d'assiduités pour un étudiant (etudid)
La requête doit avoir un content type "application/json":
DATA
----
```json
[ [
{ {
"date_debut": str, "date_debut": str,
@ -644,6 +670,12 @@ 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 # Récupération de l'étudiant
@ -697,7 +729,10 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
def assiduites_create(): def assiduites_create():
""" """
Création d'une assiduité ou plusieurs assiduites Création d'une assiduité ou plusieurs assiduites
La requête doit avoir un content type "application/json":
DATA
----
```json
[ [
{ {
"date_debut": str, "date_debut": str,
@ -710,12 +745,17 @@ def assiduites_create():
"date_fin": str, "date_fin": str,
"etat": str, "etat": str,
"etudid":int, "etudid":int,
"moduleimpl_id": int, "moduleimpl_id": int,
"desc":str, "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""}]
""" """
@ -886,13 +926,18 @@ def assiduite_delete():
""" """
Suppression d'une assiduité à partir de son id Suppression d'une assiduité à partir de son id
Forme des données envoyées : DATA
----
```json
[ [
<assiduite_id:int>, <assiduite_id:int>,
... ...
] ]
```
SAMPLES
-------
/assiduite/delete;[2,2,3]
""" """
# Récupération des ids envoyés dans la liste # Récupération des ids envoyés dans la liste
@ -967,13 +1012,24 @@ def _delete_one(assiduite_id: int) -> tuple[int, str]:
def assiduite_edit(assiduite_id: int): def assiduite_edit(assiduite_id: int):
""" """
Edition d'une assiduité à partir de son id Edition d'une assiduité à partir de son id
La requête doit avoir un content type "application/json":
DATA
----
```json
{ {
"etat"?: str, "etat"?: str,
"moduleimpl_id"?: int "moduleimpl_id"?: int
"desc"?: str "desc"?: str
"est_just"?: bool "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 # Récupération de l'assiduité à modifier
@ -1015,7 +1071,10 @@ def assiduite_edit(assiduite_id: int):
def assiduites_edit(): def assiduites_edit():
""" """
Edition de plusieurs assiduités Edition de plusieurs assiduités
La requête doit avoir un content type "application/json":
DATA
----
```json
[ [
{ {
"assiduite_id" : int, "assiduite_id" : int,
@ -1025,6 +1084,13 @@ def assiduites_edit():
"est_just"?: bool "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) edit_list: list[object] = request.get_json(force=True)

View File

@ -6,6 +6,11 @@
""" """
API : billets d'absences API : billets d'absences
CATEGORY
--------
Billets d'absence
""" """
from flask import g, request from flask import g, request
@ -20,6 +25,7 @@ from app.models import BilletAbsence
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.scodoc import sco_abs_billets from app.scodoc import sco_abs_billets
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
@bp.route("/billets_absence/etudiant/<int:etudid>") @bp.route("/billets_absence/etudiant/<int:etudid>")
@ -29,7 +35,7 @@ from app.scodoc.sco_permissions import Permission
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def billets_absence_etudiant(etudid: int): 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) billets = sco_abs_billets.query_billets_etud(etudid)
return [billet.to_dict() for billet in billets] return [billet.to_dict() for billet in billets]
@ -41,13 +47,30 @@ def billets_absence_etudiant(etudid: int):
@permission_required(Permission.AbsAddBillet) @permission_required(Permission.AbsAddBillet)
@as_json @as_json
def billets_absence_create(): def billets_absence_create():
"""Ajout d'un billet d'absence""" """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
}
```
SAMPLES
-------
/billets_absence/create;{""etudid"":""1"",""abs_begin"":""2023-10-27T10:00"",""abs_end"":""2023-10-28T10:00"",""description"":""grave malade"",""justified"":""1""}
"""
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
etudid = data.get("etudid") etudid = data.get("etudid")
abs_begin = data.get("abs_begin") abs_begin = data.get("abs_begin")
abs_end = data.get("abs_end") abs_end = data.get("abs_end")
description = data.get("description", "") description = data.get("description", "")
justified = data.get("justified", False) justified = scu.to_bool(data.get("justified", False))
if None in (etudid, abs_begin, abs_end): if None in (etudid, abs_begin, abs_end):
return json_error( return json_error(
404, message="Paramètre manquant: etudid, abs_begin, abs_end requis" 404, message="Paramètre manquant: etudid, abs_begin, abs_end requis"

View File

@ -9,6 +9,11 @@
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/), Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api). mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
CATEGORY
--------
Département
""" """
from datetime import datetime from datetime import datetime
@ -27,24 +32,19 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error 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") @bp.route("/departements")
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departements_list(): def departements_list():
"""Liste les départements""" """Liste tous les départements.
SAMPLES
-------
/departements;
"""
return [dept.to_dict(with_dept_name=True) for dept in Departement.query] return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
@ -54,7 +54,13 @@ def departements_list():
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departements_ids(): def departements_ids():
"""Liste des ids de départements""" """Liste des ids de tous les départements.
SAMPLES
-------
/departements_ids;
"""
return [dept.id for dept in Departement.query] return [dept.id for dept in Departement.query]
@ -67,15 +73,10 @@ def departement_by_acronym(acronym: str):
""" """
Info sur un département. Accès par acronyme. Info sur un département. Accès par acronyme.
Exemple de résultat : SAMPLES
{ -------
"id": 1, /departement/TAPI;
"acronym": "TAPI",
"dept_name" : "TEST",
"description": null,
"visible": true,
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
}
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return dept.to_dict(with_dept_name=True) return dept.to_dict(with_dept_name=True)
@ -86,9 +87,14 @@ def departement_by_acronym(acronym: str):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departement_by_id(dept_id: int): def departement_get(dept_id: int):
""" """
Info sur un département. Accès par id. Info sur un département. Accès par id.
SAMPLES
-------
/departement/id/1;
""" """
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
return dept.to_dict() return dept.to_dict()
@ -102,11 +108,19 @@ def departement_by_id(dept_id: int):
def departement_create(): def departement_create():
""" """
Création d'un département. Création d'un département.
The request content type should be "application/json": Le content type doit être `application/json`.
DATA
----
```json
{ {
"acronym": str, "acronym": str,
"visible":bool, "visible": bool,
} }
```
SAMPLES
-------
/departement/create;{""acronym"":""MYDEPT"",""visible"":""1""}
""" """
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
acronym = str(data.get("acronym", "")) acronym = str(data.get("acronym", ""))
@ -130,10 +144,12 @@ def departement_create():
@as_json @as_json
def departement_edit(acronym): def departement_edit(acronym):
""" """
Edition d'un département: seul visible peut être modifié Édition d'un département: seul le champ `visible` peut être modifié.
The request content type should be "application/json":
DATA
----
{ {
"visible":bool, "visible": bool,
} }
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
@ -155,7 +171,7 @@ def departement_edit(acronym):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def departement_delete(acronym): def departement_delete(acronym):
""" """
Suppression d'un département. Suppression d'un département identifié par son acronyme.
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
acronym = dept.acronym acronym = dept.acronym
@ -172,25 +188,16 @@ def departement_delete(acronym):
@as_json @as_json
def departement_etudiants(acronym: str): def departement_etudiants(acronym: str):
""" """
Retourne la liste des étudiants d'un département Retourne la liste des étudiants d'un département.
acronym: l'acronyme d'un département PARAMS
------
acronym : l'acronyme d'un département
SAMPLES
-------
/departement/TAPI/etudiants;
Exemple de résultat :
[
{
"civilite": "M",
"code_ine": "7899X61616",
"code_nip": "F6777H88",
"date_naissance": null,
"email": "toto@toto.fr",
"emailperso": null,
"etudid": 18,
"nom": "MOREL",
"prenom": "JACQUES"
},
...
]
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [etud.to_dict_short() for etud in dept.etudiants] return [etud.to_dict_short() for etud in dept.etudiants]
@ -215,7 +222,13 @@ def departement_etudiants_by_id(dept_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departement_formsemestres_ids(acronym: str): def departement_formsemestres_ids(acronym: str):
"""liste des ids formsemestre du département""" """Liste des ids de tous les formsemestres du département.
SAMPLES
-------
/departement/TAPI/formsemestres_ids;
"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [formsemestre.id for formsemestre in dept.formsemestres] return [formsemestre.id for formsemestre in dept.formsemestres]
@ -226,7 +239,13 @@ def departement_formsemestres_ids(acronym: str):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departement_formsemestres_ids_by_id(dept_id: int): def departement_formsemestres_ids_by_id(dept_id: int):
"""liste des ids formsemestre du département""" """Liste des ids de tous les formsemestres du département.
SAMPLES
-------
/departement/id/1/formsemestres_ids;
"""
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
return [formsemestre.id for formsemestre in dept.formsemestres] return [formsemestre.id for formsemestre in dept.formsemestres]
@ -239,7 +258,7 @@ def departement_formsemestres_ids_by_id(dept_id: int):
@as_json @as_json
def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None): def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None):
""" """
Liste les semestres du département indiqué (par son acronyme ou son id) 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 contenant la date courante, ou à défaut celle indiquée en argument
(au format ISO). (au format ISO).
@ -247,6 +266,9 @@ def departement_formsemestres_courants(acronym: str = "", dept_id: int | None =
----- -----
date_courante:<string:date_courante> date_courante:<string:date_courante>
SAMPLES
-------
/departement/id/1/formsemestres_courants?date_courante=2022-01-01
""" """
dept = ( dept = (
Departement.query.filter_by(acronym=acronym).first_or_404() Departement.query.filter_by(acronym=acronym).first_or_404()

View File

@ -6,6 +6,10 @@
""" """
API : accès aux étudiants API : accès aux étudiants
CATEGORY
--------
Étudiants
""" """
from datetime import datetime from datetime import datetime
from operator import attrgetter from operator import attrgetter
@ -38,9 +42,8 @@ from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_photos
from app.scodoc.sco_utils import json_error, suppress_accents 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 import app.scodoc.sco_utils as scu
# Un exemple: # Un exemple:
@ -98,25 +101,16 @@ def etudiants_courants(long: bool = False):
et les formsemestres contenant la date courante, et les formsemestres contenant la date courante,
ou à défaut celle indiquée en argument (au format ISO). ou à défaut celle indiquée en argument (au format ISO).
En format "long": voir l'exemple.
QUERY QUERY
----- -----
date_courante:<string:date_courante> date_courante:<string:date_courante>
Exemple de résultat : SAMPLES
[ -------
{ /etudiants/courants?date_courante=2022-05-01;
"id": 1234, /etudiants/courants/long?date_courante=2022-05-01;
"code_nip": "12345678",
"code_ine": null,
"nom": "JOHN",
"nom_usuel": None,
"prenom": "DEUF",
"civilite": "M",
}
...
]
En format "long": voir documentation.
""" """
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView) allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
@ -160,10 +154,13 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
""" """
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé. Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
PARAMS
------
etudid : l'etudid de l'étudiant etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant nip : le code nip de l'étudiant
ine : le code ine 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. 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. Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
""" """
@ -197,6 +194,8 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non
----- -----
size:<string:size> size:<string:size>
PARAMS
------
etudid : l'etudid de l'étudiant etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant nip : le code nip de l'étudiant
ine : le code ine de l'étudiant ine : le code ine de l'étudiant
@ -269,9 +268,12 @@ def etudiant_set_photo_image(etudid: int = None):
@as_json @as_json
def etudiants(etudid: int = None, nip: str = None, ine: str = None): def etudiants(etudid: int = None, nip: str = None, ine: str = None):
""" """
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie Info sur le ou les étudiants correspondants.
toujours une liste.
Comme `/etudiant` mais renvoie toujours une liste.
Si non trouvé, liste vide, pas d'erreur. Si non trouvé, liste vide, pas d'erreur.
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a 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.). été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
""" """
@ -304,8 +306,9 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def etudiants_by_name(start: str = "", min_len=3, limit=32): def etudiants_by_name(start: str = "", min_len=3, limit=32):
"""Liste des étudiants dont le nom débute par start. """Liste des étudiants dont le nom débute par `start`.
Si start fait moins de min_len=3 caractères, liste vide.
Si `start` fait moins de `min_len=3` caractères, liste vide.
La casse et les accents sont ignorés. La casse et les accents sont ignorés.
""" """
if len(start) < min_len: if len(start) < min_len:
@ -340,13 +343,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
@as_json @as_json
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
""" """
Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique. Liste des formsemestres qu'un étudiant a suivi, triés par ordre chronologique.
Accès par etudid, nip ou ine. Accès par etudid, nip ou ine.
Attention, si accès via NIP ou INE, les semestres peuvent être de départements Attention, si accès via NIP ou INE, les formsemestres peuvent être de départements
différents (si l'étudiant a changé de département). L'id du département est `dept_id`. 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 formsemestre suivis dans le département. Si accès par département, ne retourne que les formsemestres suivis dans le département.
""" """
if etudid is not None: if etudid is not None:
q_etud = Identite.query.filter_by(id=etudid) q_etud = Identite.query.filter_by(id=etudid)
@ -414,13 +417,18 @@ def bulletin(
""" """
Retourne le bulletin d'un étudiant dans un formsemestre. Retourne le bulletin d'un étudiant dans un formsemestre.
PARAMS
------
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre
code_type : "etudid", "nip" ou "ine" code_type : "etudid", "nip" ou "ine"
code : valeur du code INE, NIP ou etudid, selon code_type. code : valeur du code INE, NIP ou etudid, selon code_type.
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
pdf : si spécifié, bulletin au format PDF (et non JSON). pdf : si spécifié, bulletin au format PDF (et non JSON).
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin SAMPLES
-------
/etudiant/etudid/1/formsemestre/1/bulletin
""" """
if version == "pdf": if version == "pdf":
version = "long" version = "long"
@ -474,34 +482,14 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
""" """
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
PARAMS
------
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant etudid : l'etudid d'un étudiant
Exemple de résultat : SAMPLES
[ -------
{ /etudiant/etudid/1/formsemestre/1/groups
"partition_id": 1,
"id": 1,
"formsemestre_id": 1,
"partition_name": null,
"numero": 0,
"bul_show_rank": false,
"show_in_lists": true,
"group_id": 1,
"group_name": null
},
{
"partition_id": 2,
"id": 2,
"formsemestre_id": 1,
"partition_name": "TD",
"numero": 1,
"bul_show_rank": false,
"show_in_lists": true,
"group_id": 2,
"group_name": "A"
}
]
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -529,9 +517,12 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def etudiant_create(force=False): 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. Si force, crée même si homonymie détectée.
L'étudiant créé n'est pas inscrit à un semestre. L'étudiant créé n'est pas inscrit à un semestre.
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme) Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
""" """
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
@ -599,7 +590,17 @@ def etudiant_edit(
code_type: str = "etudid", code_type: str = "etudid",
code: str = None, code: str = None,
): ):
"""Edition des données étudiant (identité, admission, adresses)""" """Édition des données étudiant (identité, admission, adresses).
PARAMS
------
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
`code`: la valeur du code
SAMPLES
-------
/etudiant/ine/INE1/edit;{""prenom"":""Nouveau Prénom"", ""adresses"":[{""email"":""nouvelle@adresse.fr""}]}
"""
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
if not ok: if not ok:
return etud # json error return etud # json error
@ -638,7 +639,27 @@ def etudiant_annotation(
code_type: str = "etudid", code_type: str = "etudid",
code: str = None, code: str = None,
): ):
"""Ajout d'une annotation sur un étudiant""" """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
}
```
SAMPLES
-------
/etudiant/etudid/1/annotation;{""comment"":""une annotation sur l'étudiant""}
"""
if not current_user.has_permission(Permission.ViewEtudData): if not current_user.has_permission(Permission.ViewEtudData):
return json_error(403, "non autorisé (manque ViewEtudData)") return json_error(403, "non autorisé (manque ViewEtudData)")
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
@ -675,7 +696,13 @@ def etudiant_annotation_delete(
code_type: str = "etudid", code: str = None, annotation_id: int = None code_type: str = "etudid", code: str = None, annotation_id: int = None
): ):
""" """
Suppression d'une annotation 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
""" """
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
if not ok: if not ok:

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux évaluations ScoDoc 9 API : accès aux évaluations
CATEGORY
--------
Évaluations
""" """
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
@ -32,24 +36,28 @@ import app.scodoc.sco_utils as scu
def get_evaluation(evaluation_id: int): def get_evaluation(evaluation_id: int):
"""Description d'une évaluation. """Description d'une évaluation.
DATA
----
```json
{ {
'coefficient': 1.0, 'coefficient': 1.0,
'date_debut': '2016-01-04T08:30:00', 'date_debut': '2016-01-04T08:30:00',
'date_fin': '2016-01-04T12:30:00', 'date_fin': '2016-01-04T12:30:00',
'description': 'TP NI9219 Température', 'description': 'TP Température',
'evaluation_type': 0, 'evaluation_type': 0,
'id': 15797, 'id': 15797,
'moduleimpl_id': 1234, 'moduleimpl_id': 1234,
'note_max': 20.0, 'note_max': 20.0,
'numero': 3, 'numero': 3,
'poids': { 'poids': {
'UE1.1': 1.0, 'UE1.1': 1.0,
'UE1.2': 1.0, 'UE1.2': 1.0,
'UE1.3': 1.0 'UE1.3': 1.0
}, },
'publish_incomplete': False, 'publish_incomplete': False,
'visibulletin': True 'visibulletin': True
} }
```
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -70,11 +78,15 @@ def get_evaluation(evaluation_id: int):
@as_json @as_json
def moduleimpl_evaluations(moduleimpl_id: int): 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 moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat : voir /evaluation SAMPLES
-------
/moduleimpl/1/evaluations
""" """
modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations] return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
@ -88,30 +100,15 @@ def moduleimpl_evaluations(moduleimpl_id: int):
@as_json @as_json
def evaluation_notes(evaluation_id: int): 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 evaluation_id : l'id de l'évaluation
Exemple de résultat : SAMPLES
{ -------
"11": { /evaluation/2/notes;
"etudid": 11,
"evaluation_id": 1,
"value": 15.0,
"comment": "",
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
"uid": 2
},
"12": {
"etudid": 12,
"evaluation_id": 1,
"value": 12.0,
"comment": "",
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
"uid": 2
},
...
}
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -145,17 +142,26 @@ def evaluation_notes(evaluation_id: int):
@as_json @as_json
def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
"""Écriture de notes dans une évaluation. """Écriture de notes dans une évaluation.
The request content type should be "application/json",
and contains: DATA
----
```json
{ {
'notes' : [ [etudid, value], ... ], 'notes' : [ [etudid, value], ... ],
'comment' : optional string 'comment' : optional string
} }
Result: ```
- nb_changed: nombre de notes changées
- nb_suppress: nombre de notes effacées Résultat:
- etudids_changed: étudiants dont la note est modifiée
- etudids_with_decision: liste des etudiants dont la note a changé - etudids_with_decision: liste des etudiants dont la note a changé
alors qu'ils ont une décision de jury enregistrée. alors qu'ils ont une décision de jury enregistrée.
- history_menu: un fragment de HTML expliquant l'historique de la note de chaque étudiant modifié.
SAMPLES
-------
/evaluation/1/notes/set;{""notes"": [[1, 17], [2, ""SUPR""]], ""comment"" : ""sample test""}
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -186,8 +192,9 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
@as_json @as_json
def evaluation_create(moduleimpl_id: int): def evaluation_create(moduleimpl_id: int):
"""Création d'une évaluation. """Création d'une évaluation.
The request content type should be "application/json",
and contains: DATA
----
{ {
"description" : str, "description" : str,
"evaluation_type" : int, // {0,1,2} default 0 (normale) "evaluation_type" : int, // {0,1,2} default 0 (normale)
@ -200,7 +207,13 @@ def evaluation_create(moduleimpl_id: int):
"coefficient" : float, // si non spécifié, 1.0 "coefficient" : float, // si non spécifié, 1.0
"poids" : { ue_id : poids } // optionnel "poids" : { ue_id : poids } // optionnel
} }
Result: l'évaluation créée.
Résultat: l'évaluation créée.
SAMPLES
-------
/moduleimpl/1/evaluation/create;{""description"":""Exemple éval.""}
""" """
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
if not moduleimpl.can_edit_evaluation(current_user): if not moduleimpl.can_edit_evaluation(current_user):
@ -250,7 +263,7 @@ def evaluation_create(moduleimpl_id: int):
@as_json @as_json
def evaluation_delete(evaluation_id: int): def evaluation_delete(evaluation_id: int):
"""Suppression d'une évaluation. """Suppression d'une évaluation.
Efface aussi toutes ses notes Efface aussi toutes ses notes.
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux formations ScoDoc 9 API : accès aux formations
CATEGORY
--------
Formations
""" """
from flask import flash, g, request from flask import flash, g, request
@ -38,7 +42,13 @@ from app.scodoc.sco_permissions import Permission
@as_json @as_json
def formations(): def formations():
""" """
Retourne la liste de toutes les formations (tous départements) Retourne la liste de toutes les formations (tous départements,
sauf si route départementale).
SAMPLES
-------
/formations;
""" """
query = Formation.query query = Formation.query
if g.scodoc_dept: if g.scodoc_dept:
@ -58,7 +68,12 @@ def formations_ids():
Retourne la liste de toutes les id de formations Retourne la liste de toutes les id de formations
(tous départements, ou du département indiqué dans la route) (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 ]`.
SAMPLES
-------
/formations_ids;
""" """
query = Formation.query query = Formation.query
if g.scodoc_dept: if g.scodoc_dept:
@ -72,26 +87,14 @@ def formations_ids():
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formation_by_id(formation_id: int): def formation_get(formation_id: int):
""" """
La formation d'id donné La formation d'id donné.
formation_id : l'id d'une formation SAMPLES
-------
/formation/1;
Exemple de résultat :
{
"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) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -123,97 +126,14 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
""" """
Retourne la formation, avec UE, matières, modules Retourne la formation, avec UE, matières, modules
PARAMS
------
formation_id : l'id d'une formation formation_id : l'id d'une formation
export_ids : True ou False, si l'on veut ou non exporter les ids export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
Exemple de résultat : SAMPLES
{ -------
"id": 1, /formation/1/export
"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": [
{
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"numero": 1,
"module": [
{
"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"
}
]
},
{
"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) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -236,11 +156,13 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
@as_json @as_json
def referentiel_competences(formation_id: int): def referentiel_competences(formation_id: int):
""" """
Retourne le référentiel de compétences Retourne le référentiel de compétences de la formation
ou null si pas de référentiel associé.
formation_id : l'id d'une formation SAMPLES
-------
/formation/1/referentiel_competences;
return null si pas de référentiel associé.
""" """
query = Formation.query.filter_by(id=formation_id) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -259,8 +181,14 @@ def referentiel_competences(formation_id: int):
@as_json @as_json
def ue_set_parcours(ue_id: int): def ue_set_parcours(ue_id: int):
"""Associe UE et parcours BUT. """Associe UE et parcours BUT.
La liste des ids de parcours est passée en argument JSON. La liste des ids de parcours est passée en argument JSON.
JSON arg: [parcour_id1, parcour_id2, ...]
DATA
----
```json
[ parcour_id1, parcour_id2, ... ]
```
""" """
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -293,7 +221,7 @@ def ue_set_parcours(ue_id: int):
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
@as_json @as_json
def ue_assoc_niveau(ue_id: int, niveau_id: int): def ue_assoc_niveau(ue_id: int, niveau_id: int):
"""Associe l'UE au niveau de compétence""" """Associe l'UE au niveau de compétence."""
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -323,7 +251,7 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
@as_json @as_json
def ue_desassoc_niveau(ue_id: int): def ue_desassoc_niveau(ue_id: int):
"""Désassocie cette UE de son niveau de compétence """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) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -345,7 +273,12 @@ def ue_desassoc_niveau(ue_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def get_ue(ue_id: int): def get_ue(ue_id: int):
"""Renvoie l'UE""" """Renvoie l'UE.
SAMPLES
-------
/formation/ue/1;
"""
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -359,7 +292,12 @@ def get_ue(ue_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def formation_module_get(module_id: int): def formation_module_get(module_id: int):
"""Renvoie le module""" """Renvoie le module.
SAMPLES
-------
/formation/module/1;
"""
query = Module.query.filter_by(id=module_id) query = Module.query.filter_by(id=module_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -390,15 +328,17 @@ def formation_module_get(module_id: int):
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""): def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
"""Change le code Apogée de l'UE. """Change le code Apogée de l'UE.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. 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. 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
Le retour est une chaîne (le code enregistré), pas json. 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: if ue_id is None:
ue_id = request.form.get("oid") ue_id = request.form.get("oid")
@ -444,14 +384,16 @@ def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""): def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
"""Change le code Apogée du RCUE de l'UE. """Change le code Apogée du RCUE de l'UE.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Ce changement peut être fait sur formation verrouillée.
Si code_apogee n'est pas spécifié ou vide, Si code_apogee n'est pas spécifié ou vide,
utilise l'argument value du POST (utilisé par jinplace.js) utilise l'argument value du POST (utilisé par `jinplace.js`)
Le retour est une chaîne (le code enregistré), pas json. Le retour est une chaîne (le code enregistré), pas du json.
""" """
if not code_apogee: if not code_apogee:
code_apogee = request.form.get("value", "") code_apogee = request.form.get("value", "")
@ -497,15 +439,17 @@ def formation_module_set_code_apogee(
module_id: int | None = None, code_apogee: str = "" module_id: int | None = None, code_apogee: str = ""
): ):
"""Change le code Apogée du module. """Change le code Apogée du module.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. 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. Ce changement peut être fait sur formation verrouillée.
Si code_apogee n'est pas spécifié ou vide,
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) utilise l'argument value du POST (utilisé par jinplace.js)
Le retour est une chaîne (le code enregistré), pas json. Le retour est une chaîne (le code enregistré), pas du json.
""" """
if module_id is None: if module_id is None:
module_id = request.form.get("oid") module_id = request.form.get("oid")

View File

@ -6,6 +6,12 @@
""" """
ScoDoc 9 API : accès aux formsemestres ScoDoc 9 API : accès aux formsemestres
CATEGORY
--------
FormSemestre
""" """
from operator import attrgetter, itemgetter from operator import attrgetter, itemgetter
@ -48,43 +54,15 @@ from app.tables.recap import TableRecap, RowRecap
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_infos(formsemestre_id: int): def formsemestre_get(formsemestre_id: int):
""" """
Information sur le formsemestre indiqué. Information sur le formsemestre indiqué.
formsemestre_id : l'id du formsemestre formsemestre_id : l'id du formsemestre
Exemple de résultat : SAMPLES
{ -------
"block_moyennes": false, /formsemestre/1
"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",
}
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -101,9 +79,11 @@ def formsemestre_infos(formsemestre_id: int):
@as_json @as_json
def formsemestres_query(): def formsemestres_query():
""" """
Retourne les formsemestres filtrés par Retourne les formsemestres filtrés par étape Apogée ou année scolaire
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant ou département (acronyme ou id) ou état ou code étudiant.
PARAMS
------
etape_apo : un code étape apogée etape_apo : un code étape apogée
annee_scolaire : année de début de l'année scolaire annee_scolaire : année de début de l'année scolaire
dept_acronym : acronyme du département (eg "RT") dept_acronym : acronyme du département (eg "RT")
@ -190,7 +170,36 @@ def formsemestres_query():
@permission_required(Permission.EditFormSemestre) @permission_required(Permission.EditFormSemestre)
@as_json @as_json
def formsemestre_edit(formsemestre_id: int): def formsemestre_edit(formsemestre_id: int):
"""Modifie les champs d'un formsemestre.""" """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,
}
```
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
editable_keys = { editable_keys = {
@ -228,13 +237,19 @@ def formsemestre_edit(formsemestre_id: int):
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_apo_etapes(): def formsemestre_set_apo_etapes():
"""Change les codes étapes du semestre indiqué. """Change les codes étapes du semestre indiqué.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V1RT, V1RT2", codes séparés par des virgules 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")) formsemestre_id = int(request.form.get("oid"))
etapes_apo_str = request.form.get("value") etapes_apo_str = request.form.get("value")
@ -265,13 +280,20 @@ def formsemestre_set_apo_etapes():
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_elt_sem_apo(): def formsemestre_set_elt_sem_apo():
"""Change les codes étapes du semestre indiqué. """Change les codes étapes du semestre indiqué.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules 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")) oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip() value = (request.form.get("value") or "").strip()
@ -293,13 +315,20 @@ def formsemestre_set_elt_sem_apo():
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_elt_annee_apo(): def formsemestre_set_elt_annee_apo():
"""Change les codes étapes du semestre indiqué (par le champ oid). """Change les codes étapes du semestre indiqué (par le champ oid).
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules 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")) oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip() value = (request.form.get("value") or "").strip()
@ -320,14 +349,21 @@ def formsemestre_set_elt_annee_apo():
@scodoc @scodoc
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_elt_passage_apo(): def formsemestre_set_elt_passage_apo():
"""Change les codes apogée de passage du semestre indiqué (par le champ oid). """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 Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules 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")) oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip() value = (request.form.get("value") or "").strip()
@ -353,11 +389,16 @@ def formsemestre_set_elt_passage_apo():
@as_json @as_json
def bulletins(formsemestre_id: int, version: str = "long"): def bulletins(formsemestre_id: int, version: str = "long"):
""" """
Retourne les bulletins d'un formsemestre donné Retourne les bulletins d'un formsemestre.
formsemestre_id : l'id d'un formesemestre PARAMS
------
formsemestre_id : int
version : string ("long", "short", "selectedevals")
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin SAMPLES
-------
/formsemestre/1/bulletins
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -387,66 +428,9 @@ def formsemestre_programme(formsemestre_id: int):
""" """
Retourne la liste des UEs, ressources et SAEs d'un semestre Retourne la liste des UEs, ressources et SAEs d'un semestre
formsemestre_id : l'id d'un formsemestre SAMPLES
-------
Exemple de résultat : /formsemestre/1/programme
{
"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": [
{
...
},
...
],
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
}
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -519,6 +503,10 @@ def formsemestre_etudiants(
----- -----
etat:<string:etat> etat:<string:etat>
SAMPLES
-------
/formsemestre/1/etudiants/query;
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -565,36 +553,9 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
""" """
Informations sur l'état des évaluations d'un formsemestre. Informations sur l'état des évaluations d'un formsemestre.
formsemestre_id : l'id d'un semestre SAMPLES
-------
Exemple de résultat : /formsemestre/1/etat_evals
[
{
"id": 1, // moduleimpl_id
"titre": "Initiation aux réseaux informatiques",
"evaluations": [
{
"id": 1,
"description": null,
"datetime_epreuve": null,
"heure_fin": "09:00:00",
"coefficient": "02.00"
"is_complete": true,
"nb_inscrits": 16,
"nb_manquantes": 0,
"ABS": 0,
"ATT": 0,
"EXC": 0,
"saisie_notes": {
"datetime_debut": "2021-09-11T00:00:00+02:00",
"datetime_fin": "2022-08-25T00:00:00+02:00",
"datetime_mediane": "2022-03-19T00:00:00+01:00"
}
},
...
]
},
]
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
@ -669,7 +630,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_resultat(formsemestre_id: int): 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. Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
Si `format=raw`, ne converti pas les valeurs. Si `format=raw`, ne converti pas les valeurs.
@ -678,6 +640,9 @@ def formsemestre_resultat(formsemestre_id: int):
----- -----
format:<string:format> format:<string:format>
SAMPLES
-------
/formsemestre/1/resultats;
""" """
format_spec = request.args.get("format", None) format_spec = request.args.get("format", None)
if format_spec is not None and format_spec != "raw": if format_spec is not None and format_spec != "raw":
@ -724,7 +689,7 @@ def formsemestre_resultat(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def groups_get_auto_assignment(formsemestre_id: int): def groups_get_auto_assignment(formsemestre_id: int):
"""rend les données stockées par""" """Rend les données stockées par `groups_save_auto_assignment`."""
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
@ -745,12 +710,17 @@ def groups_get_auto_assignment(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def groups_save_auto_assignment(formsemestre_id: int): def groups_save_auto_assignment(formsemestre_id: int):
"""enregistre les données""" """Enregistre les données, associées à ce formsemestre.
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
"""
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_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: if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
return json_error(413, "data too large") return json_error(413, "data too large")
formsemestre.groups_auto_assignment_data = request.data formsemestre.groups_auto_assignment_data = request.data
@ -765,17 +735,16 @@ def groups_save_auto_assignment(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_edt(formsemestre_id: int): 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. Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
group_ids permet de filtrer sur les groupes ScoDoc. Expérimental, ne pas utiliser hors ScoDoc.
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
QUERY QUERY
----- -----
group_ids:<string:group_ids> group_ids : string (optionnel) filtre sur les groupes ScoDoc.
show_modules_titles:<bool:show_modules_titles> show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:

View File

@ -5,7 +5,12 @@
############################################################################## ##############################################################################
""" """
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions.
CATEGORY
--------
Jury
""" """
import datetime import datetime
@ -48,7 +53,12 @@ from app.scodoc.sco_utils import json_error
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def decisions_jury(formsemestre_id: int): def decisions_jury(formsemestre_id: int):
"""Décisions du jury des étudiants du formsemestre.""" """Décisions du jury des étudiants du formsemestre.
SAMPLES
-------
/formsemestre/1/decisions_jury
"""
# APC, pair: # APC, pair:
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
if formsemestre is None: if formsemestre is None:
@ -91,7 +101,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def validation_ue_delete(etudid: int, validation_id: int): def validation_ue_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation d'UE."
return _validation_ue_delete(etudid, validation_id) return _validation_ue_delete(etudid, validation_id)
@ -108,7 +118,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def validation_formsemestre_delete(etudid: int, validation_id: int): def validation_formsemestre_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation de semestre."
# c'est la même chose (formations classiques) # c'est la même chose (formations classiques)
return _validation_ue_delete(etudid, validation_id) return _validation_ue_delete(etudid, validation_id)
@ -160,7 +170,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def autorisation_inscription_delete(etudid: int, validation_id: int): def autorisation_inscription_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette autorisation d'inscription."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404
@ -189,8 +199,12 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
@as_json @as_json
def validation_rcue_record(etudid: int): def validation_rcue_record(etudid: int):
"""Enregistre une validation de RCUE. """Enregistre une validation de RCUE.
Si une validation existe déjà pour ce RCUE, la remplace. Si une validation existe déjà pour ce RCUE, la remplace.
The request content type should be "application/json":
DATA
----
```json
{ {
"code" : str, "code" : str,
"ue1_id" : int, "ue1_id" : int,
@ -200,6 +214,7 @@ def validation_rcue_record(etudid: int):
"date" : date_iso, // si non spécifié, now() "date" : date_iso, // si non spécifié, now()
"parcours_id" :int, "parcours_id" :int,
} }
```
""" """
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
@ -314,7 +329,7 @@ def validation_rcue_record(etudid: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def validation_rcue_delete(etudid: int, validation_id: int): def validation_rcue_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation de RCUE."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404
@ -342,7 +357,7 @@ def validation_rcue_delete(etudid: int, validation_id: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def validation_annee_but_delete(etudid: int, validation_id: int): def validation_annee_but_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation d'année BUT."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404
@ -371,7 +386,7 @@ def validation_annee_but_delete(etudid: int, validation_id: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def validation_dut120_delete(etudid: int, validation_id: int): def validation_dut120_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation de DUT120."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404

View File

@ -38,9 +38,11 @@ from app.scodoc.sco_groups import get_group_members
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatif(justif_id: int = None): 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: Exemple de résultat:
```json
{ {
"justif_id": 1, "justif_id": 1,
"etudid": 2, "etudid": 2,
@ -52,6 +54,11 @@ def justificatif(justif_id: int = None):
"entry_date": "2022-10-31T08:00+01:00", "entry_date": "2022-10-31T08:00+01:00",
"user_id": 1 or null, "user_id": 1 or null,
} }
```
SAMPLES
-------
/justificatif/1;
""" """
@ -92,38 +99,32 @@ def justificatif(justif_id: int = None):
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False): def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
""" """
Retourne toutes les assiduités d'un étudiant Retourne toutes les assiduités d'un étudiant
chemin : /justificatifs/<int:etudid>
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
QUERY QUERY
----- -----
user_id:<int:user_id> user_id:<int:user_id>
est_just:<bool:est_just>
date_debut:<string:date_debut_iso> date_debut:<string:date_debut_iso>
date_fin:<string:date_fin_iso> date_fin:<string:date_fin_iso>
etat:<array[string]:etat> etat:<array[string]:etat>
order:<bool:order> order:<bool:order>
courant:<bool:courant> courant:<bool:courant>
group_id:<int:group_id> 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;
""" """
# Récupération de l'étudiant # Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine) etud: Identite = tools.get_etud(etudid, nip, ine)
@ -164,7 +165,7 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
def justificatifs_dept(dept_id: int = None, with_query: bool = False): def justificatifs_dept(dept_id: int = None, with_query: bool = False):
""" """
Renvoie tous les justificatifs d'un département Renvoie tous les justificatifs d'un département
(en ajoutant un champ "formsemestre" si possible) (en ajoutant un champ "`formsemestre`" si possible).
QUERY QUERY
----- -----
@ -176,6 +177,21 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
order:<bool:order> order:<bool:order>
courant:<bool:courant> courant:<bool:courant>
group_id:<int:group_id> 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 # Récupération du département et des étudiants du département
@ -204,9 +220,9 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
def _set_sems(justi: Justificatif, restrict: bool) -> dict: def _set_sems(justi: Justificatif, restrict: bool) -> dict:
""" """
_set_sems Ajoute le formsemestre associé au justificatif s'il existe _set_sems Ajoute le formsemestre associé au justificatif s'il existe.
Si le formsemestre n'existe pas, renvoie la simple représentation du justificatif Si le formsemestre n'existe pas, renvoie la simple représentation du justificatif.
Args: Args:
justi (Justificatif): Le justificatif justi (Justificatif): Le justificatif
@ -247,7 +263,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict:
@as_json @as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne tous les justificatifs du formsemestre """Retourne tous les justificatifs du formsemestre.
QUERY QUERY
----- -----
@ -259,6 +275,21 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
order:<bool:order> order:<bool:order>
courant:<bool:courant> courant:<bool:courant>
group_id:<int:group_id> 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;
""" """
# Récupération du formsemestre # Récupération du formsemestre
@ -306,8 +337,11 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_create(etudid: int = None, nip=None, ine=None): def justif_create(etudid: int = None, nip=None, ine=None):
""" """
Création d'un justificatif pour l'étudiant (etudid) Création d'un justificatif pour l'étudiant.
La requête doit avoir un content type "application/json":
DATA
----
```json
[ [
{ {
"date_debut": str, "date_debut": str,
@ -322,6 +356,10 @@ 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""}]
""" """
@ -451,15 +489,24 @@ def _create_one(
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_edit(justif_id: int): def justif_edit(justif_id: int):
""" """
Edition d'un justificatif à partir de son id Édition d'un justificatif à partir de son id.
La requête doit avoir un content type "application/json":
DATA
----
```json
{ {
"etat"?: str, "etat"?: str,
"raison"?: str "raison"?: str
"date_debut"?: str "date_debut"?: str
"date_fin"?: str "date_fin"?: str
} }
```
SAMPLES
-------
/justificatif/1/edit;{""etat"":""valide""}
/justificatif/1/edit;{""raison"":""MEDIC""}
""" """
# Récupération du justificatif à modifier # Récupération du justificatif à modifier
@ -564,15 +611,20 @@ def justif_edit(justif_id: int):
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_delete(): def justif_delete():
""" """
Suppression d'un justificatif à partir de son id Suppression d'un justificatif à partir de son id.
Forme des données envoyées :
DATA
----
```json
[ [
<justif_id:int>, <justif_id:int>,
... ...
] ]
```
SAMPLES
-------
/justificatif/delete;[2, 2, 3]
""" """
@ -647,7 +699,9 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_import(justif_id: int = None): def justif_import(justif_id: int = None):
""" """
Importation d'un fichier (création d'archive) 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é # On vérifie qu'un fichier a bien été envoyé
@ -698,7 +752,10 @@ def justif_import(justif_id: int = None):
def justif_export(justif_id: int | None = None, filename: str | None = None): def justif_export(justif_id: int | None = None, filename: str | None = None):
""" """
Retourne un fichier d'une archive d'un justificatif. Retourne un fichier d'une archive d'un justificatif.
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
La permission est `ScoView` + (`AbsJustifView` ou être l'auteur du justificatif).
> 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é # On récupère le justificatif concerné
justificatif_unique = Justificatif.get_justificatif(justif_id) justificatif_unique = Justificatif.get_justificatif(justif_id)
@ -735,15 +792,21 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_remove(justif_id: int = None): def justif_remove(justif_id: int = None):
""" """
Supression d'un fichier ou d'une archive Supression d'un fichier ou d'une archive.
{
"remove": <"all"/"list">
> Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier)
DATA
----
```json
{
"remove": <"all"/"list">,
"filenames"?: [ "filenames"?: [
<filename:str>, <filename:str>,
... ...
] ]
} }
```
""" """
# On récupère le dictionnaire # On récupère le dictionnaire
@ -808,7 +871,12 @@ def justif_remove(justif_id: int = None):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justif_list(justif_id: int = None): def justif_list(justif_id: int = None):
""" """
Liste les fichiers du justificatif Liste les fichiers du justificatif.
SAMPLES
-------
/justificatif/1/list;
""" """
# Récupération du justificatif concerné # Récupération du justificatif concerné
@ -850,7 +918,12 @@ def justif_list(justif_id: int = None):
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_justifies(justif_id: int = None): def justif_justifies(justif_id: int = None):
""" """
Liste assiduite_id justifiées par le justificatif Liste `assiduite_id` justifiées par le justificatif.
SAMPLES
-------
/justificatif/1/justifies;
""" """
# On récupère le justificatif concerné # On récupère le justificatif concerné

View File

@ -50,7 +50,12 @@ from app.scodoc.sco_utils import json_error
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def logo_list_globals(): def logo_list_globals():
"""Liste tous les logos""" """Liste des noms des logos définis pour le site ScoDoc.
SAMPLES
-------
/logos
"""
logos = list_logos()[None] logos = list_logos()[None]
return list(logos.keys()) return list(logos.keys())
@ -59,6 +64,15 @@ def logo_list_globals():
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def logo_get_global(logoname): 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.
SAMPLES
-------
/logo/B
"""
logo = find_logo(logoname=logoname) logo = find_logo(logoname=logoname)
if logo is None: if logo is None:
return json_error(404, message="logo not found") return json_error(404, message="logo not found")
@ -75,12 +89,19 @@ def _core_get_logos(dept_id) -> list:
return list(logos.keys()) return list(logos.keys())
@bp.route("/departement/<string:departement>/logos") @bp.route("/departement/<string:dept_acronym>/logos")
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def logo_get_local_by_acronym(departement): def departement_logos(dept_acronym: str):
dept_id = Departement.from_acronym(departement).id """Liste des noms des logos définis pour le département
désigné par son acronyme.
SAMPLES
-------
/departement/TAPI/logos
"""
dept_id = Departement.from_acronym(dept_acronym).id
return _core_get_logos(dept_id) return _core_get_logos(dept_id)
@ -88,7 +109,10 @@ def logo_get_local_by_acronym(departement):
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def logo_get_local_by_id(dept_id): def departement_logos_by_id(dept_id):
"""Liste des noms des logos définis pour le département
désigné par son id.
"""
return _core_get_logos(dept_id) return _core_get_logos(dept_id)
@ -108,6 +132,12 @@ def _core_get_logo(dept_id, logoname) -> Response:
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def logo_get_local_dept_by_acronym(departement, logoname): 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`
"""
dept_id = Departement.from_acronym(departement).id dept_id = Departement.from_acronym(departement).id
return _core_get_logo(dept_id, logoname) return _core_get_logo(dept_id, logoname)
@ -116,4 +146,10 @@ def logo_get_local_dept_by_acronym(departement, logoname):
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def logo_get_local_dept_by_id(dept_id, logoname): 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`
"""
return _core_get_logo(dept_id, logoname) return _core_get_logo(dept_id, logoname)

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux moduleimpl ScoDoc 9 API : accès aux moduleimpl
CATEGORY
--------
ModuleImpl
""" """
from flask_json import as_json from flask_json import as_json
@ -28,38 +32,15 @@ from app.scodoc.sco_permissions import Permission
@as_json @as_json
def moduleimpl(moduleimpl_id: int): def moduleimpl(moduleimpl_id: int):
""" """
Retourne un moduleimpl en fonction de son id Retourne le moduleimpl.
PARAMS
------
moduleimpl_id : l'id d'un moduleimpl moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat : SAMPLES
{ -------
"id": 1, /moduleimpl/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) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return modimpl.to_dict(convert_objects=True) return modimpl.to_dict(convert_objects=True)
@ -72,16 +53,11 @@ def moduleimpl(moduleimpl_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def moduleimpl_inscriptions(moduleimpl_id: int): def moduleimpl_inscriptions(moduleimpl_id: int):
"""Liste des inscriptions à ce moduleimpl """Liste des inscriptions à ce moduleimpl.
Exemple de résultat :
[ SAMPLES
{ -------
"id": 1, /moduleimpl/1/inscriptions
"etudid": 666,
"moduleimpl_id": 1234,
},
...
]
""" """
modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [i.to_dict() for i in modimpl.inscriptions] return [i.to_dict() for i in modimpl.inscriptions]
@ -93,22 +69,11 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def moduleimpl_notes(moduleimpl_id: int): def moduleimpl_notes(moduleimpl_id: int):
"""Liste des notes dans ce moduleimpl """Liste des notes dans ce moduleimpl.
Exemple de résultat :
[ SAMPLES
{ -------
"etudid": 17776, // code de l'étudiant /moduleimpl/1/notes
"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) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
app.set_sco_dept(modimpl.formsemestre.departement.acronym) app.set_sco_dept(modimpl.formsemestre.departement.acronym)

View File

@ -6,6 +6,11 @@
""" """
ScoDoc 9 API : partitions ScoDoc 9 API : partitions
CATEGORY
--------
Groupes et Partitions
""" """
from operator import attrgetter from operator import attrgetter
@ -40,22 +45,9 @@ from app.scodoc import sco_utils as scu
def partition_info(partition_id: int): def partition_info(partition_id: int):
"""Info sur une partition. """Info sur une partition.
Exemple de résultat : SAMPLES
``` -------
{ /partition/1
'bul_show_rank': False,
'formsemestre_id': 39,
'groups': [
{'id': 268, 'name': 'A', 'partition_id': 100},
{'id': 269, 'name': 'B', 'partition_id': 100}
],
'groups_editable': True,
'id': 100,
'numero': 100,
'partition_name': 'TD',
'show_in_lists': True
}
```
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -71,24 +63,11 @@ def partition_info(partition_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_partitions(formsemestre_id: int): def formsemestre_partitions(formsemestre_id: int):
"""Liste de toutes les partitions d'un formsemestre """Liste de toutes les partitions d'un formsemestre.
formsemestre_id : l'id d'un formsemestre
{
partition_id : {
"bul_show_rank": False,
"formsemestre_id": 1063,
"groups" :
group_id : {
"id" : 12,
"name" : "A",
"partition_id" : partition_id,
}
},
...
}
SAMPLES
-------
/formsemestre/1/partitions
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -112,21 +91,14 @@ def group_etudiants(group_id: int):
""" """
Retourne la liste des étudiants dans un groupe Retourne la liste des étudiants dans un groupe
(inscrits au groupe et inscrits au semestre). (inscrits au groupe et inscrits au semestre).
PARAMS
------
group_id : l'id d'un groupe group_id : l'id d'un groupe
Exemple de résultat : SAMPLES
[ -------
{ /group/1/etudiants
'civilite': 'M',
'id': 123456,
'ine': None,
'nip': '987654321',
'nom': 'MARTIN',
'nom_usuel': null,
'prenom': 'JEAN'}
},
...
]
""" """
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -152,11 +124,11 @@ def group_etudiants(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_etudiants_query(group_id: int): def group_etudiants_query(group_id: int):
"""Étudiants du groupe, filtrés par état (aucun, I, D, DEF) """Étudiants du groupe, filtrés par état (aucun, `I`, `D`, `DEF`)
QUERY QUERY
----- -----
etat:<string:etat> etat : string
""" """
etat = request.args.get("etat") etat = request.args.get("etat")
@ -186,7 +158,7 @@ def group_etudiants_query(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_set_etudiant(group_id: int, etudid: int): def group_set_etudiant(group_id: int, etudid: int):
"""Affecte l'étudiant au groupe indiqué""" """Affecte l'étudiant au groupe indiqué."""
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -248,7 +220,8 @@ def group_remove_etud(group_id: int, etudid: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_remove_etud(partition_id: int, etudid: int): 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) (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
""" """
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
@ -293,12 +266,19 @@ def partition_remove_etud(partition_id: int, etudid: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_create(partition_id: int): # partition-group-create def group_create(partition_id: int): # partition-group-create
"""Création d'un groupe dans une partition """Création d'un groupe dans une partition.
The request content type should be "application/json": DATA
----
```json
{ {
"group_name" : nom_du_groupe, "group_name" : nom_du_groupe,
} }
```
SAMPLES
-------
/partition/1/group/create;{""group_name"" : ""Nouveau Groupe""}
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -345,7 +325,7 @@ def group_create(partition_id: int): # partition-group-create
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_delete(group_id: int): def group_delete(group_id: int):
"""Suppression d'un groupe""" """Suppression d'un groupe."""
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
query = ( query = (
@ -374,7 +354,19 @@ def group_delete(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_edit(group_id: int): def group_edit(group_id: int):
"""Edit a group""" """Édition d'un groupe.
DATA
----
```json
{
"group_name" : "A1"
}
SAMPLES
-------
/group/1/edit;{""group_name"":""A1""}
"""
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
query = ( query = (
@ -415,9 +407,14 @@ def group_edit(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_set_edt_id(group_id: int, edt_id: str): def group_set_edt_id(group_id: int, edt_id: str):
"""Set edt_id for this group. """Set edt_id du groupe.
Contrairement à /edit, peut-être changé pour toute partition
ou formsemestre non verrouillé. Contrairement à `/edit`, peut-être changé pour toute partition
d'un formsemestre non verrouillé.
SAMPLES
-------
/group/1/set_edt_id/EDT_GR1
""" """
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -443,16 +440,19 @@ def group_set_edt_id(group_id: int, edt_id: str):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_create(formsemestre_id: int): def partition_create(formsemestre_id: int):
"""Création d'une partition dans un semestre """Création d'une partition dans un semestre.
The request content type should be "application/json": DATA
----
```json
{ {
"partition_name": str, "partition_name": str,
"numero":int, "numero": int,
"bul_show_rank":bool, "bul_show_rank": bool,
"show_in_lists":bool, "show_in_lists": bool,
"groups_editable":bool "groups_editable": bool
} }
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -508,8 +508,13 @@ def partition_create(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_set_partitions_order(formsemestre_id: int): def formsemestre_set_partitions_order(formsemestre_id: int):
"""Modifie l'ordre des partitions du formsemestre """Modifie l'ordre des partitions du formsemestre.
JSON args: [partition_id1, partition_id2, ...]
DATA
----
```json
[ partition_id1, partition_id2, ... ]
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -520,7 +525,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
if not formsemestre.can_change_groups(): if not formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(401, "opération non autorisée")
partition_ids = request.get_json(force=True) # may raise 400 Bad Request partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, int) and not all( if not isinstance(partition_ids, list) and not all(
isinstance(x, int) for x in partition_ids isinstance(x, int) for x in partition_ids
): ):
return json_error( return json_error(
@ -549,8 +554,13 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_order_groups(partition_id: int): def partition_order_groups(partition_id: int):
"""Modifie l'ordre des groupes de la partition """Modifie l'ordre des groupes de la partition.
JSON args: [group_id1, group_id2, ...]
DATA
----
```json
[ group_id1, group_id2, ... ]
```
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -561,7 +571,7 @@ def partition_order_groups(partition_id: int):
if not partition.formsemestre.can_change_groups(): if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(401, "opération non autorisée")
group_ids = request.get_json(force=True) # may raise 400 Bad Request group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, int) and not all( if not isinstance(group_ids, list) and not all(
isinstance(x, int) for x in group_ids isinstance(x, int) for x in group_ids
): ):
return json_error( return json_error(
@ -586,10 +596,13 @@ def partition_order_groups(partition_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_edit(partition_id: int): def partition_edit(partition_id: int):
"""Modification d'une partition dans un semestre """Modification d'une partition dans un semestre.
The request content type should be "application/json" Tous les champs sont optionnels.
All fields are optional:
DATA
----
```json
{ {
"partition_name": str, "partition_name": str,
"numero":int, "numero":int,
@ -597,6 +610,11 @@ def partition_edit(partition_id: int):
"show_in_lists":bool, "show_in_lists":bool,
"groups_editable":bool "groups_editable":bool
} }
```
SAMPLES
-------
/partition/1/edit;{""bul_show_rank"":1}
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -631,9 +649,8 @@ def partition_edit(partition_id: int):
for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"): for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"):
value = data.get(boolean_field) value = data.get(boolean_field)
value = scu.to_bool(value) if value is not None else None
if value is not None and value != getattr(partition, boolean_field): if value is not None and value != getattr(partition, boolean_field):
if not isinstance(value, bool):
return json_error(API_CLIENT_ERROR, f"invalid type for {boolean_field}")
if boolean_field == "groups_editable" and partition.is_parcours(): if boolean_field == "groups_editable" and partition.is_parcours():
return json_error( return json_error(
API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}" API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}"
@ -660,9 +677,9 @@ def partition_edit(partition_id: int):
def partition_delete(partition_id: int): def partition_delete(partition_id: int):
"""Suppression d'une partition (et de tous ses groupes). """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. 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. sont désinscrits des parcours.
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)

View File

@ -3,12 +3,18 @@ from app import db, log
from app.api import api_bp as bp from app.api import api_bp as bp
from app.auth.logic import basic_auth, token_auth from app.auth.logic import basic_auth, token_auth
"""
CATEGORY
--------
Authentification API
"""
@bp.route("/tokens", methods=["POST"]) @bp.route("/tokens", methods=["POST"])
@basic_auth.login_required @basic_auth.login_required
@as_json @as_json
def token_get(): def token_get():
"renvoie un jeton jwt pour l'utilisateur courant" "Renvoie un jeton jwt pour l'utilisateur courant."
token = basic_auth.current_user().get_token() token = basic_auth.current_user().get_token()
log(f"API: giving token to {basic_auth.current_user()}") log(f"API: giving token to {basic_auth.current_user()}")
db.session.commit() db.session.commit()
@ -18,7 +24,7 @@ def token_get():
@bp.route("/tokens", methods=["DELETE"]) @bp.route("/tokens", methods=["DELETE"])
@token_auth.login_required @token_auth.login_required
def token_revoke(): def token_revoke():
"révoque le jeton de l'utilisateur courant" "Révoque le jeton de l'utilisateur courant."
user = token_auth.current_user() user = token_auth.current_user()
user.revoke_token() user.revoke_token()
db.session.commit() db.session.commit()

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux utilisateurs ScoDoc 9 API : accès aux utilisateurs
CATEGORY
--------
Utilisateurs
""" """
from flask import g, request from flask import g, request
@ -32,7 +36,11 @@ from app.scodoc.sco_utils import json_error
@as_json @as_json
def user_info(uid: int): def user_info(uid: int):
""" """
Info sur un compte utilisateur scodoc Info sur un compte utilisateur ScoDoc.
SAMPLES
-------
/user/2
""" """
user: User = db.session.get(User, uid) user: User = db.session.get(User, uid)
if user is None: if user is None:
@ -53,7 +61,11 @@ def user_info(uid: int):
@as_json @as_json
def users_info_query(): def users_info_query():
"""Utilisateurs, filtrés par dept, active ou début nom """Utilisateurs, filtrés par dept, active ou début nom
Exemple:
```
/users/query?departement=dept_acronym&active=1&starts_with=<string:nom> /users/query?departement=dept_acronym&active=1&starts_with=<string:nom>
```
Seuls les utilisateurs "accessibles" (selon les permissions) sont retournés. 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 Si accès via API web, le département de l'URL est ignoré, seules
@ -61,9 +73,9 @@ def users_info_query():
QUERY QUERY
----- -----
active:<bool:active> active: bool
departement:<string:departement> departement: string
starts_with:<string:starts_with> starts_with: string
""" """
query = User.query query = User.query
@ -113,7 +125,10 @@ def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
@as_json @as_json
def user_create(): def user_create():
"""Création d'un utilisateur """Création d'un utilisateur
The request content type should be "application/json":
DATA
----
```json
{ {
"active":bool (default True), "active":bool (default True),
"dept": str or null, "dept": str or null,
@ -122,6 +137,7 @@ def user_create():
"user_name": str, "user_name": str,
... ...
} }
```
""" """
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
user_name = args.get("user_name") user_name = args.get("user_name")
@ -158,8 +174,10 @@ def user_create():
@permission_required(Permission.UsersAdmin) @permission_required(Permission.UsersAdmin)
@as_json @as_json
def user_edit(uid: int): def user_edit(uid: int):
"""Modification d'un utilisateur """Modification d'un utilisateur.
Champs modifiables: Champs modifiables:
```json
{ {
"dept": str or null, "dept": str or null,
"nom": str, "nom": str,
@ -167,6 +185,7 @@ def user_edit(uid: int):
"active":bool "active":bool
... ...
} }
```
""" """
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
user: User = User.query.get_or_404(uid) user: User = User.query.get_or_404(uid)
@ -205,12 +224,21 @@ def user_edit(uid: int):
@permission_required(Permission.UsersAdmin) @permission_required(Permission.UsersAdmin)
@as_json @as_json
def user_password(uid: int): def user_password(uid: int):
"""Modification du mot de passe d'un utilisateur """Modification du mot de passe d'un utilisateur.
Champs modifiables:
Si le mot de passe ne convient pas, erreur 400.
DATA
----
```json
{ {
"password": str "password": str
} }
Si le mot de passe ne convient pas, erreur 400. ```
SAMPLES
-------
/user/3/password;{""password"" : ""rePlaCemeNT456averylongandcomplicated""}
""" """
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
user: User = User.query.get_or_404(uid) user: User = User.query.get_or_404(uid)
@ -243,7 +271,7 @@ def user_password(uid: int):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def user_role_add(uid: int, role_name: str, dept: str = None): def user_role_add(uid: int, role_name: str, dept: str = None):
"""Add a role in the given dept to the user""" """Ajoute un rôle à l'utilisateur dans le département donné."""
user: User = User.query.get_or_404(uid) user: User = User.query.get_or_404(uid)
role: Role = Role.query.filter_by(name=role_name).first_or_404() role: Role = Role.query.filter_by(name=role_name).first_or_404()
if dept is not None: # check if dept is not None: # check
@ -272,7 +300,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def user_role_remove(uid: int, role_name: str, dept: str = None): def user_role_remove(uid: int, role_name: str, dept: str = None):
"""Remove the role (in the given dept) from the user""" """Retire le rôle (dans le département donné) à cet utilisateur."""
user: User = User.query.get_or_404(uid) user: User = User.query.get_or_404(uid)
role: Role = Role.query.filter_by(name=role_name).first_or_404() role: Role = Role.query.filter_by(name=role_name).first_or_404()
if dept is not None: # check if dept is not None: # check
@ -299,7 +327,12 @@ def user_role_remove(uid: int, role_name: str, dept: str = None):
@permission_required(Permission.UsersView) @permission_required(Permission.UsersView)
@as_json @as_json
def permissions_list(): def permissions_list():
"""Liste des noms de permissions définies""" """Liste des noms de permissions définies.
SAMPLES
-------
/permissions
"""
return list(Permission.permission_by_name.keys()) return list(Permission.permission_by_name.keys())
@ -310,7 +343,12 @@ def permissions_list():
@permission_required(Permission.UsersView) @permission_required(Permission.UsersView)
@as_json @as_json
def role_get(role_name: str): def role_get(role_name: str):
"""Un rôle""" """Un rôle.
SAMPLES
-------
/role/Ens
"""
return Role.query.filter_by(name=role_name).first_or_404().to_dict() return Role.query.filter_by(name=role_name).first_or_404().to_dict()
@ -321,7 +359,12 @@ def role_get(role_name: str):
@permission_required(Permission.UsersView) @permission_required(Permission.UsersView)
@as_json @as_json
def roles_list(): def roles_list():
"""Tous les rôles définis""" """Tous les rôles définis.
SAMPLES
-------
/roles
"""
return [role.to_dict() for role in Role.query] return [role.to_dict() for role in Role.query]
@ -338,7 +381,7 @@ def roles_list():
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def role_permission_add(role_name: str, perm_name: str): def role_permission_add(role_name: str, perm_name: str):
"""Add permission to role""" """Ajoute une permission à un rôle."""
role: Role = Role.query.filter_by(name=role_name).first_or_404() role: Role = Role.query.filter_by(name=role_name).first_or_404()
permission = Permission.get_by_name(perm_name) permission = Permission.get_by_name(perm_name)
if permission is None: if permission is None:
@ -363,7 +406,7 @@ def role_permission_add(role_name: str, perm_name: str):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def role_permission_remove(role_name: str, perm_name: str): def role_permission_remove(role_name: str, perm_name: str):
"""Remove permission from role""" """Retire une permission d'un rôle."""
role: Role = Role.query.filter_by(name=role_name).first_or_404() role: Role = Role.query.filter_by(name=role_name).first_or_404()
permission = Permission.get_by_name(perm_name) permission = Permission.get_by_name(perm_name)
if permission is None: if permission is None:
@ -382,10 +425,19 @@ def role_permission_remove(role_name: str, perm_name: str):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def role_create(role_name: str): def role_create(role_name: str):
"""Create a new role with permissions. """Création d'un nouveau rôle avec les permissions données.
DATA
----
```json
{ {
"permissions" : [ 'ScoView', ... ] "permissions" : [ 'ScoView', ... ]
} }
```
SAMPLES
-------
/role/create/customRole;{""permissions"": [""ScoView"", ""UsersView""]}
""" """
role: Role = Role.query.filter_by(name=role_name).first() role: Role = Role.query.filter_by(name=role_name).first()
if role: if role:
@ -410,11 +462,16 @@ def role_create(role_name: str):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def role_edit(role_name: str): def role_edit(role_name: str):
"""Edit a role. On peut spécifier un nom et/ou des permissions. """Édition d'un rôle. On peut spécifier un nom et/ou des permissions.
DATA
----
```json
{ {
"name" : name "name" : name
"permissions" : [ 'ScoView', ... ] "permissions" : [ 'ScoView', ... ]
} }
```
""" """
role: Role = Role.query.filter_by(name=role_name).first_or_404() role: Role = Role.query.filter_by(name=role_name).first_or_404()
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
@ -442,7 +499,12 @@ def role_edit(role_name: str):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def role_delete(role_name: str): def role_delete(role_name: str):
"""Delete a role""" """Suppression d'un rôle.
SAMPLES
-------
/role/customRole/delete
"""
role: Role = Role.query.filter_by(name=role_name).first_or_404() role: Role = Role.query.filter_by(name=role_name).first_or_404()
db.session.delete(role) db.session.delete(role)
db.session.commit() db.session.commit()

View File

@ -348,7 +348,7 @@ class Assiduite(ScoDocModel):
""" """
Retourne le module associé à l'assiduité Retourne le module associé à l'assiduité
Si traduire est vrai, retourne le titre du module précédé du code Si traduire est vrai, retourne le titre du module précédé du code
Sinon rentourne l'objet Module ou None Sinon retourne l'objet Module ou None
""" """
if self.moduleimpl_id is not None: if self.moduleimpl_id is not None:
@ -358,7 +358,7 @@ class Assiduite(ScoDocModel):
return f"{mod.code} {mod.titre}" return f"{mod.code} {mod.titre}"
return mod return mod
elif self.external_data is not None and "module" in self.external_data: if self.external_data is not None and "module" in self.external_data:
return ( return (
"Autre module (pas dans la liste)" "Autre module (pas dans la liste)"
if self.external_data["module"] == "Autre" if self.external_data["module"] == "Autre"

View File

@ -52,6 +52,17 @@ class Departement(db.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>" 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): def to_dict(self, with_dept_name=True, with_dept_preferences=False):
data = { data = {
"id": self.id, "id": self.id,

View File

@ -242,6 +242,16 @@ class GroupDescr(ScoDocModel):
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">""" f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
) )
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
Exclude `partition_id` : a group cannot be moved from a partition to another.
"""
return super().filter_model_attributes(
data,
excluded=(excluded or set()) | {"partition_id"},
)
def get_nom_with_part(self, default="-") -> str: def get_nom_with_part(self, default="-") -> str:
"""Nom avec partition: 'TD A' """Nom avec partition: 'TD A'
Si groupe par défaut (tous), utilise default ou "-" Si groupe par défaut (tous), utilise default ou "-"

View File

@ -0,0 +1,280 @@
{# Documentation de l'API ScoDoc 9 #}
# API pour ScoDoc 9
!!! warning "Attention"
*Page générée par la commande `flask gen-api-doc`. Ne pas modifier manuellement.*
L'API ScoDoc permet à des applications tierces d'interroger ScoDoc. Elle offre
un accès aux objets de l'application via une API REST.
Les composants internes de ScoDoc, et notamment le schéma de la base de données,
sont susceptibles d'évoluer à tout moment sans préavis: il est vivement
déconseillé d'écrire une extension ne passant pas par l'API. Vous ne devez même
pas supposer qu'il existe une base de données SQL.
La version ScoDoc 9 a introduit une nouvelle API avec un nouveau mécanisme d'authentification.
**Les clients de l'ancienne API ScoDoc 7 doivent être adaptés pour fonctionner avec ScoDoc 9.**
Cette API est encore incomplète: n'hésitez pas à demander de nouveaux accès ([contacts](Contact.md))
(et canal `#API` du Discord développeurs si vous y avez accès).
L'API fournit des données JSON, sauf exception (bulletins PDF par exemple).
Les objets ScoDoc manipulables sont identifiés par des id numériques.
* `etudid` : étudiant
* `formation_id` : un programme de formation (page "programmes");
* `ue_id` : une UE dans un programme;
* `matiere_id` : une matière dans un programme;
* `module_id` : un module dans un programme;
* `moduleimpl_id` : un module réalisé dans un semestre;
* `formsemestre_id` : un "semestre" de formation.
(pour plus de précisions, voir le [guide développeurs](GuideDeveloppeurs.md))
L'URL complète est de la forme:
`https://scodoc.example.com/ScoDoc/api/<fonction>`.
(<fonction> à choisir dans [Référence](#reference))
## Configuration de ScoDoc pour utiliser l'API
Il est nécessaire de disposer d'un compte utilisateur avec les droits adéquats.
Les droits à accorder dépendent des fonctionnalités nécessaires. la permission
`ScoView` est généralement suffisante car elle permet toutes les consultations.
Cependant si, par l'API, on veut effectuer des opérations de modification ou
encore consulter les comptes utilisateurs, d'autres droits (`ScoChangeGroups`,
`UsersView`, `ScoSuperAdmin`, ...) peuvent être requis. La consultation du
[tableau récapitulatif](#tableau-recapitulatif-des-entrees-de-lapi) ou la ligne
`permission`de chaque entrée vous donnera la permission requise pour chaque
opération.
En général, il est recommandé de créer un rôle, de lui attribuer les permissions
que l'on veut utiliser, puis de créer un utilisateur ayant ce rôle.
En ligne de commande, cela peut se faire comme suit (voir détail des commandes
[sur le guide de configuration](GuideConfig.md)).
```bash
# se connecter comme utilisateur scodoc
su - scodoc
# Créer un rôle
flask create-role LecteurAPI
# Lui donner les droits nécessaires: ici ScoView
flask edit-role LecteurAPI -a ScoView
# Créer un nouvel utilisateur avec ce rôle:
flask user-create lecteur_api LecteurAPI @all
# Ou bien, si on veut utiliser un compte existant:
# associer notre rôle à un utilisateur
flask user-role lecteur_api -a LecteurAPI
# Au besoin, changer le mot de passe de l'utilisateur
# (on aura besoin de ce mot de passe dans la configuration du client d'API)
flask user-password lecteur_api
...
```
Si vous êtes intéressé par le développement, voir
* [la section sur les tests unitaires de l'API](TestsScoDoc.md#tests-de-lapi-scodoc9);
* [la documentation développeurs](GuideDeveloppeurs.md) et sur les [vues de l'API](DevInternals.md#vues-de-lapi-et-permissions).
!!! note
* Si vous utilisez le CAS, pensez à laisser les comptes utilisateurs API se
connecter via ScoDoc sans CAS. Pour cela, cocher l'option
*Autorise connexion via CAS si CAS est activé*
dans leur formulaire de configuration.
* Si l'utilisateur est associé à un département (cas des comptes créés via l'interface Web),
il ne pourra accéder à l'API que via une *route départementale*, c'est à dire une route comprenant
l'acronyme de son département, de la forme `https://...//ScoDoc/DEPARTEMENT/api/...`.
## Essais avec HTTPie
[HTTPie](https://httpie.io/) est un client universel livre et gratuit très commode, disponible
pour Windows, Linux, en ligne de commande ou interface graphique.
Exemple d'utilisation en ligne de commande et interroger votre ScoDoc pour
obtenir la liste des départements:
```bash
http -a USER:PASSWORD POST 'http://localhost:5000/ScoDoc/api/tokens'
```
Qui affiche:
```text
HTTP/1.1 200 OK
Content-Length: 50
Content-Type: application/json
Date: Thu, 05 May 2022 04:29:33 GMT
{
"token": "jS7iVl1234cRDzboAfO5xseE0Ain6Zyz"
}
```
(remplacer `USER:PASSWORD` par les identifiants de votre utilisateur et adapter
l'URL qui est ici celle d'un client local sur le serveur de test).
Avec ce jeton (*token*), on peut interroger le serveur:
```bash
http GET http://localhost:5000/ScoDoc/api/departements "Authorization:Bearer jS7iVlH1234cRDzboAfO5xseE0Ain6Zyz"
```
qui affiche par exemple:
```text
HTTP/1.1 200 OK
Content-Length: 151
Content-Type: application/json
Date: Thu, 05 May 2022 05:21:33 GMT
[
{
"acronym": "TAPI",
"date_creation": "Wed, 04 May 2022 21:09:25 GMT",
"description": null,
"id": 1,
"visible": true
}
]
```
## Fonctions d'API ScoDoc 9
La documentation ci-dessous concerne la nouvelle API, disponible à partir de la
version de ScoDoc 9.3.25.
### Accès à l'API REST
L'API est accessible à l'adresse:
`https://scodoc.monsite.tld/ScoDoc/api/<fonction>`, et aussi via les *routes
départementales* de la forme
`https://scodoc.monsite.tld/ScoDoc/<dept_acronyme>/api/<fonction>` pour un accès
avec des droits restreints au département indiqué. La liste des `<fonctions>` est
donnée ci-dessous dans [Référence](#reference).
#### Authentification
Lors de votre authentification (*connexion avec login et mot de passe*) à Scodoc, il
vous sera attribué un jeton (token jwt *généré automatiquement*) vous permettant
d'utiliser l'api suivant les droits correspondant à votre session.
Pour obtenir le jeton, il faut un compte sur ScoDoc (`user_name`et `password`).
Les autorisations et rôles sont gérés exactement comme pour l'application.
Exemple avec `curl` (un outil en ligne de commande présent sur la plupart des
systèmes, voir plus haut pour la même chose avec la commande `http`):
```bash
curl -u user_name:password --request POST https://SERVEUR/ScoDoc/api/tokens
```
où `SERVEUR` est l'adresse (IP ou nom) de votre serveur.
La réponse doit ressembler à ceci:
```json
{
"token": "LuXXxk+i74TXYZZl8MulgbiCGmVHXXX"
}
```
Vous trouverez dans `/opt/scodoc/tests/api/exemple-api-basic.py` un exemple
complet en python d'interrogation de l'API.
#### Codes HTTP
Chaque appel à l'API donne lieu à une réponse retournant un code spécifique en
fonction du résultat obtenu. L'analyse de ce code vous permet de vous assurer
que la requête a été traitée avec succès.
Tous les codes >= 400 indiquent que la requête n'a pas été traitée avec succès
par le serveur ScoDoc.
* [200](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/200) : OK.
* [401](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/401) : Authentification nécessaire. (jeton non précisé ou invalide)
* [403](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/403) : Action
non autorisée pour l'utilisateur associé au jeton.
* [404](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/401) : Adresse
incorrecte, paramètre manquant ou invalide, ou objet inexistant.
* [500](https://developer.mozilla.org/fr/docs/Web/HTTP/Status/500) : Erreur
inconnue côté serveur.
## Règles générales
* une route s'écrit comme une suite de noms et d'identifiants;
* les noms token, département, formation, formsemestre, groupe, etudiant,
bulletin, absence, logo, programme, évaluation, résultat, décision désignent
des types d'objets;
* les noms (verbes ou groupes verbaux): set_etudiant, remove_etudiant, query,
create, delete, edit, order sont des actions;
* les noms restants (ids, courants, long, ...) sont des options, les autres noms
sont des options ou des actions;
* le dernier nom apparaissant sur une route donne le type d'objet renvoyé. Ce
nom peut apparaître au singulier ou au pluriel.
* au singulier un seul objet est renvoyé, si aucun objet n'est trouvé, retourne un 404;
* au pluriel une collection d'objets est renvoyée, si aucun objet n'est
trouvé, retourne une collection vide.
* un type d'objet au singulier est généralement suivi immédiatement de son
identifiant (unique). Exception: pour un étudiant, on peut également utiliser
le NIP ou l'INE (qui ne sont pas uniques dans la base car un étudiant de même
INE/NIP peut passer par plusieurs départements).
## Référence
La [carte syntaxique](#carte-syntaxique) vous permet de retrouver une entrée à
partir de sa syntaxe (le `?` amène sur la documentation associée).
Le [tableau récapitulatif](#tableau-recapitulatif-des-entrees-de-lapi) vous
permet de rechercher une entrée à partir du résultat attendu.
### Carte syntaxique
<div style="overflow: scroll;">
<div style="width: 1200px;">
![carte_syntaxique](img/API_Chart.svg)
</div>
</div>
(carte générée avec `flask gen-api-doc`)
### Tableau récapitulatif des entrées de l'API
{{table_api|safe}}
(table générée avec `flask gen-api-doc`)
#### Note sur les exemples d'utilisation
Pour uniformiser les résultats des exemples, ceux sont soumis à quelques post-traitements non réalisés par l'API.
- les clés sont triées (ce n'est pas toujours garanti);
- les listes de plus de 2 éléments sont tronquées à 2 éléments, la fin de la liste étant
représentée par la notation en json '...';
- les dates (au format ISO) sont systématiquement remplacées par une date fixe et ne sont pas réalistes.
{{doc_api|safe}}
---------------------------------------------------------------------------------------------------------------------
### En savoir plus
Voir exemples d'utilisation de l'API en Python, dans `tests/api/`.
!!! note "Voir aussi"
- [Guide configuration et ligne de commande](GuideConfig.md)
- [Guide administrateur ScoDoc](GuideAdminSys.md)
- [ServicesXml](ServicesXml.md) : anciens web services XML (obsolète)
- [FAQ](FAQ.md)
- [Contacts](Contact.md)

View File

@ -0,0 +1,32 @@
{# Template pour la doc mardown d'un point d'entrée de l'API #}
#### **`{{doc.nom}}`**
{% if doc.routes %}
{% if doc.routes|length == 1 %}
* **Route:** `{{doc.routes[0]|safe}}`
{% else %}
* **Routes:**
{% for route in doc.routes %}
* `{{route|safe}}`
{% endfor %}
{% endif %}
{% endif %}
* **Méthode:** `{{doc.method}}`
* **Permission:** `{{doc.permission}}`
{% if doc.params %}
* **Paramètres:**
{% for param in doc.params %}
* `{{param.nom|safe}}` : {{param.description|safe}}
{% endfor %}
{% endif %}
{% if doc.description %}
* **Description:** {{doc.description|safe}}
{% endif %}
{% if doc.data %}
* **Data:** {{doc.data|safe}}
{% endif %}
{% if doc.sample %}
* **Exemple de résultat:** [{{doc.sample.nom}}](./samples/sample_{{doc.sample.href}})
{% else %}
{% endif %}

View File

@ -744,10 +744,10 @@ def generate_ens_calendars(): # generate-ens-calendars
@click.option( @click.option(
"-e", "-e",
"--endpoint", "--endpoint",
default="api", default="api.",
help="Endpoint à partir duquel générer la carte des routes", help="Endpoint à partir duquel générer la documentation des routes",
) )
@with_appcontext @with_appcontext
def gen_api_map(endpoint): def gen_api_doc(endpoint): # gen-api-map
"""Génère la carte des routes de l'API.""" """Génère la documentation des routes de l'API."""
tools.gen_api_map(app, endpoint_start=endpoint) tools.gen_api_doc(app, endpoint_start=endpoint)

40
tests/api/api_shell.py Normal file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env python
"""Script pour tester l'API en mode interactif
Utilisation:
```py
python -i tests/api/api_shell.py
```
"""
import pdb
from pprint import pprint as pp
from setup_test_api import (
API_PASSWORD,
API_URL,
API_USER,
APIError,
CHECK_CERTIFICATE,
get_auth_headers,
GET,
POST,
SCODOC_URL,
set_headers,
)
set_headers(get_auth_headers("admin_api", "admin_api"))
print(
"""
Connecté au serveur ScoDoc. Vous pouvez utiliser:
GET( route )
POST( route, data )
Exemple avec pretty print:
pp(GET("/departements")[0])
"""
)
pp(GET("/departements")[0])

View File

@ -30,7 +30,7 @@ from setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
get_auth_headers, get_auth_headers,
GET, GET,
POST_JSON, POST,
SCODOC_URL, SCODOC_URL,
) )

View File

@ -23,7 +23,7 @@ from setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
get_auth_headers, get_auth_headers,
GET, GET,
POST_JSON, POST,
SCODOC_URL, SCODOC_URL,
) )

View File

@ -39,7 +39,7 @@ from setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
get_auth_headers, get_auth_headers,
GET, GET,
POST_JSON, POST,
SCODOC_URL, SCODOC_URL,
) )
@ -127,14 +127,12 @@ group_id = 5315
POST(f"/group/{group_id}/set_etudiant/{etudid}", headers=HEADERS) POST(f"/group/{group_id}/set_etudiant/{etudid}", headers=HEADERS)
POST_JSON( POST(f"/partition/{pid}/group/create", data={"group_name": "Omega10"}, headers=HEADERS)
f"/partition/{pid}/group/create", data={"group_name": "Omega10"}, headers=HEADERS
)
partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS) partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS)
pp(partitions) pp(partitions)
POST_JSON(f"/group/5559/delete", headers=HEADERS) POST(f"/group/5559/delete", headers=HEADERS)
POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}, headers=HEADERS) POST(f"/group/5327/edit", data={"group_name": "TDXXX"}, headers=HEADERS)
# --------- Toutes les bulletins, un à un, et les décisions de jury d'un semestre # --------- Toutes les bulletins, un à un, et les décisions de jury d'un semestre
formsemestre_id = 911 formsemestre_id = 911
@ -178,19 +176,19 @@ etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=HEADERS)[10]
etudid = etud["id"] etudid = etud["id"]
# 1- Crée une partition, puis la change de nom # 1- Crée une partition, puis la change de nom
js = POST_JSON( js = POST(
f"/formsemestre/{formsemestre_id}/partition/create", f"/formsemestre/{formsemestre_id}/partition/create",
data={"partition_name": "PART"}, data={"partition_name": "PART"},
) )
partition_id = js["id"] partition_id = js["id"]
POST_JSON( POST(
f"/partition/{partition_id}/edit", f"/partition/{partition_id}/edit",
data={"partition_name": "PART1", "show_in_lists": True}, data={"partition_name": "PART1", "show_in_lists": True},
headers=HEADERS, headers=HEADERS,
) )
# 2- Crée un groupe # 2- Crée un groupe
js = POST_JSON( js = POST(
f"/partition/{partition_id}/group/create", f"/partition/{partition_id}/group/create",
data={"group_name": "G1"}, data={"group_name": "G1"},
headers=HEADERS, headers=HEADERS,
@ -198,28 +196,28 @@ js = POST_JSON(
group_1 = js["id"] group_1 = js["id"]
# 3- Crée deux autres groupes # 3- Crée deux autres groupes
js = POST_JSON( js = POST(
f"/partition/{partition_id}/group/create", f"/partition/{partition_id}/group/create",
data={"group_name": "G2"}, data={"group_name": "G2"},
headers=HEADERS, headers=HEADERS,
) )
js = POST_JSON( js = POST(
f"/partition/{partition_id}/group/create", f"/partition/{partition_id}/group/create",
data={"group_name": "G3"}, data={"group_name": "G3"},
headers=HEADERS, headers=HEADERS,
) )
# 4- Affecte étudiant au groupe G1 # 4- Affecte étudiant au groupe G1
POST_JSON(f"/group/{group_1}/set_etudiant/{etudid}", headers=HEADERS) POST(f"/group/{group_1}/set_etudiant/{etudid}", headers=HEADERS)
# 5- retire du groupe # 5- retire du groupe
POST_JSON(f"/group/{group_1}/remove_etudiant/{etudid}", headers=HEADERS) POST(f"/group/{group_1}/remove_etudiant/{etudid}", headers=HEADERS)
# 6- affecte au groupe G2 # 6- affecte au groupe G2
partition = GET(f"/partition/{partition_id}") partition = GET(f"/partition/{partition_id}")
assert len(partition["groups"]) == 3 assert len(partition["groups"]) == 3
group_2 = [g for g in partition["groups"].values() if g["group_name"] == "G2"][0]["id"] group_2 = [g for g in partition["groups"].values() if g["group_name"] == "G2"][0]["id"]
POST_JSON(f"/group/{group_2}/set_etudiant/{etudid}", headers=HEADERS) POST(f"/group/{group_2}/set_etudiant/{etudid}", headers=HEADERS)
# 7- Membres du groupe # 7- Membres du groupe
etuds_g2 = GET(f"/group/{group_2}/etudiants", headers=HEADERS) etuds_g2 = GET(f"/group/{group_2}/etudiants", headers=HEADERS)
@ -229,7 +227,7 @@ assert etuds_g2[0]["id"] == etudid
# 8- Ordres des groupes # 8- Ordres des groupes
group_3 = [g for g in partition["groups"].values() if g["group_name"] == "G3"][0]["id"] group_3 = [g for g in partition["groups"].values() if g["group_name"] == "G3"][0]["id"]
POST_JSON( POST(
f"/partition/{partition_id}/groups/order", f"/partition/{partition_id}/groups/order",
data=[group_2, group_1, group_3], data=[group_2, group_1, group_3],
headers=HEADERS, headers=HEADERS,
@ -242,7 +240,7 @@ new_groups = [
assert new_groups == [group_2, group_1, group_3] assert new_groups == [group_2, group_1, group_3]
# 9- Suppression # 9- Suppression
POST_JSON(f"/partition/{partition_id}/delete") POST(f"/partition/{partition_id}/delete")
# ------ # ------
# Tests accès API: # Tests accès API:
@ -260,13 +258,13 @@ POST_JSON(f"/partition/{partition_id}/delete")
""" """
# #
POST_JSON( POST(
"/partition/2264/groups/order", "/partition/2264/groups/order",
data=[5563, 5562, 5561, 5560, 5558, 5557, 5316, 5315], data=[5563, 5562, 5561, 5560, 5558, 5557, 5316, 5315],
headers=HEADERS, headers=HEADERS,
) )
POST_JSON( POST(
"/formsemestre/1063/partitions/order", "/formsemestre/1063/partitions/order",
data=[2264, 2263, 2265, 2266, 2267, 2372, 2378], data=[2264, 2263, 2265, 2266, 2267, 2372, 2378],
headers=HEADERS, headers=HEADERS,

View File

@ -41,7 +41,7 @@ from setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
get_auth_headers, get_auth_headers,
GET, GET,
POST_JSON, POST,
SCODOC_URL, SCODOC_URL,
) )

View File

@ -1,170 +0,0 @@
#!/usr/bin/env python3
# -*- mode: python -*-
# -*- coding: utf-8 -*-
# OBSOLETE - NE PLUS UTILISER CETTE API
# VOIR https://scodoc.org/ScoDoc9API/
"""Exemple connexion sur ScoDoc 9 et utilisation de l'ancienne API ScoDoc 7
à la mode "PHP": les gens passaient directement __ac_name et __ac_password
dans chaque requête, en POST ou en GET.
Cela n'a jamais été documenté mais était implicitement supporté. C'est "deprecated"
et ne sera plus supporté à partir de juillet 2022.
Ce script va tester:
- Liste semestres
- Liste modules
- Creation d'une évaluation
- Saisie d'une note
Utilisation: créer les variables d'environnement: (indiquer les valeurs
pour le serveur ScoDoc que vous voulez interroger)
export SCODOC_URL="https://scodoc.xxx.net/"
export SCODOC_USER="xxx"
export SCODOC_PASSWD="xxx"
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
"""
from dotenv import load_dotenv
import json
import os
import pdb
import requests
import urllib3
from pprint import pprint as pp
# --- Lecture configuration (variables d'env ou .env)
BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env"))
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"]
SCODOC_DEPT = os.environ["SCODOC_DEPT"]
DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite"
SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"]
print(f"SCODOC_URL={SCODOC_URL}")
# ---
if not CHECK_CERTIFICATE:
urllib3.disable_warnings()
class ScoError(Exception):
pass
def GET(path: str, params=None, errmsg=None):
"""Get and returns as JSON"""
# ajoute auth
params["__ac_name"] = SCODOC_USER
params["__ac_password"] = SCODOC_PASSWORD
r = requests.get(
DEPT_URL + "/" + path, params=params, verify=CHECK_CERTIFICATE, timeout=10
)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.json() # decode la reponse JSON
def POST(path: str, data: dict, errmsg=None):
"""Post"""
data["__ac_name"] = data.get("__ac_name", SCODOC_USER)
data["__ac_password"] = data.get("__ac_password", SCODOC_PASSWORD)
r = requests.post(
DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE, timeout=10
)
return r
# ---
# pas besoin d'ouvrir une session, on y va directement:
# --- Recupere la liste de tous les semestres:
sems = GET("Notes/formsemestre_list", params={"fmt": "json"})
# sems est une liste de semestres (dictionnaires)
for sem in sems:
if sem["etat"]:
break
if sem["etat"] == "0":
raise ScoError("Aucun semestre non verrouillé !")
# Affiche le semestre trouvé:
pp(sem)
# Liste des étudiants dans le 1er semestre non verrouillé:
group_list = GET(
"groups_view",
params={
"formsemestre_id": sem["formsemestre_id"],
"with_codes": 1,
"fmt": "json",
},
)
if not group_list:
# config inadaptée pour les tests...
raise ScoError("aucun étudiant inscrit dans le semestre")
etud = group_list[0] # le premier étudiant inscrit ici
# test un POST
r = POST(
"Absences/AddBilletAbsence",
{
"begin": "2021-10-25",
"end": "2021-10-26",
"description": "test API scodoc7",
"etudid": etud["etudid"],
},
)
assert r.status_code == 200
assert r.text.startswith('<?xml version="1.0" encoding="utf-8"?>')
assert "billet_id" in r.text
# Essai avec un compte invalide
r_invalid = POST(
"Absences/AddBilletAbsence",
{
"__ac_name": "xxx",
"begin": "2021-10-25",
"end": "2021-10-26",
"description": "test API scodoc7",
"etudid": etud["etudid"],
},
)
assert r_invalid.status_code == 403 # compte invalide => not authorized
# AddBilletAbsence en json
r = POST(
"Absences/AddBilletAbsence",
{
"begin": "2021-10-25",
"end": "2021-10-26",
"description": "test API scodoc7",
"etudid": etud["etudid"],
"xml_reply": 0,
},
)
assert r.status_code == 200
assert isinstance(json.loads(r.text)[0]["billet_id"], int)
# Les fonctions ci-dessous ne fonctionnent plus en ScoDoc 9
# Voir https://scodoc.org/git/viennet/ScoDoc/issues/149
# # ---- Liste les modules et prend le premier
# mods = GET("/Notes/moduleimpl_list", params={"formsemestre_id": sem["formsemestre_id"]})
# print(f"{len(mods)} modules dans le semestre {sem['titre']}")
# mod = mods[0]
# # ---- Etudiants inscrits dans ce module
# inscrits = GET(
# "Notes/do_moduleimpl_inscription_list",
# params={"moduleimpl_id": mod["moduleimpl_id"]},
# )
# print(f"{len(inscrits)} inscrits dans ce module")
# # prend le premier inscrit, au hasard:
# etudid = inscrits[0]["etudid"]

View File

@ -10,7 +10,7 @@
Si entry_names est spécifié, la génération est restreinte aux exemples cités. Si entry_names est spécifié, la génération est restreinte aux exemples cités.
Exemple: Exemple:
python make_samples departements departement-formsemestres python make_samples departements departement_formsemestres
Doit être exécutée immédiatement apres une initialisation de la base pour test API! Doit être exécutée immédiatement apres une initialisation de la base pour test API!
(car dépendant des identifiants générés lors de la création des objets) (car dépendant des identifiants générés lors de la création des objets)
@ -71,7 +71,7 @@ from setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
get_auth_headers, get_auth_headers,
GET, GET,
POST_JSON, POST,
SCODOC_URL, SCODOC_URL,
) )
@ -98,18 +98,23 @@ class Sample:
elif permission == "UsersAdmin": elif permission == "UsersAdmin":
HEADERS = get_auth_headers("admin_api", "admin_api") HEADERS = get_auth_headers("admin_api", "admin_api")
else: else:
raise SampleException(f"Bad permission : {permission}") raise SampleException(f"Bad permission : {permission}, url={self.url}")
if self.method == "GET": if self.method == "GET":
self.result = GET(self.url, HEADERS) self.result = GET(self.url, HEADERS)
elif self.method == "POST": elif self.method == "POST":
if self.content == "": if self.content == "":
self.result = POST_JSON(self.url, headers=HEADERS) self.result = POST(self.url, headers=HEADERS)
else: else:
HEADERS["Content-Type"] = "application/json ; charset=utf-8" HEADERS["Content-Type"] = "application/json ; charset=utf-8"
self.result = POST_JSON(self.url, json.loads(self.content), HEADERS) try:
data = json.loads(self.content)
except json.decoder.JSONDecodeError as exc:
raise ValueError(
f"JSON invalide: {self.content}\nurl={self.url}"
) from exc
self.result = POST(self.url, data, HEADERS)
elif self.method[0] != "#": elif self.method[0] != "#":
error = f'Bad method : "{self.method}"' raise SampleException(f'Bad method : "{self.method}", url={self.url}')
raise SampleException(error)
self.shorten() self.shorten()
with open("sample_TEST.json.md", "tw", encoding="utf-8") as f: with open("sample_TEST.json.md", "tw", encoding="utf-8") as f:
self.dump(f) self.dump(f)
@ -235,3 +240,5 @@ if not CHECK_CERTIFICATE:
urllib3.disable_warnings() urllib3.disable_warnings()
make_samples(SAMPLES_FILENAME) make_samples(SAMPLES_FILENAME)
print(f"Fichiers samples générés dans {DATA_DIR}")

View File

@ -79,6 +79,16 @@ if pytest:
return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN) return get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
class _DefaultHeaders:
headers = {}
def set_headers(headers: dict):
"""Set default headers"""
print(f"set_headers: {headers}")
_DefaultHeaders.headers = headers
def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False): def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
"""Get and optionaly returns as JSON """Get and optionaly returns as JSON
Special case for non json result (image or pdf): Special case for non json result (image or pdf):
@ -91,7 +101,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
url = API_URL + path url = API_URL + path
reply = requests.get( reply = requests.get(
url, url,
headers=headers or {}, headers=_DefaultHeaders.headers if headers is None else headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
timeout=SCO_TEST_API_TIMEOUT, timeout=SCO_TEST_API_TIMEOUT,
) )
@ -119,7 +129,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
raise APIError("Unknown returned content {r.headers.get('Content-Type', None} !\n") raise APIError("Unknown returned content {r.headers.get('Content-Type', None} !\n")
def POST_JSON( def POST(
path: str, data: dict = {}, headers: dict = None, errmsg=None, dept=None, raw=False path: str, data: dict = {}, headers: dict = None, errmsg=None, dept=None, raw=False
): ):
"""Post """Post
@ -132,7 +142,7 @@ def POST_JSON(
r = requests.post( r = requests.post(
url, url,
json=data, json=data,
headers=headers or {}, headers=_DefaultHeaders.headers if headers is None else headers,
verify=CHECK_CERTIFICATE, verify=CHECK_CERTIFICATE,
timeout=SCO_TEST_API_TIMEOUT, timeout=SCO_TEST_API_TIMEOUT,
) )
@ -200,7 +210,7 @@ def check_failure_post(path: str, headers: dict, data: dict, err: str = None):
""" """
try: try:
data = POST_JSON(path=path, headers=headers, data=data, dept=DEPT_ACRONYM) data = POST(path=path, headers=headers, data=data, dept=DEPT_ACRONYM)
# ^ Renvoie un 404 # ^ Renvoie un 404
except APIError as api_err: except APIError as api_err:
if err is not None: if err is not None:

View File

@ -10,7 +10,7 @@ from types import NoneType
from tests.api.setup_test_api import ( from tests.api.setup_test_api import (
GET, GET,
POST_JSON, POST,
DEPT_ACRONYM, DEPT_ACRONYM,
APIError, APIError,
api_headers, api_headers,
@ -260,7 +260,7 @@ def test_route_create(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = create_data("present", "03") data = create_data("present", "03")
res = POST_JSON( res = POST(
f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM
) )
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
@ -275,7 +275,7 @@ def test_route_create(api_admin_headers):
check_fields(data, fields=ASSIDUITES_FIELDS) check_fields(data, fields=ASSIDUITES_FIELDS)
data2 = create_data("absent", "04", MODULE, "desc") data2 = create_data("absent", "04", MODULE, "desc")
res = POST_JSON( res = POST(
f"/assiduite/{ETUDID}/create", [data2], api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{ETUDID}/create", [data2], api_admin_headers, dept=DEPT_ACRONYM
) )
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
@ -286,7 +286,7 @@ def test_route_create(api_admin_headers):
# Mauvais fonctionnement # Mauvais fonctionnement
check_failure_post(f"/assiduite/{FAUX}/create", api_admin_headers, [data]) check_failure_post(f"/assiduite/{FAUX}/create", api_admin_headers, [data])
res = POST_JSON( res = POST(
f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM
) )
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
@ -296,7 +296,7 @@ def test_route_create(api_admin_headers):
== "Duplication: la période rentre en conflit avec une plage enregistrée" == "Duplication: la période rentre en conflit avec une plage enregistrée"
) )
res = POST_JSON( res = POST(
f"/assiduite/{ETUDID}/create", f"/assiduite/{ETUDID}/create",
[create_data("absent", "05", FAUX)], [create_data("absent", "05", FAUX)],
api_admin_headers, api_admin_headers,
@ -316,7 +316,7 @@ def test_route_create(api_admin_headers):
for d in range(randint(2, 4)) for d in range(randint(2, 4))
] ]
res = POST_JSON( res = POST(
f"/assiduite/{ETUDID}/create", data, api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{ETUDID}/create", data, api_admin_headers, dept=DEPT_ACRONYM
) )
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
@ -334,7 +334,7 @@ def test_route_create(api_admin_headers):
create_data("absent", "01"), create_data("absent", "01"),
] ]
res = POST_JSON( res = POST(
f"/assiduite/{ETUDID}/create", data2, api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{ETUDID}/create", data2, api_admin_headers, dept=DEPT_ACRONYM
) )
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
@ -359,13 +359,13 @@ def test_route_edit(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = {"etat": "retard", "moduleimpl_id": MODULE} data = {"etat": "retard", "moduleimpl_id": MODULE}
res = POST_JSON( res = POST(
f"/assiduite/{TO_REMOVE[0]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{TO_REMOVE[0]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM
) )
assert res == {"OK": True} assert res == {"OK": True}
data["moduleimpl_id"] = None data["moduleimpl_id"] = None
res = POST_JSON( res = POST(
f"/assiduite/{TO_REMOVE[1]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM f"/assiduite/{TO_REMOVE[1]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM
) )
assert res == {"OK": True} assert res == {"OK": True}
@ -389,13 +389,13 @@ def test_route_delete(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = TO_REMOVE[0] data = TO_REMOVE[0]
res = POST_JSON("/assiduite/delete", [data], api_admin_headers, dept=DEPT_ACRONYM) res = POST("/assiduite/delete", [data], api_admin_headers, dept=DEPT_ACRONYM)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
assert dat["message"] == "OK" assert dat["message"] == "OK"
# Mauvais fonctionnement # Mauvais fonctionnement
res = POST_JSON("/assiduite/delete", [data], api_admin_headers, dept=DEPT_ACRONYM) res = POST("/assiduite/delete", [data], api_admin_headers, dept=DEPT_ACRONYM)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 1 assert len(res["errors"]) == 1
@ -405,7 +405,7 @@ def test_route_delete(api_admin_headers):
data = TO_REMOVE[1:] data = TO_REMOVE[1:]
res = POST_JSON("/assiduite/delete", data, api_admin_headers, dept=DEPT_ACRONYM) res = POST("/assiduite/delete", data, api_admin_headers, dept=DEPT_ACRONYM)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
assert dat["message"] == "OK" assert dat["message"] == "OK"
@ -418,7 +418,7 @@ def test_route_delete(api_admin_headers):
FAUX + 2, FAUX + 2,
] ]
res = POST_JSON("/assiduite/delete", data2, api_admin_headers, dept=DEPT_ACRONYM) res = POST("/assiduite/delete", data2, api_admin_headers, dept=DEPT_ACRONYM)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 3 assert len(res["errors"]) == 3

View File

@ -8,7 +8,7 @@ Utilisation :
import datetime import datetime
import requests import requests
from tests.api.setup_test_api import GET, POST_JSON, api_headers from tests.api.setup_test_api import GET, POST, api_headers
ETUDID = 1 ETUDID = 1
@ -28,7 +28,7 @@ def test_billets(api_headers):
abs_end="2022-08-01", abs_end="2022-08-01",
description="test 1", description="test 1",
) )
billet_r = POST_JSON("/billets_absence/create", billet_d, headers=api_headers) billet_r = POST("/billets_absence/create", billet_d, headers=api_headers)
assert billet_r["etudid"] == billet_d["etudid"] assert billet_r["etudid"] == billet_d["etudid"]
assert datetime.datetime.fromisoformat(billet_r["abs_begin"]).replace( assert datetime.datetime.fromisoformat(billet_r["abs_begin"]).replace(
tzinfo=None tzinfo=None
@ -43,12 +43,10 @@ def test_billets(api_headers):
abs_end="2022-08-03", abs_end="2022-08-03",
description="test 2", description="test 2",
) )
billet_r = POST_JSON("/billets_absence/create", billet_d2, headers=api_headers) billet_r = POST("/billets_absence/create", billet_d2, headers=api_headers)
billets = GET("/billets_absence/etudiant/1", headers=api_headers) billets = GET("/billets_absence/etudiant/1", headers=api_headers)
assert len(billets) == 2 assert len(billets) == 2
# Suppression # Suppression
for billet in billets: for billet in billets:
reply = POST_JSON( reply = POST(f"/billets_absence/{billet['id']}/delete", headers=api_headers)
f"/billets_absence/{billet['id']}/delete", headers=api_headers
)
assert reply["OK"] == True assert reply["OK"] == True

View File

@ -23,7 +23,7 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
api_headers, api_headers,
api_admin_headers, api_admin_headers,
) )
@ -42,7 +42,7 @@ def test_create_dept(api_admin_headers):
/departement/<string:dept_acronym>/edit /departement/<string:dept_acronym>/edit
/departement/<string:dept_acronym>/delete /departement/<string:dept_acronym>/delete
""" """
dept = POST_JSON( dept = POST(
"/departement/create", "/departement/create",
{"acronym": "XTEST", "visible": True}, {"acronym": "XTEST", "visible": True},
headers=api_admin_headers, headers=api_admin_headers,
@ -50,14 +50,14 @@ def test_create_dept(api_admin_headers):
dept_r = GET(f"/departement/{dept['acronym']}", headers=api_admin_headers) dept_r = GET(f"/departement/{dept['acronym']}", headers=api_admin_headers)
assert dept["acronym"] == dept_r["acronym"] assert dept["acronym"] == dept_r["acronym"]
assert dept_r["visible"] is True assert dept_r["visible"] is True
dept_e = POST_JSON( dept_e = POST(
f"/departement/{dept['acronym']}/edit", f"/departement/{dept['acronym']}/edit",
{"visible": False}, {"visible": False},
headers=api_admin_headers, headers=api_admin_headers,
) )
dept_r = GET(f"/departement/{dept['acronym']}", headers=api_admin_headers) dept_r = GET(f"/departement/{dept['acronym']}", headers=api_admin_headers)
assert dept_r["visible"] is False assert dept_r["visible"] is False
r = POST_JSON( r = POST(
f"/departement/{dept['acronym']}/delete", f"/departement/{dept['acronym']}/delete",
headers=api_admin_headers, headers=api_admin_headers,
) )

View File

@ -30,7 +30,7 @@ from tests.api.setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
DEPT_ACRONYM, DEPT_ACRONYM,
GET, GET,
POST_JSON, POST,
get_auth_headers, get_auth_headers,
) )
from tests.api.setup_test_api import api_headers # pylint: disable=unused-import from tests.api.setup_test_api import api_headers # pylint: disable=unused-import
@ -262,7 +262,7 @@ def test_etudiants_by_name(api_headers):
"dept": DEPT_ACRONYM, "dept": DEPT_ACRONYM,
"civilite": "X", "civilite": "X",
} }
_ = POST_JSON( _ = POST(
"/etudiant/create", "/etudiant/create",
args, args,
headers=admin_header, headers=admin_header,
@ -292,7 +292,7 @@ def test_etudiant_annotations(api_headers):
"dept": DEPT_ACRONYM, "dept": DEPT_ACRONYM,
"civilite": "M", "civilite": "M",
} }
etud = POST_JSON( etud = POST(
"/etudiant/create", "/etudiant/create",
args, args,
headers=admin_header, headers=admin_header,
@ -304,7 +304,7 @@ def test_etudiant_annotations(api_headers):
assert etud["nom"] assert etud["nom"]
assert etud["annotations"] == [] assert etud["annotations"] == []
# ajoute annotation # ajoute annotation
annotation = POST_JSON( annotation = POST(
f"/etudiant/etudid/{etudid}/annotation", f"/etudiant/etudid/{etudid}/annotation",
{"comment": "annotation 1"}, {"comment": "annotation 1"},
headers=admin_header, headers=admin_header,
@ -318,7 +318,7 @@ def test_etudiant_annotations(api_headers):
assert etud["annotations"][0]["comment"] == "annotation 1" assert etud["annotations"][0]["comment"] == "annotation 1"
assert etud["annotations"][0]["id"] == annotation_id assert etud["annotations"][0]["id"] == annotation_id
# Supprime annotation # Supprime annotation
POST_JSON( POST(
f"/etudiant/etudid/{etudid}/annotation/{annotation_id}/delete", f"/etudiant/etudid/{etudid}/annotation/{annotation_id}/delete",
headers=admin_header, headers=admin_header,
) )
@ -932,7 +932,7 @@ def test_etudiant_bulletin_semestre(api_headers):
### -------- Modifie publication bulletins ### -------- Modifie publication bulletins
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN) admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
formsemestre = POST_JSON( formsemestre = POST(
f"/formsemestre/{1}/edit", {"bul_hide_xml": True}, headers=admin_header f"/formsemestre/{1}/edit", {"bul_hide_xml": True}, headers=admin_header
) )
assert formsemestre["bul_hide_xml"] is True assert formsemestre["bul_hide_xml"] is True
@ -943,7 +943,7 @@ def test_etudiant_bulletin_semestre(api_headers):
# /ScoDoc/api/etudiant/nip/12345/formsemestre/123/bulletin/long/pdf/nosi # /ScoDoc/api/etudiant/nip/12345/formsemestre/123/bulletin/long/pdf/nosi
# TODO voir forme utilisée par ScoDoc en interne: # TODO voir forme utilisée par ScoDoc en interne:
# formsemestre_bulletinetud?formsemestre_id=1263&etudid=16387 # formsemestre_bulletinetud?formsemestre_id=1263&etudid=16387
formsemestre = POST_JSON( formsemestre = POST(
f"/formsemestre/{1}/edit", {"bul_hide_xml": False}, headers=admin_header f"/formsemestre/{1}/edit", {"bul_hide_xml": False}, headers=admin_header
) )
@ -1000,7 +1000,7 @@ def test_etudiant_create(api_headers):
} }
], ],
} }
etud = POST_JSON( etud = POST(
"/etudiant/create", "/etudiant/create",
args, args,
headers=admin_header, headers=admin_header,
@ -1023,7 +1023,7 @@ def test_etudiant_create(api_headers):
# assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] # assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"]
# Edition # Edition
etud = POST_JSON( etud = POST(
f"/etudiant/etudid/{etudid}/edit", f"/etudiant/etudid/{etudid}/edit",
{ {
"civilite": "F", "civilite": "F",
@ -1039,7 +1039,7 @@ def test_etudiant_create(api_headers):
assert len(etud["adresses"]) == 1 assert len(etud["adresses"]) == 1
# assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"] # assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"]
# assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] # assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"]
etud = POST_JSON( etud = POST(
f"/etudiant/etudid/{etudid}/edit", f"/etudiant/etudid/{etudid}/edit",
{ {
"adresses": [ "adresses": [
@ -1051,7 +1051,7 @@ def test_etudiant_create(api_headers):
headers=admin_header, headers=admin_header,
) )
assert etud["adresses"][0]["villedomicile"] == "Barcelona" assert etud["adresses"][0]["villedomicile"] == "Barcelona"
etud = POST_JSON( etud = POST(
f"/etudiant/etudid/{etudid}/edit", f"/etudiant/etudid/{etudid}/edit",
{ {
"admission": { "admission": {

View File

@ -25,7 +25,7 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
api_admin_headers, api_admin_headers,
api_headers, api_headers,
check_failure_post, check_failure_post,
@ -114,7 +114,7 @@ def test_evaluation_create(api_admin_headers):
) )
nb_evals = len(evaluations) nb_evals = len(evaluations)
# #
e = POST_JSON( e = POST(
f"/moduleimpl/{moduleimpl_id}/evaluation/create", f"/moduleimpl/{moduleimpl_id}/evaluation/create",
{"description": "eval test"}, {"description": "eval test"},
api_admin_headers, api_admin_headers,
@ -157,7 +157,7 @@ def test_evaluation_create(api_admin_headers):
"publish_incomplete": True, "publish_incomplete": True,
"note_max": 100.0, "note_max": 100.0,
} }
e = POST_JSON( e = POST(
f"/moduleimpl/{moduleimpl_id}/evaluation/create", f"/moduleimpl/{moduleimpl_id}/evaluation/create",
data, data,
api_admin_headers, api_admin_headers,
@ -209,7 +209,7 @@ def test_evaluation_create(api_admin_headers):
ue_ids = [ue["id"] for ue in ues] ue_ids = [ue["id"] for ue in ues]
poids = {ue_id: float(i) + 0.5 for i, ue_id in enumerate(ue_ids)} poids = {ue_id: float(i) + 0.5 for i, ue_id in enumerate(ue_ids)}
data.update({"description": "eval avec poids", "poids": poids}) data.update({"description": "eval avec poids", "poids": poids})
e = POST_JSON( e = POST(
f"/moduleimpl/{moduleimpl_id}/evaluation/create", f"/moduleimpl/{moduleimpl_id}/evaluation/create",
data, data,
api_admin_headers, api_admin_headers,
@ -223,7 +223,7 @@ def test_evaluation_create(api_admin_headers):
assert new_nb_evals == nb_evals + 1 assert new_nb_evals == nb_evals + 1
nb_evals = new_nb_evals nb_evals = new_nb_evals
# Delete # Delete
ans = POST_JSON( ans = POST(
f"/evaluation/{e_ret['id']}/delete", f"/evaluation/{e_ret['id']}/delete",
headers=api_admin_headers, headers=api_admin_headers,
) )

View File

@ -26,7 +26,7 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
) )
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
verify_fields, verify_fields,
@ -338,11 +338,11 @@ def test_api_ue_apo(api_admin_headers):
ue_id = 1 ue_id = 1
ue = GET(path=f"/formation/ue/{ue_id}", headers=api_admin_headers) ue = GET(path=f"/formation/ue/{ue_id}", headers=api_admin_headers)
assert ue["id"] == ue_id assert ue["id"] == ue_id
r = POST_JSON( r = POST(
f"/formation/ue/{ue_id}/set_code_apogee/APOUE", {}, api_admin_headers, raw=True f"/formation/ue/{ue_id}/set_code_apogee/APOUE", {}, api_admin_headers, raw=True
) )
assert r.text == "APOUE" assert r.text == "APOUE"
r = POST_JSON( r = POST(
f"/formation/ue/{ue_id}/set_code_apogee_rcue/APORCUE", f"/formation/ue/{ue_id}/set_code_apogee_rcue/APORCUE",
{}, {},
api_admin_headers, api_admin_headers,
@ -363,7 +363,7 @@ def test_api_module_apo(api_admin_headers):
module = GET(path=f"/formation/module/{module_id}", headers=api_admin_headers) module = GET(path=f"/formation/module/{module_id}", headers=api_admin_headers)
assert module["id"] == module_id assert module["id"] == module_id
assert module["code_apogee"] == "" assert module["code_apogee"] == ""
r = POST_JSON( r = POST(
f"/formation/module/{module_id}/set_code_apogee/APOMOD", f"/formation/module/{module_id}/set_code_apogee/APOMOD",
{}, {},
api_admin_headers, api_admin_headers,
@ -379,7 +379,7 @@ def test_api_module_edit(api_admin_headers):
/formation/module/<int:module_id>/edit /formation/module/<int:module_id>/edit
""" """
module_id = 1 module_id = 1
module = POST_JSON( module = POST(
f"/formation/module/{module_id}/edit", f"/formation/module/{module_id}/edit",
{"heures_cours": 16, "semestre_id": 1, "abbrev": "ALLO"}, {"heures_cours": 16, "semestre_id": 1, "abbrev": "ALLO"},
api_admin_headers, api_admin_headers,
@ -390,7 +390,7 @@ def test_api_module_edit(api_admin_headers):
assert module["abbrev"] == "ALLO" assert module["abbrev"] == "ALLO"
# tente de changer l'UE: ne devrait rien faire: # tente de changer l'UE: ne devrait rien faire:
ue_id = module["ue_id"] ue_id = module["ue_id"]
module = POST_JSON( module = POST(
f"/formation/module/{module_id}/edit", f"/formation/module/{module_id}/edit",
{"ue_id": 666}, {"ue_id": 666},
api_admin_headers, api_admin_headers,
@ -398,7 +398,7 @@ def test_api_module_edit(api_admin_headers):
assert module["ue_id"] == ue_id assert module["ue_id"] == ue_id
# tente de changer la formation: ne devrait rien faire: # tente de changer la formation: ne devrait rien faire:
formation_id = module["formation_id"] formation_id = module["formation_id"]
module = POST_JSON( module = POST(
f"/formation/module/{module_id}/edit", f"/formation/module/{module_id}/edit",
{"formation_id": 666}, {"formation_id": 666},
api_admin_headers, api_admin_headers,
@ -406,7 +406,7 @@ def test_api_module_edit(api_admin_headers):
assert module["formation_id"] == formation_id assert module["formation_id"] == formation_id
# -- assignation de parcours (ce test suppose que les parcours 1, 2, 3 existent) # -- assignation de parcours (ce test suppose que les parcours 1, 2, 3 existent)
assert module["parcours"] == [] assert module["parcours"] == []
module = POST_JSON( module = POST(
f"/formation/module/{module_id}/edit", f"/formation/module/{module_id}/edit",
{"parcours": [1, 2, 3]}, {"parcours": [1, 2, 3]},
api_admin_headers, api_admin_headers,
@ -419,7 +419,7 @@ def test_api_ue_edit(api_admin_headers):
/formation/ue/<int:ue_id>/edit /formation/ue/<int:ue_id>/edit
""" """
ue_id = 1 ue_id = 1
ue = POST_JSON( ue = POST(
f"/formation/ue/{ue_id}/edit", f"/formation/ue/{ue_id}/edit",
{"titre": "formation test modifiée", "numero": 22}, {"titre": "formation test modifiée", "numero": 22},
api_admin_headers, api_admin_headers,
@ -430,7 +430,7 @@ def test_api_ue_edit(api_admin_headers):
# tente de changer le niveau de compétence: ne devrait rien faire: # tente de changer le niveau de compétence: ne devrait rien faire:
niveau_competence_id = ue["niveau_competence_id"] niveau_competence_id = ue["niveau_competence_id"]
ue = POST_JSON( ue = POST(
f"/formation/ue/{ue_id}/edit", f"/formation/ue/{ue_id}/edit",
{"niveau_competence_id": 666}, {"niveau_competence_id": 666},
api_admin_headers, api_admin_headers,
@ -438,7 +438,7 @@ def test_api_ue_edit(api_admin_headers):
assert ue["niveau_competence_id"] == niveau_competence_id assert ue["niveau_competence_id"] == niveau_competence_id
# tente de changer la formation: ne devrait rien faire: # tente de changer la formation: ne devrait rien faire:
formation_id = ue["formation_id"] formation_id = ue["formation_id"]
ue = POST_JSON( ue = POST(
f"/formation/ue/{ue_id}/edit", f"/formation/ue/{ue_id}/edit",
{"formation_id": 666}, {"formation_id": 666},
api_admin_headers, api_admin_headers,

View File

@ -23,7 +23,7 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
api_headers, api_headers,
) )
@ -54,7 +54,7 @@ def test_jury_decisions(api_headers):
# ues = export_formation["ue"] # ues = export_formation["ue"]
# # Enregistre une validation d'RCUE # # Enregistre une validation d'RCUE
# etudid = etudiants[0]["id"] # etudid = etudiants[0]["id"]
# validation = POST_JSON( # validation = POST(
# f"/etudiant/{etudid}/jury/validation_rcue/record", # f"/etudiant/{etudid}/jury/validation_rcue/record",
# data={ # data={
# "code": "ADM", # "code": "ADM",

View File

@ -12,7 +12,7 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
APIError, APIError,
api_headers, api_headers,
api_admin_headers, api_admin_headers,
@ -105,7 +105,7 @@ def check_failure_post(path, headers, data, err=None):
APIError: Une erreur car la requête a fonctionné (mauvais comportement) APIError: Une erreur car la requête a fonctionné (mauvais comportement)
""" """
try: try:
data = POST_JSON(path=path, headers=headers, data=data) data = POST(path=path, headers=headers, data=data)
# ^ Renvoi un 404 # ^ Renvoi un 404
except APIError as api_err: except APIError as api_err:
if err is not None: if err is not None:
@ -204,14 +204,14 @@ def test_route_create(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = create_data("valide", "01") data = create_data("valide", "01")
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data], api_admin_headers) res = POST(f"/justificatif/{ETUDID}/create", [data], api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["success"]) == 1 assert len(res["success"]) == 1
TO_REMOVE.append(res["success"][0]["message"]["justif_id"]) TO_REMOVE.append(res["success"][0]["message"]["justif_id"])
data2 = create_data("modifie", "02", "raison") data2 = create_data("modifie", "02", "raison")
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data2], api_admin_headers) res = POST(f"/justificatif/{ETUDID}/create", [data2], api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["success"]) == 1 assert len(res["success"]) == 1
@ -220,7 +220,7 @@ def test_route_create(api_admin_headers):
# Mauvais fonctionnement # Mauvais fonctionnement
check_failure_post(f"/justificatif/{FAUX}/create", api_admin_headers, [data]) check_failure_post(f"/justificatif/{FAUX}/create", api_admin_headers, [data])
res = POST_JSON( res = POST(
f"/justificatif/{ETUDID}/create", f"/justificatif/{ETUDID}/create",
[create_data("absent", "03")], [create_data("absent", "03")],
api_admin_headers, api_admin_headers,
@ -239,7 +239,7 @@ def test_route_create(api_admin_headers):
for d in range(randint(3, 5)) for d in range(randint(3, 5))
] ]
res = POST_JSON(f"/justificatif/{ETUDID}/create", data, api_admin_headers) res = POST(f"/justificatif/{ETUDID}/create", data, api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
check_fields(dat["message"], CREATE_FIELD) check_fields(dat["message"], CREATE_FIELD)
@ -253,7 +253,7 @@ def test_route_create(api_admin_headers):
create_data("valide", 32), create_data("valide", 32),
] ]
res = POST_JSON(f"/justificatif/{ETUDID}/create", data2, api_admin_headers) res = POST(f"/justificatif/{ETUDID}/create", data2, api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 3 assert len(res["errors"]) == 3
@ -270,11 +270,11 @@ def test_route_edit(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = {"etat": "modifie", "raison": "test"} data = {"etat": "modifie", "raison": "test"}
res = POST_JSON(f"/justificatif/{TO_REMOVE[0]}/edit", data, api_admin_headers) res = POST(f"/justificatif/{TO_REMOVE[0]}/edit", data, api_admin_headers)
assert isinstance(res, dict) and "couverture" in res.keys() assert isinstance(res, dict) and "couverture" in res.keys()
data["raison"] = None data["raison"] = None
res = POST_JSON(f"/justificatif/{TO_REMOVE[1]}/edit", data, api_admin_headers) res = POST(f"/justificatif/{TO_REMOVE[1]}/edit", data, api_admin_headers)
assert isinstance(res, dict) and "couverture" in res.keys() assert isinstance(res, dict) and "couverture" in res.keys()
# Mauvais fonctionnement # Mauvais fonctionnement
@ -296,13 +296,13 @@ def test_route_delete(api_admin_headers):
# Bon fonctionnement # Bon fonctionnement
data = TO_REMOVE[0] data = TO_REMOVE[0]
res = POST_JSON("/justificatif/delete", [data], api_admin_headers) res = POST("/justificatif/delete", [data], api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
assert dat["message"] == "OK" assert dat["message"] == "OK"
# Mauvais fonctionnement # Mauvais fonctionnement
res = POST_JSON("/justificatif/delete", [data], api_admin_headers) res = POST("/justificatif/delete", [data], api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 1 assert len(res["errors"]) == 1
@ -312,7 +312,7 @@ def test_route_delete(api_admin_headers):
data = TO_REMOVE[1:] data = TO_REMOVE[1:]
res = POST_JSON("/justificatif/delete", data, api_admin_headers) res = POST("/justificatif/delete", data, api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
for dat in res["success"]: for dat in res["success"]:
assert dat["message"] == "OK" assert dat["message"] == "OK"
@ -325,7 +325,7 @@ def test_route_delete(api_admin_headers):
FAUX + 2, FAUX + 2,
] ]
res = POST_JSON("/justificatif/delete", data2, api_admin_headers) res = POST("/justificatif/delete", data2, api_admin_headers)
check_fields(res, BATCH_FIELD) check_fields(res, BATCH_FIELD)
assert len(res["errors"]) == 3 assert len(res["errors"]) == 3
@ -473,15 +473,13 @@ def test_remove_justificatif(api_admin_headers):
filename: str = "tests/api/test_api_justificatif2.txt" filename: str = "tests/api/test_api_justificatif2.txt"
_send_file(2, filename, api_admin_headers) _send_file(2, filename, api_admin_headers)
res: dict = POST_JSON( res: dict = POST("/justificatif/1/remove", {"remove": "all"}, api_admin_headers)
"/justificatif/1/remove", {"remove": "all"}, api_admin_headers
)
assert res == {"response": "removed"} assert res == {"response": "removed"}
l = GET("/justificatif/1/list", api_admin_headers) l = GET("/justificatif/1/list", api_admin_headers)
assert isinstance(l, dict) assert isinstance(l, dict)
assert l["total"] == 0 assert l["total"] == 0
res: dict = POST_JSON( res: dict = POST(
"/justificatif/2/remove", "/justificatif/2/remove",
{"remove": "list", "filenames": ["test_api_justificatif2.txt"]}, {"remove": "list", "filenames": ["test_api_justificatif2.txt"]},
api_admin_headers, api_admin_headers,
@ -491,7 +489,7 @@ def test_remove_justificatif(api_admin_headers):
assert isinstance(l, dict) assert isinstance(l, dict)
assert l["total"] == 1 assert l["total"] == 1
res: dict = POST_JSON( res: dict = POST(
"/justificatif/2/remove", "/justificatif/2/remove",
{"remove": "list", "filenames": ["test_api_justificatif.txt"]}, {"remove": "list", "filenames": ["test_api_justificatif.txt"]},
api_admin_headers, api_admin_headers,

View File

@ -21,7 +21,7 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
api_headers, api_headers,
) )
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
@ -65,15 +65,15 @@ def test_formsemestre_partition(api_headers):
headers = api_headers headers = api_headers
formsemestre_id = 1 formsemestre_id = 1
partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=headers) partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=headers)
# au départ, pas de partitions # au départ, une partition (TD avec groupes A et B)
assert partitions == {} assert len(partitions) == 1
# --- Création partition # --- Création partition
partition_d = { partition_d = {
"partition_name": "T&Dé", "partition_name": "T&Dé",
"bul_show_rank": True, "bul_show_rank": True,
"show_in_lists": True, "show_in_lists": True,
} }
partition_r = POST_JSON( partition_r = POST(
f"/formsemestre/{formsemestre_id}/partition/create", f"/formsemestre/{formsemestre_id}/partition/create",
partition_d, partition_d,
headers=headers, headers=headers,
@ -83,7 +83,7 @@ def test_formsemestre_partition(api_headers):
assert partition_r["groups"] == {} assert partition_r["groups"] == {}
# --- Création Groupe # --- Création Groupe
group_d = {"group_name": "Aé-&"} group_d = {"group_name": "Aé-&"}
group_r = POST_JSON( group_r = POST(
f"/partition/{partition_r['id']}/group/create", f"/partition/{partition_r['id']}/group/create",
group_d, group_d,
headers=headers, headers=headers,
@ -102,14 +102,14 @@ def test_formsemestre_partition(api_headers):
# --- Ajout d'un groupe avec edt_id # --- Ajout d'un groupe avec edt_id
group_d = {"group_name": "extra", "edt_id": "GEDT"} group_d = {"group_name": "extra", "edt_id": "GEDT"}
group_r = POST_JSON( group_r = POST(
f"/partition/{partition_r['id']}/group/create", f"/partition/{partition_r['id']}/group/create",
group_d, group_d,
headers=headers, headers=headers,
) )
assert group_r["edt_id"] == "GEDT" assert group_r["edt_id"] == "GEDT"
# Edit edt_id # Edit edt_id
group_r = POST_JSON( group_r = POST(
f"/group/{group_r['id']}/edit", f"/group/{group_r['id']}/edit",
{"edt_id": "GEDT2"}, {"edt_id": "GEDT2"},
headers=headers, headers=headers,
@ -121,7 +121,7 @@ def test_formsemestre_partition(api_headers):
assert group["edt_id"] == "GEDT2" assert group["edt_id"] == "GEDT2"
# Change edt_id via route dédiée: # Change edt_id via route dédiée:
group_t = POST_JSON( group_t = POST(
f"/group/{group_r['id']}/set_edt_id/GEDT3", f"/group/{group_r['id']}/set_edt_id/GEDT3",
headers=headers, headers=headers,
) )
@ -130,7 +130,7 @@ def test_formsemestre_partition(api_headers):
# Place un étudiant dans le groupe # Place un étudiant dans le groupe
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=headers)[0] etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=headers)[0]
repl = POST_JSON(f"/group/{group['id']}/set_etudiant/{etud['id']}", headers=headers) repl = POST(f"/group/{group['id']}/set_etudiant/{etud['id']}", headers=headers)
assert isinstance(repl, dict) assert isinstance(repl, dict)
assert repl["group_id"] == group["id"] assert repl["group_id"] == group["id"]
assert repl["etudid"] == etud["id"] assert repl["etudid"] == etud["id"]
@ -139,21 +139,17 @@ def test_formsemestre_partition(api_headers):
assert len(etuds) == 1 assert len(etuds) == 1
assert etuds[0]["id"] == etud["id"] assert etuds[0]["id"] == etud["id"]
# Retire l'étudiant du groupe # Retire l'étudiant du groupe
repl = POST_JSON( repl = POST(f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers)
f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers
)
assert len(GET(f"/group/{group['id']}/etudiants", headers=headers)) == 0 assert len(GET(f"/group/{group['id']}/etudiants", headers=headers)) == 0
# Le retire à nouveau ! (bug #465) # Le retire à nouveau ! (bug #465)
repl = POST_JSON( repl = POST(f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers)
f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers
)
assert repl["group_id"] == group["id"] assert repl["group_id"] == group["id"]
# Avec partition (vérifie encodeur JSON) # Avec partition (vérifie encodeur JSON)
partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=headers) partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=headers)
assert partitions assert partitions
# Delete partition # Delete partition
repl = POST_JSON(f"/partition/{partition_r['id']}/delete", headers=headers) repl = POST(f"/partition/{partition_r['id']}/delete", headers=headers)
assert repl["OK"] is True assert repl["OK"] is True

View File

@ -11,7 +11,7 @@ from tests.api.setup_test_api import (
APIError, APIError,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON, POST,
api_headers, api_headers,
api_admin_headers, api_admin_headers,
get_auth_headers, get_auth_headers,
@ -76,7 +76,7 @@ def test_edit_users(api_admin_headers):
""" """
admin_h = api_admin_headers admin_h = api_admin_headers
nb_users = len(GET("/users/query", headers=admin_h)) nb_users = len(GET("/users/query", headers=admin_h))
user = POST_JSON( user = POST(
"/user/create", "/user/create",
{"user_name": "test_edit_users", "nom": "Toto"}, {"user_name": "test_edit_users", "nom": "Toto"},
headers=admin_h, headers=admin_h,
@ -86,7 +86,7 @@ def test_edit_users(api_admin_headers):
assert user["active"] is True assert user["active"] is True
assert (nb_users + 1) == len(GET("/users/query", headers=admin_h)) assert (nb_users + 1) == len(GET("/users/query", headers=admin_h))
# Change le dept et rend inactif # Change le dept et rend inactif
user = POST_JSON( user = POST(
f"/user/{user['id']}/edit", f"/user/{user['id']}/edit",
{"active": False, "dept": "TAPI", "edt_id": "GGG"}, {"active": False, "dept": "TAPI", "edt_id": "GGG"},
headers=admin_h, headers=admin_h,
@ -107,37 +107,37 @@ def test_roles(api_admin_headers):
/user/<int:uid>/edit /user/<int:uid>/edit
""" """
admin_h = api_admin_headers admin_h = api_admin_headers
user = POST_JSON( user = POST(
"/user/create", "/user/create",
{"user_name": "test_roles", "nom": "Role", "prenom": "Test"}, {"user_name": "test_roles", "nom": "Role", "prenom": "Test"},
headers=admin_h, headers=admin_h,
) )
uid = user["id"] uid = user["id"]
ans = POST_JSON(f"/user/{uid}/role/Secr/add", headers=admin_h) ans = POST(f"/user/{uid}/role/Secr/add", headers=admin_h)
assert ans["user_name"] == "test_roles" assert ans["user_name"] == "test_roles"
role = POST_JSON("/role/create/Test_X", headers=admin_h) role = POST("/role/create/Test_X", headers=admin_h)
assert role["role_name"] == "Test_X" assert role["role_name"] == "Test_X"
assert role["permissions"] == [] assert role["permissions"] == []
role = GET("/role/Test_X", headers=admin_h) role = GET("/role/Test_X", headers=admin_h)
assert role["role_name"] == "Test_X" assert role["role_name"] == "Test_X"
assert role["permissions"] == [] assert role["permissions"] == []
role = POST_JSON("/role/Test_X/edit", {"role_name": "Test_Y"}, headers=admin_h) role = POST("/role/Test_X/edit", {"role_name": "Test_Y"}, headers=admin_h)
assert role["role_name"] == "Test_Y" assert role["role_name"] == "Test_Y"
role = GET("/role/Test_Y", headers=admin_h) role = GET("/role/Test_Y", headers=admin_h)
assert role["role_name"] == "Test_Y" assert role["role_name"] == "Test_Y"
role = POST_JSON( role = POST(
"/role/Test_Y/edit", "/role/Test_Y/edit",
{"permissions": ["ScoView", "AbsChange"]}, {"permissions": ["ScoView", "AbsChange"]},
headers=admin_h, headers=admin_h,
) )
assert set(role["permissions"]) == {"ScoView", "AbsChange"} assert set(role["permissions"]) == {"ScoView", "AbsChange"}
role = POST_JSON("/role/Test_Y/add_permission/AbsAddBillet", headers=admin_h) role = POST("/role/Test_Y/add_permission/AbsAddBillet", headers=admin_h)
assert set(role["permissions"]) == {"ScoView", "AbsChange", "AbsAddBillet"} assert set(role["permissions"]) == {"ScoView", "AbsChange", "AbsAddBillet"}
role = GET("/role/Test_Y", headers=admin_h) role = GET("/role/Test_Y", headers=admin_h)
assert set(role["permissions"]) == {"ScoView", "AbsChange", "AbsAddBillet"} assert set(role["permissions"]) == {"ScoView", "AbsChange", "AbsAddBillet"}
role = POST_JSON("/role/Test_Y/remove_permission/AbsChange", headers=admin_h) role = POST("/role/Test_Y/remove_permission/AbsChange", headers=admin_h)
assert set(role["permissions"]) == {"ScoView", "AbsAddBillet"} assert set(role["permissions"]) == {"ScoView", "AbsAddBillet"}
ans = POST_JSON("/role/Test_Y/delete", headers=admin_h) ans = POST("/role/Test_Y/delete", headers=admin_h)
assert ans["OK"] is True assert ans["OK"] is True
@ -153,7 +153,7 @@ def test_modif_users_depts(api_admin_headers):
# On va utiliser les 3 1er dept (TAPI, AA, BB) # On va utiliser les 3 1er dept (TAPI, AA, BB)
# On crée un nouvel utilisateur "chef2", admin dans les 2 premiers dept # On crée un nouvel utilisateur "chef2", admin dans les 2 premiers dept
# puis un utilisateur lambda, dans le dept 2 (AA) # puis un utilisateur lambda, dans le dept 2 (AA)
chef2 = POST_JSON( chef2 = POST(
"/user/create", "/user/create",
{ {
"user_name": "chef2", "user_name": "chef2",
@ -163,23 +163,23 @@ def test_modif_users_depts(api_admin_headers):
}, },
headers=admin_h, headers=admin_h,
) )
role_chef = POST_JSON( role_chef = POST(
"/role/create/chef", "/role/create/chef",
{"permissions": ["ScoView", "UsersAdmin", "UsersView"]}, {"permissions": ["ScoView", "UsersAdmin", "UsersView"]},
headers=admin_h, headers=admin_h,
) )
_ = POST_JSON( _ = POST(
f"/user/{chef2['id']}/role/chef/add/departement/{dept1['acronym']}", f"/user/{chef2['id']}/role/chef/add/departement/{dept1['acronym']}",
headers=admin_h, headers=admin_h,
) )
_ = POST_JSON( _ = POST(
f"/user/{chef2['id']}/role/chef/add/departement/{dept2['acronym']}", f"/user/{chef2['id']}/role/chef/add/departement/{dept2['acronym']}",
headers=admin_h, headers=admin_h,
) )
# Un mot de passe trop simple: # Un mot de passe trop simple:
ok = False ok = False
try: try:
_ = POST_JSON( _ = POST(
f"/user/{chef2['id']}/password", f"/user/{chef2['id']}/password",
{"password": "123456"}, {"password": "123456"},
headers=admin_h, headers=admin_h,
@ -190,13 +190,13 @@ def test_modif_users_depts(api_admin_headers):
assert ok assert ok
# Un "vrai" mot de passe: # Un "vrai" mot de passe:
chef2_password = "17HIOPpYhabb8qw'E:/jd7FFddjd" chef2_password = "17HIOPpYhabb8qw'E:/jd7FFddjd"
_ = POST_JSON( _ = POST(
f"/user/{chef2['id']}/password", f"/user/{chef2['id']}/password",
{"password": chef2_password}, {"password": chef2_password},
headers=admin_h, headers=admin_h,
) )
# Création user lambda: # Création user lambda:
u_lambda = POST_JSON( u_lambda = POST(
"/user/create", "/user/create",
{ {
"user_name": "lambda", "user_name": "lambda",
@ -210,7 +210,7 @@ def test_modif_users_depts(api_admin_headers):
# Le chef va modifier u_lambda: # Le chef va modifier u_lambda:
chef_h = get_auth_headers(chef2["user_name"], chef2_password) chef_h = get_auth_headers(chef2["user_name"], chef2_password)
# on utilise une URL avec département car on n'a pas le droit sur tous: # on utilise une URL avec département car on n'a pas le droit sur tous:
u = POST_JSON( u = POST(
f"/user/{u_lambda['id']}/edit", f"/user/{u_lambda['id']}/edit",
{"nom": "toto"}, {"nom": "toto"},
headers=chef_h, headers=chef_h,
@ -218,7 +218,7 @@ def test_modif_users_depts(api_admin_headers):
) )
assert u["nom"] == "toto" assert u["nom"] == "toto"
# Dans l'autre ? # Dans l'autre ?
u = POST_JSON( u = POST(
f"/user/{u_lambda['id']}/edit", f"/user/{u_lambda['id']}/edit",
{"nom": "toto"}, {"nom": "toto"},
headers=chef_h, headers=chef_h,
@ -227,7 +227,7 @@ def test_modif_users_depts(api_admin_headers):
# mais pas dans le troisième: # mais pas dans le troisième:
ok = False ok = False
try: try:
u = POST_JSON( u = POST(
f"/user/{u_lambda['id']}/edit", f"/user/{u_lambda['id']}/edit",
{"nom": "toto"}, {"nom": "toto"},
headers=chef_h, headers=chef_h,
@ -240,7 +240,7 @@ def test_modif_users_depts(api_admin_headers):
# Nettoyage: # Nettoyage:
# on ne peut pas supprimer l'utilisateur lambda, mais on # on ne peut pas supprimer l'utilisateur lambda, mais on
# le rend inactif et on le retire de son département # le rend inactif et on le retire de son département
u = POST_JSON( u = POST(
f"/user/{u_lambda['id']}/edit", f"/user/{u_lambda['id']}/edit",
{"active": False, "dept": None}, {"active": False, "dept": None},
headers=admin_h, headers=admin_h,

View File

@ -1,5 +1,6 @@
import pytest import pytest
import flask
from flask import g from flask import g
from flask_login import login_user from flask_login import login_user

View File

@ -50,8 +50,9 @@
"moy_sae_14_3": "17.83", "moy_sae_14_3": "17.83",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "3/3", "ues_validables": "3/3",
"nbabs": 0, "nbabs": 1,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -109,8 +110,9 @@
"moy_sae_14_3": "10.74", "moy_sae_14_3": "10.74",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "3/3", "ues_validables": "3/3",
"nbabs": 2, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -170,6 +172,7 @@
"ues_validables": "2/3", "ues_validables": "2/3",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -229,6 +232,7 @@
"ues_validables": "2/3", "ues_validables": "2/3",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -286,8 +290,9 @@
"moy_sae_14_3": "11.09", "moy_sae_14_3": "11.09",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "1/3", "ues_validables": "1/3",
"nbabs": 3, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -347,6 +352,7 @@
"ues_validables": "2/3", "ues_validables": "2/3",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -404,8 +410,9 @@
"moy_sae_14_3": "05.17", "moy_sae_14_3": "05.17",
"moy_sae_15_3": "~", "moy_sae_15_3": "~",
"ues_validables": "1/3", "ues_validables": "1/3",
"nbabs": 3, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -465,6 +472,7 @@
"ues_validables": "0/3", "ues_validables": "0/3",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -524,6 +532,7 @@
"ues_validables": "0/3", "ues_validables": "0/3",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -583,6 +592,7 @@
"ues_validables": "0/3", "ues_validables": "0/3",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -642,6 +652,7 @@
"ues_validables": "", "ues_validables": "",
"nbabs": 2, "nbabs": 2,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "D\u00e9f.",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -699,8 +710,9 @@
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "", "ues_validables": "",
"nbabs": 2, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "D\u00e9m.",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -758,8 +770,9 @@
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "", "ues_validables": "",
"nbabs": 0, "nbabs": 2,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "D\u00e9f.",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -817,8 +830,9 @@
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "", "ues_validables": "",
"nbabs": 1, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "D\u00e9m.",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -878,6 +892,7 @@
"ues_validables": "", "ues_validables": "",
"nbabs": 0, "nbabs": 0,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "D\u00e9f.",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",
@ -935,8 +950,9 @@
"moy_sae_14_3": "", "moy_sae_14_3": "",
"moy_sae_15_3": "", "moy_sae_15_3": "",
"ues_validables": "", "ues_validables": "",
"nbabs": 0, "nbabs": 1,
"nbabsjust": 0, "nbabsjust": 0,
"part_2": "D\u00e9m.",
"code_cursus": "S1", "code_cursus": "S1",
"bac": "", "bac": "",
"specialite": "", "specialite": "",

View File

@ -1,36 +1,38 @@
"entry_name";"url";"permission";"method";"content" "entry_name";"url";"permission";"method";"content"
"assiduite";"/assiduite/1";"ScoView";"GET"; "assiduite";"/assiduite/1";"ScoView";"GET";
"assiduites";"/assiduites/1";"ScoView";"GET"; "assiduite_justificatifs";"/assiduite/1/justificatifs";"ScoView";"GET";
"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET"; "assiduite_justificatifs";"/assiduite/1/justificatifs/long";"ScoView";"GET";
"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduites";"/assiduites/1/query?with_justifs=";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?split";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?split";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"assiduites";"/assiduites/1";"ScoView";"GET";
"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET";
"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduites";"/assiduites/1/query?with_justifs=";"ScoView";"GET";
"assiduites_evaluations";"/assiduites/1/evaluations";"ScoView";"GET";
"assiduites_group";"/assiduites/group/query?etudids=1,2,3";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET"; "assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET"; "assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; "assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"assiduites_group";"/assiduites/group/query?etudids=1,2,3";"ScoView";"GET";
"assiduites_justificatifs";"/assiduite/1/justificatifs";"ScoView";"GET";
"assiduites_justificatifs";"/assiduite/1/justificatifs/long";"ScoView";"GET";
"assiduite_create";"/assiduite/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]" "assiduite_create";"/assiduite/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]"
"assiduite_create";"/assiduite/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]" "assiduite_create";"/assiduite/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]"
"assiduites_create";"/assiduites/create";"UsersAdmin";"POST";"[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]" "assiduites_create";"/assiduites/create";"UsersAdmin";"POST";"[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]"
"assiduites_create";"/assiduites/create";"UsersAdmin";"POST";"[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]" "assiduites_create";"/assiduites/create";"UsersAdmin";"POST";"[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]"
"assiduite_delete";"/assiduite/delete";"UsersAdmin";"POST";"[2,2,3]"
"assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"":""absent""}" "assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"":""absent""}"
"assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""moduleimpl_id"":2}" "assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""moduleimpl_id"":2}"
"assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}" "assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
"assiduite_delete";"/assiduite/delete";"UsersAdmin";"POST";"[2,2,3]"
"justificatif";"/justificatif/1";"ScoView";"GET"; "justificatif";"/justificatif/1";"ScoView";"GET";
"justificatifs";"/justificatifs/1";"ScoView";"GET"; "justificatifs";"/justificatifs/1";"ScoView";"GET";
"justificatifs";"/justificatifs/1/query?etat=attente";"ScoView";"GET"; "justificatifs";"/justificatifs/1/query?etat=attente";"ScoView";"GET";
"justificatifs_dept";"/justificatifs/dept/1";"ScoView";"GET"; "justificatifs_dept";"/justificatifs/dept/1";"ScoView";"GET";
"justificatifs_formsemestre";"/justificatifs/formsemestre/1";"ScoView";"GET"; "justificatifs_formsemestre";"/justificatifs/formsemestre/1";"ScoView";"GET";
"justificatif_create";"/justificatif/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""attente""}]" "justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""etat"":""valide""}"
"justificatif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""etat"":""valide""}" "justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""raison"":""MEDIC""}"
"justificatif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""raison"":""MEDIC""}" "justif_delete";"/justificatif/delete";"UsersAdmin";"POST";"[2, 2, 3]"
"justificatif_delete";"/justificatif/delete";"UsersAdmin";"POST";"[2,2,3]" "justif_list";"/justificatif/1/list";"ScoView";"GET";
"justif_justifies";"/justificatif/1/justifies";"UsersAdmin";"GET";
1 entry_name url permission method content
2 assiduite /assiduite/1 ScoView GET
3 assiduites assiduite_justificatifs /assiduites/1 /assiduite/1/justificatifs ScoView GET
4 assiduites assiduite_justificatifs /assiduites/1/query?etat=retard /assiduite/1/justificatifs/long ScoView GET
assiduites /assiduites/1/query?moduleimpl_id=1 ScoView GET
assiduites /assiduites/1/query?with_justifs= ScoView GET
5 assiduites_count /assiduites/1/count ScoView GET
6 assiduites_count /assiduites/1/count/query?etat=retard ScoView GET
7 assiduites_count /assiduites/1/count/query?split ScoView GET
8 assiduites_count /assiduites/1/count/query?etat=present,retard&metric=compte,heure ScoView GET
9 assiduites /assiduites/1 ScoView GET
10 assiduites /assiduites/1/query?etat=retard ScoView GET
11 assiduites /assiduites/1/query?moduleimpl_id=1 ScoView GET
12 assiduites /assiduites/1/query?with_justifs= ScoView GET
13 assiduites_evaluations /assiduites/1/evaluations ScoView GET
14 assiduites_group /assiduites/group/query?etudids=1,2,3 ScoView GET
15 assiduites_formsemestre /assiduites/formsemestre/1 ScoView GET
16 assiduites_formsemestre /assiduites/formsemestre/1/query?etat=retard ScoView GET
17 assiduites_formsemestre /assiduites/formsemestre/1/query?moduleimpl_id=1 ScoView GET
18 assiduites_formsemestre_count /assiduites/formsemestre/1/count ScoView GET
19 assiduites_formsemestre_count /assiduites/formsemestre/1/count/query?etat=retard ScoView GET
20 assiduites_formsemestre_count /assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure ScoView GET
assiduites_group /assiduites/group/query?etudids=1,2,3 ScoView GET
assiduites_justificatifs /assiduite/1/justificatifs ScoView GET
assiduites_justificatifs /assiduite/1/justificatifs/long ScoView GET
21 assiduite_create /assiduite/1/create UsersAdmin POST [{"date_debut": "2023-10-27T08:00","date_fin": "2023-10-27T10:00","etat": "absent"}]
22 assiduite_create /assiduite/1/create UsersAdmin POST [{"date_debut": "2023-10-27T08:00","date_fin": "2023-10-27T10:00","etat": "absent"}]
23 assiduites_create /assiduites/create UsersAdmin POST [{"etudid":1,"date_debut": "2023-10-26T08:00","date_fin": "2023-10-26T10:00","etat": "absent"}]
24 assiduites_create /assiduites/create UsersAdmin POST [{"etudid":-1,"date_debut": "2023-10-26T08:00","date_fin": "2023-10-26T10:00","etat": "absent"}]
25 assiduite_delete /assiduite/delete UsersAdmin POST [2,2,3]
26 assiduite_edit /assiduite/1/edit UsersAdmin POST {"etat":"absent"}
27 assiduite_edit /assiduite/1/edit UsersAdmin POST {"moduleimpl_id":2}
28 assiduite_edit /assiduite/1/edit UsersAdmin POST {"etat": "retard","moduleimpl_id":3}
assiduite_delete /assiduite/delete UsersAdmin POST [2,2,3]
29 justificatif /justificatif/1 ScoView GET
30 justificatifs /justificatifs/1 ScoView GET
31 justificatifs /justificatifs/1/query?etat=attente ScoView GET
32 justificatifs_dept /justificatifs/dept/1 ScoView GET
33 justificatifs_formsemestre /justificatifs/formsemestre/1 ScoView GET
34 justificatif_create justif_edit /justificatif/1/create /justificatif/1/edit UsersAdmin POST [{"date_debut": "2023-10-27T08:00","date_fin": "2023-10-27T10:00","etat": "attente"}] {"etat":"valide"}
35 justificatif_edit justif_edit /justificatif/1/edit UsersAdmin POST {"etat":"valide"} {"raison":"MEDIC"}
36 justificatif_edit justif_delete /justificatif/1/edit /justificatif/delete UsersAdmin POST {"raison":"MEDIC"} [2, 2, 3]
37 justificatif_delete justif_list /justificatif/delete /justificatif/1/list UsersAdmin ScoView POST GET [2,2,3]
38 justif_justifies /justificatif/1/justifies UsersAdmin GET

View File

@ -1,120 +1,90 @@
"entry_name";"url";"permission";"method";"content" "entry_name";"url";"permission";"method";"content"
"assiduite_create";"/assiduite/1/create";"ScoView";"POST";"{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""}"
"assiduite_create";"/assiduite/1/create/batch";"ScoView";"POST";"{""batch"":[{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""absent""},{""date_debut"": ""2022-10-27T08:00"",""date_fin"": ""2022-10-27T10:00"",""etat"": ""retard""},{""date_debut"": ""2022-10-27T11:00"",""date_fin"": ""2022-10-27T13:00"",""etat"": ""present""}]}"
"assiduite_delete";"/assiduite/delete";"ScoView";"POST";"{""assiduite_id"": 1}"
"assiduite_delete";"/assiduite/delete/batch";"ScoView";"POST";"{""batch"":[2,2,3]}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""etat"":""absent""}"
"assiduite_edit";"/assiduite/1/edit";"ScoView";"POST";"{""moduleimpl_id"":2}"
"assiduite";"/assiduite/1";"ScoView";"GET"; "assiduite";"/assiduite/1";"ScoView";"GET";
"assiduite_justificatifs";"/assiduite/1/justificatifs";"ScoView";"GET";
"assiduite_justificatifs";"/assiduite/1/justificatifs/long";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?split";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET";
"assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET";
"assiduites";"/assiduites/1";"ScoView";"GET"; "assiduites";"/assiduites/1";"ScoView";"GET";
"assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET"; "assiduites";"/assiduites/1/query?etat=retard";"ScoView";"GET";
"assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET"; "assiduites";"/assiduites/1/query?moduleimpl_id=1";"ScoView";"GET";
"departement-create";"/departement/create";"ScoSuperAdmin";"POST";"{""acronym"": ""NEWONE"" , ""visible"": true}" "assiduites";"/assiduites/1/query?with_justifs=";"ScoView";"GET";
"departement-delete";"/departement/NEWONE/delete";"ScoSuperAdmin";"POST"; "assiduites_evaluations";"/assiduites/1/evaluations";"ScoView";"GET";
"departement-edit";"/departement/NEWONE/edit";"ScoSuperAdmin";"POST";"{""visible"": false}" "assiduites_group";"/assiduites/group/query?etudids=1,2,3";"ScoView";"GET";
"departement-etudiants";"/departement/id/1/etudiants";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1";"ScoView";"GET";
"departement-etudiants";"/departement/TAPI/etudiants";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1/query?etat=retard";"ScoView";"GET";
"departement-formsemestres_ids";"/departement/id/1/formsemestres_ids";"ScoView";"GET"; "assiduites_formsemestre";"/assiduites/formsemestre/1/query?moduleimpl_id=1";"ScoView";"GET";
"departement-formsemestres_ids";"/departement/TAPI/formsemestres_ids";"ScoView";"GET"; "assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"ScoView";"GET";
"departement-formsemestres-courants";"/departement/id/1/formsemestres_courants";"ScoView";"GET"; "assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=retard";"ScoView";"GET";
"departement-formsemestres-courants";"/departement/TAPI/formsemestres_courants";"ScoView";"GET"; "assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"ScoView";"GET";
"departement-logo";"/departement/id/1/logo/D";"ScoSuperAdmin";"GET"; "assiduite_create";"/assiduite/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]"
"departement-logo";"/departement/TAPI/logo/D";"ScoSuperAdmin";"GET"; "assiduite_create";"/assiduite/1/create";"UsersAdmin";"POST";"[{""date_debut"": ""2023-10-27T08:00"",""date_fin"": ""2023-10-27T10:00"",""etat"": ""absent""}]"
"departement-logos";"/departement/id/1/logos";"ScoSuperAdmin";"GET"; "assiduites_create";"/assiduites/create";"UsersAdmin";"POST";"[{""etudid"":1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]"
"departement-logos";"/departement/TAPI/logos";"ScoSuperAdmin";"GET"; "assiduites_create";"/assiduites/create";"UsersAdmin";"POST";"[{""etudid"":-1,""date_debut"": ""2023-10-26T08:00"",""date_fin"": ""2023-10-26T10:00"",""etat"": ""absent""}]"
"departement";"/departement/id/1";"ScoView";"GET"; "assiduite_delete";"/assiduite/delete";"UsersAdmin";"POST";"[2,2,3]"
"departement";"/departement/TAPI";"ScoView";"GET"; "assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"":""absent""}"
"departements-ids";"/departements_ids";"ScoView";"GET"; "assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""moduleimpl_id"":2}"
"departements";"/departements";"ScoView";"GET"; "assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}"
"etudiant_formsemestres";"/etudiant/nip/11/formsemestres";"ScoView";"GET"; "billets_absence_create";"/billets_absence/create";"UsersAdmin";"POST";"{""etudid"":""1"",""abs_begin"":""2023-10-27T10:00"",""abs_end"":""2023-10-28T10:00"",""description"":""grave malade"",""justified"":""1""}"
"etudiant-formsemestre-bulletin";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET"; "departements_list";"/departements";"ScoView";"GET";
#"etudiant-formsemestre-bulletin";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET"; "departements_ids";"/departements_ids";"ScoView";"GET";
#"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET"; "departement_by_acronym";"/departement/TAPI";"ScoView";"GET";
#"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET"; "departement_get";"/departement/id/1";"ScoView";"GET";
"etudiant-formsemestre-groups";"/etudiant/etudid/11/formsemestre/1/groups";"ScoView";"GET"; "departement_create";"/departement/create";"UsersAdmin";"POST";"{""acronym"":""MYDEPT"",""visible"":""1""}"
"etudiant-formsemestres";"/etudiant/etudid/11/formsemestres";"ScoView";"GET"; "departement_etudiants";"/departement/TAPI/etudiants";"ScoView";"GET";
"etudiant-formsemestres";"/etudiant/ine/INE11/formsemestres";"ScoView";"GET"; "departement_formsemestres_ids";"/departement/TAPI/formsemestres_ids";"ScoView";"GET";
"etudiant";"/etudiant/etudid/11";"ScoView";"GET"; "departement_formsemestres_ids_by_id";"/departement/id/1/formsemestres_ids";"ScoView";"GET";
"etudiant";"/etudiant/ine/INE11";"ScoView";"GET"; "departement_formsemestres_courants";"/departement/id/1/formsemestres_courants?date_courante=2022-01-01";"ScoView";"GET";
"etudiant";"/etudiant/nip/11";"ScoView";"GET"; "etudiants_courants";"/etudiants/courants?date_courante=2022-05-01";"ScoView";"GET";
"etudiants-clef";"/etudiants/etudid/11";"ScoView";"GET"; "etudiants_courants";"/etudiants/courants/long?date_courante=2022-05-01";"ScoView";"GET";
"etudiants-clef";"/etudiants/ine/INE11";"ScoView";"GET"; "bulletin";"/etudiant/etudid/1/formsemestre/1/bulletin";"ScoView";"GET";
"etudiants-clef";"/etudiants/nip/11";"ScoView";"GET"; "etudiant_groups";"/etudiant/etudid/1/formsemestre/1/groups";"ScoView";"GET";
"etudiants-courants";"/etudiants/courants?date_courante=2022-07-20";"ScoView";"GET"; "etudiant_edit";"/etudiant/ine/INE1/edit";"UsersAdmin";"POST";"{""prenom"":""Nouveau Prénom"", ""adresses"":[{""email"":""nouvelle@adresse.fr""}]}"
"etudiants-courants";"/etudiants/courants/long?date_courante=2022-07-20";"ScoView";"GET"; "etudiant_annotation";"/etudiant/etudid/1/annotation";"UsersAdmin";"POST";"{""comment"":""une annotation sur l'étudiant""}"
"evaluation";"/evaluation/1";"ScoView";"GET"; "moduleimpl_evaluations";"/moduleimpl/1/evaluations";"ScoView";"GET";
"evaluation-notes";"/evaluation/1/notes";"ScoView";"GET"; "evaluation_notes";"/evaluation/2/notes";"ScoView";"GET";
"formation-export";"/formation/1/export_with_ids";"ScoView";"GET"; "evaluation_set_notes";"/evaluation/1/notes/set";"UsersAdmin";"POST";"{""notes"": [[1, 17], [2, ""SUPR""]], ""comment"" : ""sample test""}"
"formation-export";"/formation/1/export";"ScoView";"GET"; "evaluation_create";"/moduleimpl/1/evaluation/create";"UsersAdmin";"POST";"{""description"":""Exemple éval.""}"
"formation-referentiel_competences";"/formation/1/referentiel_competences";"ScoView";"GET";
"formation";"/formation/1";"ScoView";"GET";
"formations_ids";"/formations_ids";"ScoView";"GET";
"formations";"/formations";"ScoView";"GET"; "formations";"/formations";"ScoView";"GET";
"formsemestre-bulletins";"/formsemestre/1/bulletins";"ScoView";"GET"; "formations_ids";"/formations_ids";"ScoView";"GET";
"formsemestre-decisions_jury";"/formsemestre/1/decisions_jury";"ScoView";"GET"; "formation_get";"/formation/1";"ScoView";"GET";
"formsemestre-etat_evals";"/formsemestre/1/etat_evals";"ScoView";"GET"; "formation_export_by_formation_id";"/formation/1/export";"ScoView";"GET";
"formsemestre-etudiants-query";"/formsemestre/1/etudiants/query?etat=D";"ScoView";"GET"; "referentiel_competences";"/formation/1/referentiel_competences";"ScoView";"GET";
"formsemestre-etudiants";"/formsemestre/1/etudiants";"ScoView";"GET"; "formation_module_get";"/formation/module/1";"ScoView";"GET";
"formsemestre-etudiants";"/formsemestre/1/etudiants/long";"ScoView";"GET"; "formsemestre_get";"/formsemestre/1";"ScoView";"GET";
"formsemestre-partition-create";"/formsemestre/1/partition/create";"ScoSuperAdmin";"POST";"{""partition_name"": ""PART""} " "bulletins";"/formsemestre/1/bulletins";"ScoView";"GET";
"formsemestre-partitions-order";"/formsemestre/1/partitions/order";"ScoSuperAdmin";"POST";"[ 1 ]" "formsemestre_programme";"/formsemestre/1/programme";"ScoView";"GET";
"formsemestre-partitions";"/formsemestre/1/partitions";"ScoView";"GET"; "formsemestre_etudiants";"/formsemestre/1/etudiants/query";"ScoView";"GET";
"formsemestre-programme";"/formsemestre/1/programme";"ScoView";"GET"; "formsemestre_etat_evaluations";"/formsemestre/1/etat_evals";"ScoView";"GET";
"formsemestre-resultats";"/formsemestre/1/resultats";"ScoView";"GET"; "formsemestre_resultat";"/formsemestre/1/resultats";"ScoView";"GET";
"formsemestre";"/formsemestre/1";"ScoView";"GET"; "decisions_jury";"/formsemestre/1/decisions_jury";"ScoView";"GET";
"formsemestres-query";"/formsemestres/query?annee_scolaire=2022&etape_apo=A2";"ScoView";"GET"; "justificatif";"/justificatif/1";"ScoView";"GET";
"formsemestres-query";"/formsemestres/query?nip=11";"ScoView";"GET"; "justificatifs";"/justificatifs/1";"ScoView";"GET";
"group-delete";"/group/2/delete";"ScoSuperAdmin";"POST"; "justificatifs";"/justificatifs/1/query?etat=attente";"ScoView";"GET";
"group-edit";"/group/2/edit";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP2""}" "justificatifs_dept";"/justificatifs/dept/1";"ScoView";"GET";
"group-etudiants-query";"/group/1/etudiants/query?etat=D";"ScoView";"GET"; "justificatifs_formsemestre";"/justificatifs/formsemestre/1";"ScoView";"GET";
"group-etudiants";"/group/1/etudiants";"ScoView";"GET"; "justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""etat"":""valide""}"
"group-remove_etudiant";"/group/1/remove_etudiant/10";"ScoSuperAdmin";"POST"; "justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""raison"":""MEDIC""}"
"group-set_etudiant";"/group/1/set_etudiant/10";"ScoSuperAdmin";"POST"; "justif_delete";"/justificatif/delete";"UsersAdmin";"POST";"[2, 2, 3]"
"logo";"/logo/B";"ScoSuperAdmin";"GET"; "justif_list";"/justificatif/1/list";"ScoView";"GET";
"logos";"/logos";"ScoSuperAdmin";"GET"; "justif_justifies";"/justificatif/1/justifies";"UsersAdmin";"GET";
"moduleimpl-evaluations";"/moduleimpl/1/evaluations";"ScoView";"GET"; "logo_list_globals";"/logos";"UsersAdmin";"GET";
"moduleimpl";"/moduleimpl/1";"ScoView";"GET"; "logo_get_global";"/logo/B";"UsersAdmin";"GET";
"partition-delete";"/partition/2/delete";"ScoSuperAdmin";"POST"; "departement_logos";"/departement/TAPI/logos";"UsersAdmin";"GET";
"partition-edit";"/partition/1/edit";"ScoSuperAdmin";"POST";"{""partition_name"":""P2BIS"", ""numero"":3,""bul_show_rank"":true,""show_in_lists"":false, ""groups_editable"":true}" "moduleimpl_inscriptions";"/moduleimpl/1/inscriptions";"ScoView";"GET";
"partition-group-create";"/partition/1/group/create";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP""}" "moduleimpl_notes";"/moduleimpl/1/notes";"ScoView";"GET";
"partition-groups-order";"/partition/1/groups/order";"ScoSuperAdmin";"POST";"[ 1 ]" "partition_info";"/partition/1";"ScoView";"GET";
"partition-remove_etudiant";"/partition/2/remove_etudiant/10";"ScoSuperAdmin";"POST"; "formsemestre_partitions";"/formsemestre/1/partitions";"ScoView";"GET";
"partition";"/partition/1";"ScoView";"GET"; "group_etudiants";"/group/1/etudiants";"ScoView";"GET";
"permissions";"/permissions";"ScoView";"GET"; "group_create";"/partition/1/group/create";"ScoView";"POST";"{""group_name"" : ""Nouveau Groupe""}"
"role-add_permission";"/role/customRole/add_permission/UsersView";"ScoSuperAdmin";"POST"; "group_edit";"/group/1/edit";"ScoView";"POST";"{""group_name"":""A1""}"
"role-create";"/role/create/customRole";"ScoSuperAdmin";"POST";"{""permissions"": [""ScoView"", ""UsersView""]}" "group_set_edt_id";"/group/1/set_edt_id/EDT_GR1";"ScoView";"POST";
"role-delete";"/role/customRole/delete";"ScoSuperAdmin";"POST"; "partition_edit";"/partition/1/edit";"ScoView";"POST";"{""bul_show_rank"":1}"
"role-edit";"/role/customRole/edit";"ScoSuperAdmin";"POST";"{ ""name"" : ""LaveurDeVitres"", ""permissions"" : [ ""ScoView"" ] }" "user_info";"/user/2";"UsersAdmin";"GET";
"role-remove_permission";"/role/customRole/remove_permission/UsersView";"ScoSuperAdmin";"POST"; "user_password";"/user/3/password";"UsersAdmin";"POST";"{""password"" : ""rePlaCemeNT456averylongandcomplicated""}"
"role";"/role/Observateur";"ScoView";"GET"; "permissions_list";"/permissions";"UsersAdmin";"GET";
"roles";"/roles";"ScoView";"GET"; "role_get";"/role/Ens";"UsersAdmin";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET"; "roles_list";"/roles";"UsersAdmin";"GET";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/pdf";"ScoView";"GET"; "role_create";"/role/create/customRole";"UsersAdmin";"POST";"{""permissions"": [""ScoView"", ""UsersView""]}"
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short";"ScoView";"GET"; "role_delete";"/role/customRole/delete";"UsersAdmin";"POST";
"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/ine/INE11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/pdf";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/short";"ScoView";"GET";
"test-pdf";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET";
"user-create";"/user/create";"ScoSuperAdmin";"POST";"{""user_name"": ""alain"", ""dept"": null, ""nom"": ""alain"", ""prenom"": ""bruno"", ""active"": true }"
"user-edit";"/user/10/edit";"ScoSuperAdmin";"POST";"{ ""dept"": ""TAPI"", ""nom"": ""alain2"", ""prenom"": ""bruno2"", ""active"": false }"
"user-password";"/user/3/password";"ScoSuperAdmin";"POST";"{ ""password"": ""rePlaCemeNT456averylongandcomplicated"" }"
"user-password";"/user/3/password";"ScoSuperAdmin";"POST";"{ ""password"": ""too_simple"" }"
"user-role-add";"/user/10/role/Observateur/add";"ScoSuperAdmin";"POST";
"user-role-remove";"/user/10/role/Observateur/remove";"ScoSuperAdmin";"POST";
"user";"/user/1";"ScoView";"GET";
"users-query";"/users/query?starts_with=u_";"ScoView";"GET";
Can't render this file because it contains an unexpected character in line 41 and column 2.

View File

@ -10,4 +10,4 @@ from tools.migrate_scodoc7_archives import migrate_scodoc7_dept_archives
from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos from tools.migrate_scodoc7_logos import migrate_scodoc7_dept_logos
from tools.migrate_abs_to_assiduites import migrate_abs_to_assiduites from tools.migrate_abs_to_assiduites import migrate_abs_to_assiduites
from tools.downgrade_assiduites import downgrade_module from tools.downgrade_assiduites import downgrade_module
from tools.create_api_map import gen_api_map from tools.create_api_map import gen_api_map, gen_api_doc

View File

@ -4,8 +4,14 @@ Script permettant de générer une carte SVG de l'API de ScoDoc
Écrit par Matthias HARTMANN Écrit par Matthias HARTMANN
""" """
import xml.etree.ElementTree as ET
import re import re
import sys
import unicodedata
import xml.etree.ElementTree as ET
from flask import render_template
from app.auth.models import Permission
class COLORS: class COLORS:
@ -132,7 +138,7 @@ class Token:
element, x_offset, y_offset element, x_offset, y_offset
) )
# Préparation du lien vers la doc de la route # Préparation du lien vers la doc de la route
href = "#" + self.func_name.replace("_", "-") href = "#" + self.func_name
if self.query and not href.endswith("-query"): if self.query and not href.endswith("-query"):
href += "-query" href += "-query"
question_mark_group = _create_question_mark_group(current_end_coords, href) question_mark_group = _create_question_mark_group(current_end_coords, href)
@ -266,6 +272,13 @@ class Token:
return group return group
def strip_accents(s):
"""Retourne la chaîne s séparant les accents et les caractères de base."""
return "".join(
c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn"
)
def _create_svg_element(text, color="rgb(230,156,190)"): def _create_svg_element(text, color="rgb(230,156,190)"):
""" """
Fonction générale pour créer un élément SVG simple Fonction générale pour créer un élément SVG simple
@ -437,16 +450,15 @@ def _create_question_mark_group(coords, href):
return group return group
def gen_api_map(app, endpoint_start="api"): def analyze_api_routes(app, endpoint_start: str) -> tuple:
""" """Parcours de toutes les routes de l'application
Fonction permettant de générer une carte SVG de l'API de ScoDoc analyse docstrings
Elle récupère les routes de l'API et les transforme en un arbre de Token
puis génère un fichier SVG à partir de cet arbre
""" """
# Création du token racine # Création du token racine
api_map = Token("") api_map = Token("")
# Parcours de toutes les routes de l'application doctable_lines: dict[str, dict] = {}
for rule in app.url_map.iter_rules(): for rule in app.url_map.iter_rules():
# On ne garde que les routes de l'API / APIWEB # On ne garde que les routes de l'API / APIWEB
if not rule.endpoint.lower().startswith(endpoint_start.lower()): if not rule.endpoint.lower().startswith(endpoint_start.lower()):
@ -461,8 +473,8 @@ def gen_api_map(app, endpoint_start="api"):
# Récupération de la fonction associée à la route # Récupération de la fonction associée à la route
func = app.view_functions[rule.endpoint] func = app.view_functions[rule.endpoint]
func_name = parse_doc_name(func.__doc__ or "") or func.__name__ doc_dict = _parse_doc_string(func.__doc__ or "")
func_name = doc_dict.get("DOC_ANCHOR", [None])[0] or func.__name__
# Pour chaque segment de la route # Pour chaque segment de la route
for i, segment in enumerate(segments): for i, segment in enumerate(segments):
# On cherche si le segment est déjà un enfant du token courant # On cherche si le segment est déjà un enfant du token courant
@ -500,8 +512,55 @@ def gen_api_map(app, endpoint_start="api"):
child.method = method child.method = method
current_token.add_child(child) current_token.add_child(child)
href = func_name
if child.query and not href.endswith("-query"):
href += "-query"
# category
category: str = func.__module__.replace("app.api.", "")
mod_doc: str = sys.modules[func.__module__].__doc__ or ""
mod_doc_dict: dict = _parse_doc_string(mod_doc)
if mod_doc_dict.get("CATEGORY"):
category = mod_doc_dict["CATEGORY"][0].strip()
permissions: str
try:
permissions: str = ", ".join(
sorted(Permission.permissions_names(func.scodoc_permission))
)
except AttributeError:
permissions = "Aucune permission requise"
if func_name not in doctable_lines:
doctable_lines[func_name] = {
"method": method,
"nom": func_name,
"href": href,
"query": doc_dict.get("QUERY", "") != "",
"permission": permissions,
"description": doc_dict.get("", ""),
"params": doc_dict.get("PARAMS", ""),
"category": doc_dict.get("CATEGORY", [False])[0] or category,
"samples": doc_dict.get("SAMPLES"),
}
# On met à jour le token courant pour le prochain segment # On met à jour le token courant pour le prochain segment
current_token = child current_token = child
if func_name in doctable_lines: # endpoint déjà ajouté, ajoute au besoin route
doctable_lines[func_name]["routes"] = doctable_lines[func_name].get(
"routes", []
) + [rule.rule]
return api_map, doctable_lines
# point d'entrée de la commande `flask gen-api-map`
def gen_api_map(api_map: Token, doctable_lines: dict[str, dict]) -> str:
"""
Fonction permettant de générer une carte SVG de l'API de ScoDoc
Elle récupère les routes de l'API et les transforme en un arbre de Token
puis génère un fichier SVG à partir de cet arbre
"""
# On génère le SVG à partir de l'arbre de Token # On génère le SVG à partir de l'arbre de Token
generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg") generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg")
@ -510,6 +569,11 @@ def gen_api_map(app, endpoint_start="api"):
+ "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg" + "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg"
) )
# On génère le tableau à partir de doctable_lines
table = _gen_table(sorted(doctable_lines.values(), key=lambda x: x["nom"]))
_write_gen_table(table)
return table
def _get_bbox(element, x_offset=0, y_offset=0): def _get_bbox(element, x_offset=0, y_offset=0):
""" """
@ -614,9 +678,47 @@ def generate_svg(element, fname):
tree.write(fname, encoding="utf-8", xml_declaration=True) tree.write(fname, encoding="utf-8", xml_declaration=True)
def _get_doc_lines(keyword, doc) -> list[str]: def _parse_doc_string(doc_string: str) -> dict[str, list[str]]:
"""Parse doc string and extract a dict:
{
"" : description_lines,
"keyword" : lines
}
In the docstring, each keyword is associated to a section like
KEYWORD
-------
...
(blank line)
All non blank lines not associated to a keyword go to description.
"""
doc_dict = {}
matches = re.finditer(
r"^\s*(?P<kw>[A-Z_\-]+)$\n^\s*-+\n(?P<txt>(^(?!\s*$).+$\n?)+)",
doc_string,
re.MULTILINE,
)
description = ""
i = 0
for match in matches:
start, end = match.span()
description += doc_string[i:start]
doc_dict[match.group("kw")] = [
x.strip() for x in match.group("txt").split("\n") if x.strip()
]
i = end
description += doc_string[i:]
doc_dict[""] = description.split("\n")
return doc_dict
def _get_doc_lines(keyword, doc_string: str) -> list[str]:
""" """
Renvoie les lignes de la doc qui suivent le mot clé keyword Renvoie les lignes de la doc qui suivent le mot clé keyword
Attention : s'arrête à la première ligne vide
La doc doit contenir des lignes de la forme: La doc doit contenir des lignes de la forme:
@ -626,7 +728,7 @@ def _get_doc_lines(keyword, doc) -> list[str]:
""" """
# Récupérer les lignes de la doc # Récupérer les lignes de la doc
lines = [line.strip() for line in doc.split("\n")] lines = [line.strip() for line in doc_string.split("\n")]
# On cherche la ligne "KEYWORD" et on vérifie que la ligne suivante est "-----" # On cherche la ligne "KEYWORD" et on vérifie que la ligne suivante est "-----"
# Si ce n'est pas le cas, on renvoie un dictionnaire vide # Si ce n'est pas le cas, on renvoie un dictionnaire vide
try: try:
@ -638,27 +740,18 @@ def _get_doc_lines(keyword, doc) -> list[str]:
return [] return []
# On récupère les lignes de la doc qui correspondent au keyword (enfin on espère) # On récupère les lignes de la doc qui correspondent au keyword (enfin on espère)
kw_lines = lines[kw_index + 2 :] kw_lines = lines[kw_index + 2 :]
# On s'arrête à la première ligne vide
first_empty_line: int
try:
first_empty_line: int = kw_lines.index("")
except ValueError:
first_empty_line = len(kw_lines)
kw_lines = kw_lines[:first_empty_line]
return kw_lines return kw_lines
def parse_doc_name(doc): def parse_query_doc(doc_string: str) -> dict[str, str]:
"""
renvoie le nom de la route à partir de la docstring
La doc doit contenir des lignes de la forme:
DOC_ANCHOR
----------
nom_de_la_route
Il ne peut y avoir qu'une seule ligne suivant -----
"""
name_lines: list[str] = _get_doc_lines("DOC_ANCHOR", doc)
return name_lines[0] if name_lines else None
def parse_query_doc(doc):
""" """
renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>}) renvoie un dictionnaire {param: <type:nom_param>} (ex: {assiduite_id : <int:assiduite_id>})
@ -673,7 +766,7 @@ def parse_query_doc(doc):
Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser Dès qu'une ligne ne respecte pas ce format (voir regex dans la fonction), on arrête de parser
Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre Attention, la ligne ----- doit être collée contre QUERY et contre le premier paramètre
""" """
query_lines: list[str] = _get_doc_lines("QUERY", doc) query_lines: list[str] = _get_doc_lines("QUERY", doc_string)
query = {} query = {}
regex = re.compile(r"^(\w+):(<.+>)$") regex = re.compile(r"^(\w+):(<.+>)$")
@ -691,33 +784,190 @@ def parse_query_doc(doc):
return query return query
if __name__ == "__main__": def _gen_table_line(doctable: dict = None):
# Exemple d'utilisation de la classe Token """
# Exemple simple de création d'un arbre de Token Génère une ligne de tableau markdown
root = Token("api") | nom de la route| methode HTTP| Permission |
child1 = Token("assiduites", leaf=True) """
child1.func_name = "assiduites_get"
child2 = Token("count") nom, method, permission = (
child22 = Token("all") doctable.get("nom", ""),
child23 = Token( doctable.get("method", ""),
"query", doctable.get("permission", ""),
query={
"etat": "<string:etat>",
"moduleimpl_id": "<int:moduleimpl_id>",
"count": "<int:count>",
"formsemestre_id": "<int:formsemestre_id>",
},
) )
child3 = Token("justificatifs", "POST")
child3.func_name = "justificatifs_post"
root.add_child(child1) if doctable is None:
child1.add_child(child2) doctable = {}
child2.add_child(child22)
child2.add_child(child23)
root.add_child(child3)
group_element = root.to_svg_group() lien: str = doctable.get("href", nom)
generate_svg(group_element, "/tmp/api_map.svg") doctable["query"]: bool
if doctable.get("query") and not lien.endswith("-query"):
lien += "-query"
nav: str = f"[{nom}]({'#'+lien})"
table: str = "|"
for string in [nav, method, doctable.get("permissions") or permission]:
table += f" {string} |"
return table
def _gen_table_head() -> str:
"""
Génère la première ligne du tableau markdown
"""
headers: str = "| Route | Méthode | Permission |"
line: str = "|---|---|---|"
return f"{headers}\n{line}\n"
def _gen_table(lines: list[dict]) -> str:
"""
Génère un tableau markdown à partir d'une liste de lignes
lines : liste de dictionnaire au format doc_lines.
"""
table = _gen_table_head()
table += "\n".join([_gen_table_line(line) for line in lines])
return table
def _gen_csv_line(doc_line: dict) -> str:
"""
Génère les lignes de tableau csv en fonction d'une route (doc_line)
format :
"entry_name";"url";"permission";"method";"content"
"""
entry_name: str = doc_line.get("nom", "")
method: str = doc_line.get("method", "GET")
permission: str = (
"UsersAdmin" if doc_line.get("permission") != "ScoView" else "ScoView"
)
samples: list[str] = doc_line.get("samples", [])
csv_lines: list[str] = []
for sample in samples:
fragments = sample.split(";", maxsplit=1)
if len(fragments) == 2:
url, content = fragments
elif len(fragments) == 1:
url, content = fragments[0], ""
else:
raise ValueError(f"Error: sample invalide: {sample}")
csv_line = f'"{entry_name}";"{url}";"{permission}";"{method}";'
if content:
csv_line += f'"{content}"'
csv_lines.append(csv_line)
return "\n".join(csv_lines)
def _gen_csv(lines: list[dict], filename: str = "/tmp/samples.csv") -> str:
"""
Génère un fichier csv à partir d'une liste de lignes
lines : liste de dictionnaire au format doc_lines.
"""
csv = '"entry_name";"url";"permission";"method";"content"\n'
csv += "\n".join(
[_gen_csv_line(line) for line in lines if line.get("samples") is not None]
)
with open(filename, "w", encoding="UTF-8") as f:
f.write(csv)
print(
f"Les samples ont été générés avec succès. Vous pouvez le consulter à l'adresse suivante : {filename}"
)
def _write_gen_table(table: str, filename: str = "/tmp/api_table.md"):
"""Ecriture du fichier md avec la table"""
with open(filename, "w", encoding="UTF-8") as f:
f.write(table)
print(
f"Le tableau a été généré avec succès. Vous pouvez le consulter à l'adresse suivante : {filename}"
)
def doc_route(doctable: dict) -> str:
"""Generate markdown doc for a route"""
jinja_obj: dict = {}
jinja_obj.update(doctable)
jinja_obj["nom"] = doctable["nom"].strip() # on retire les caractères blancs
if doctable.get("samples") is not None:
jinja_obj["sample"] = {
"nom": f"{jinja_obj['nom']}.json",
"href": f"{jinja_obj['nom']}.json.md",
}
jinja_obj["query"]: bool
if jinja_obj["query"]:
jinja_obj["nom"] += "(-query)"
if doctable.get("params"):
jinja_obj["params"] = []
for param in doctable["params"]:
frags = param.split(":", maxsplit=1)
if len(frags) == 2:
name, descr = frags
jinja_obj["params"].append(
{"nom": name.strip(), "description": descr.strip()}
)
else:
print(f"Warning: {doctable['nom']} : invalid PARAMS {param}")
if doctable.get("description"):
descr = "\n".join(s for s in doctable["description"])
jinja_obj["description"] = descr.strip()
return render_template("doc/apidoc.j2", doc=jinja_obj)
def gen_api_doc(app, endpoint_start="api."):
"commande gen-api-doc"
api_map, doctable_lines = analyze_api_routes(app, endpoint_start)
mddoc: str = ""
categories: dict = {}
for value in doctable_lines.values():
category = value["category"].capitalize()
if category not in categories:
categories[category] = []
categories[category].append(value)
# sort categories by name
categories: dict = dict(
sorted(categories.items(), key=lambda x: strip_accents(x[0]))
)
category: str
routes: list[dict]
for category, routes in categories.items():
# sort routes by name
routes.sort(key=lambda x: strip_accents(x["nom"]))
mddoc += f"### API {category.capitalize()}\n\n"
for route in routes:
mddoc += doc_route(route)
mddoc += "\n\n"
table_api = gen_api_map(api_map, doctable_lines)
mdpage = render_template("doc/ScoDoc9API.j2", doc_api=mddoc, table_api=table_api)
_gen_csv(list(doctable_lines.values()))
fname = "/tmp/ScoDoc9API.md"
with open(fname, "w", encoding="utf-8") as f:
f.write(mdpage)
print(
"La documentation API a été générée avec succès. "
f"Vous pouvez la consulter à l'adresse suivante : {fname}"
)

View File

@ -211,10 +211,17 @@ def create_formsemestre(
) )
db.session.add(modimpl) db.session.add(modimpl)
db.session.commit() db.session.commit()
# Partition par défaut (requise):
partition_id = sco_groups.partition_create( partition_id = sco_groups.partition_create(
formsemestre.id, default=True, redirect=False formsemestre.id, default=True, redirect=False
) )
group = sco_groups.create_group(partition_id, default=True) sco_groups.create_group(partition_id, default=True)
# Ajoute partition normale, TD avec groupes A et B:
partition_id = sco_groups.partition_create(
formsemestre.id, partition_name="TD", redirect=False
)
sco_groups.create_group(partition_id, group_name="A")
sco_groups.create_group(partition_id, group_name="B")
return formsemestre return formsemestre