Compare commits

...

5 Commits

Author SHA1 Message Date
Iziram
3b2888cd5b APIDoc : génération fichier samples 2024-07-25 13:03:10 +02:00
Iziram
b542e7dab5 API Doc : meilleur tri + optimisations simples 2024-07-25 12:00:40 +02:00
Iziram
f7a8c1d2db API Doc : lien query OK 2024-07-25 11:45:26 +02:00
5cefe1a337 Doc API: typos 2024-07-25 11:08:40 +02:00
c0d2f66081 Doc API: template + fix _ vs - 2024-07-25 10:55:30 +02:00
6 changed files with 520 additions and 107 deletions

View File

@ -64,6 +64,11 @@ def assiduite(assiduite_id: int = None):
"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)
@ -93,6 +98,11 @@ def assiduite_justificatifs(assiduite_id: int = None, long: bool = False):
... ...
] ]
``` ```
SAMPLES
-------
/assiduite/1/justificatifs;
/assiduite/1/justificatifs/long;
""" """
return get_assiduites_justif(assiduite_id, long) return get_assiduites_justif(assiduite_id, long)
@ -156,6 +166,13 @@ def assiduites_count(
metric: la/les métriques de comptage (journee, demi, heure, compte) metric: la/les métriques de comptage (journee, demi, heure, compte)
split: divise le comptage par état 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
@ -221,6 +238,7 @@ 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 PARAMS
----- -----
@ -231,6 +249,14 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
date_fin:date de fin de l'assiduité (infé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 etat:etat de l'étudiant &rightarrow; absent, present ou retard
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité 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=;
""" """
@ -300,6 +326,11 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
] ]
} }
] ]
SAMPLES
-------
/assiduites/1/evaluations;
``` ```
""" """
@ -344,7 +375,7 @@ def evaluation_assiduites(evaluation_id):
CATEGORY CATEGORY
-------- --------
evaluations Évaluations
""" """
# Récupération de l'évaluation # Récupération de l'évaluation
try: try:
@ -384,6 +415,7 @@ def assiduites_group(with_query: bool = False):
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 PARAMS
----- -----
@ -395,6 +427,11 @@ def assiduites_group(with_query: bool = False):
etat:etat de l'étudiant &rightarrow; absent, present ou retard etat:etat de l'étudiant &rightarrow; absent, present ou retard
etudids:liste des ids des étudiants concernés par la recherche etudids:liste des ids des étudiants concernés par la recherche
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité 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;
""" """
@ -474,6 +511,13 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
date_fin:date de fin de l'assiduité (infé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 etat:etat de l'étudiant &rightarrow; absent, present ou retard
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité 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
@ -549,6 +593,13 @@ def assiduites_formsemestre_count(
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
metric: la/les métriques de comptage (journee, demi, heure, compte) metric: la/les métriques de comptage (journee, demi, heure, compte)
split: divise le comptage par état 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
@ -621,6 +672,11 @@ 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
etud: Identite = tools.get_etud(etudid, nip, ine) etud: Identite = tools.get_etud(etudid, nip, ine)
@ -696,6 +752,11 @@ def assiduites_create():
] ]
``` ```
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""}]
""" """
create_list: list[object] = request.get_json(force=True) create_list: list[object] = request.get_json(force=True)
@ -874,6 +935,10 @@ def assiduite_delete():
] ]
``` ```
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
assiduites_list: list[int] = request.get_json(force=True) assiduites_list: list[int] = request.get_json(force=True)
@ -958,6 +1023,13 @@ def assiduite_edit(assiduite_id: int):
"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
@ -1013,6 +1085,12 @@ def assiduites_edit():
} }
] ]
``` ```
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

@ -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;
""" """
@ -112,6 +119,12 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
order:retourne les justificatifs dans l'ordre décroissant (non vide = True) 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) courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
group_id:<int:group_id> 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)
@ -174,6 +187,11 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
order:retourne les justificatifs dans l'ordre décroissant (non vide = True) 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) courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
group_id:<int:group_id> 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
@ -267,6 +285,11 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
order:retourne les justificatifs dans l'ordre décroissant (non vide = True) 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) courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
group_id:<int:group_id> group_id:<int:group_id>
SAMPLES
-------
/justificatifs/formsemestre/1;
""" """
# Récupération du formsemestre # Récupération du formsemestre
@ -334,6 +357,9 @@ 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""}]
""" """
@ -475,6 +501,12 @@ def justif_edit(justif_id: int):
"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
@ -589,6 +621,11 @@ def justif_delete():
... ...
] ]
``` ```
SAMPLES
-------
/justificatif/delete;[2, 2, 3]
""" """
# Récupération des justif_ids # Récupération des justif_ids
@ -834,6 +871,11 @@ def justif_remove(justif_id: int = None):
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é
@ -876,6 +918,11 @@ def justif_list(justif_id: int = None):
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

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

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

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

@ -4,12 +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 sys
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 from app.auth.models import Permission
from flask import render_template
class COLORS: class COLORS:
@ -136,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)
@ -270,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
@ -503,9 +512,7 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple:
child.method = method child.method = method
current_token.add_child(child) current_token.add_child(child)
# Gestion de doctable href = func_name
doctable = parse_doctable_doc(func.__doc__ or "")
href = func_name.replace("_", "-")
if child.query and not href.endswith("-query"): if child.query and not href.endswith("-query"):
href += "-query" href += "-query"
@ -527,14 +534,15 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple:
if func_name not in doctable_lines: if func_name not in doctable_lines:
doctable_lines[func_name] = { doctable_lines[func_name] = {
"doctable": doctable,
"method": method, "method": method,
"nom": func_name, "nom": func_name,
"href": href, "href": href,
"query": doc_dict.get("QUERY", "") != "",
"permission": permissions, "permission": permissions,
"description": doc_dict.get("", ""), "description": doc_dict.get("", ""),
"params": doc_dict.get("PARAMS", ""), "params": doc_dict.get("PARAMS", ""),
"category": doc_dict.get("CATEGORY", [False])[0] or category, "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
@ -547,15 +555,13 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple:
# point d'entrée de la commande `flask gen-api-map` # point d'entrée de la commande `flask gen-api-map`
def gen_api_map(app, endpoint_start="api.") -> str: 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 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 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 puis génère un fichier SVG à partir de cet arbre
""" """
api_map, doctable_lines = analyze_api_routes(app, endpoint_start)
# 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")
print( print(
@ -745,23 +751,6 @@ def _get_doc_lines(keyword, doc_string: str) -> list[str]:
return kw_lines return kw_lines
def parse_doc_name(doc_string: 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_string)
return name_lines[0] if name_lines else None
def parse_query_doc(doc_string: str) -> dict[str, str]: def parse_query_doc(doc_string: str) -> dict[str, str]:
""" """
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>})
@ -795,45 +784,28 @@ def parse_query_doc(doc_string: str) -> dict[str, str]:
return query return query
def parse_doctable_doc(doc_string: str) -> dict[str, str]: def _gen_table_line(doctable: dict = None):
"""
Retourne un dictionnaire représentant les informations du tableau d'api
à partir de la doc (DOC-TABLE)
éléments optionnels:
- `permissions` permissions nécessaires pour accéder à la route (ScoView, AbsChange, ...)
- `href` nom (sans #) de l'ancre dans la page ScoDoc9API
DOC-TABLE
---------
permissions: ScoView
href: une-fonction
"""
doc_lines: list[str] = _get_doc_lines("DOC-TABLE", doc_string)
table = {}
# on parcourt les lignes de la doc
for line in doc_lines:
# On sépare le paramètre et sa valeur
param, value = line.split(":")
# On met à jour le dictionnaire
table[param.strip()] = value.strip()
return table
def _gen_table_line(
nom="", href="", method="", permission="", doctable: dict = None, **kwargs
):
""" """
Génère une ligne de tableau markdown Génère une ligne de tableau markdown
| nom de la route| methode HTTP| Permission | | nom de la route| methode HTTP| Permission |
""" """
lien: str = href
if "href" in doctable: nom, method, permission = (
lien: str = doctable.get("href") doctable.get("nom", ""),
doctable.get("method", ""),
doctable.get("permission", ""),
)
if doctable is None:
doctable = {}
lien: str = doctable.get("href", nom)
doctable["query"]: bool
if doctable.get("query") and not lien.endswith("-query"):
lien += "-query"
nav: str = f"[{nom}]({'#'+lien})" nav: str = f"[{nom}]({'#'+lien})"
table: str = "|" table: str = "|"
@ -858,19 +830,58 @@ def _gen_table(lines: list[dict]) -> str:
""" """
Génère un tableau markdown à partir d'une liste de lignes Génère un tableau markdown à partir d'une liste de lignes
lines : liste de dictionnaire au format : lines : liste de dictionnaire au format doc_lines.
- doctable : dict généré par parse_doctable_doc
- nom : nom de la fonction associée à la route
- method : GET ou POST
- permission : Permissions de la route (auto récupérée)
""" """
table = _gen_table_head() table = _gen_table_head()
table += "\n".join([_gen_table_line(**line) for line in lines]) table += "\n".join([_gen_table_line(line) for line in lines])
return table 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:
url, content = sample.split(";", maxsplit=1)
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"): def _write_gen_table(table: str, filename: str = "/tmp/api_table.md"):
"""Ecriture du fichier md avec la table""" """Ecriture du fichier md avec la table"""
with open(filename, "w", encoding="UTF-8") as f: with open(filename, "w", encoding="UTF-8") as f:
@ -886,6 +897,16 @@ def doc_route(doctable: dict) -> str:
jinja_obj.update(doctable) jinja_obj.update(doctable)
jinja_obj["nom"] = doctable["nom"].strip() # on retire les caractères blancs 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"): if doctable.get("params"):
jinja_obj["params"] = [] jinja_obj["params"] = []
for param in doctable["params"]: for param in doctable["params"]:
@ -901,43 +922,41 @@ def doc_route(doctable: dict) -> str:
descr = "\n".join(s for s in doctable["description"]) descr = "\n".join(s for s in doctable["description"])
jinja_obj["description"] = descr.strip() jinja_obj["description"] = descr.strip()
jinja_obj["sample"] = {
"nom": f"{jinja_obj['nom']}.json",
"href": f"{jinja_obj['nom'].replace('_', '-')}.json.md",
}
return render_template("doc/apidoc.j2", doc=jinja_obj) return render_template("doc/apidoc.j2", doc=jinja_obj)
def gen_api_doc(app, endpoint_start="api."): def gen_api_doc(app, endpoint_start="api."):
"commande gen-api-doc" "commande gen-api-doc"
_, doctable_lines = analyze_api_routes(app, endpoint_start) api_map, doctable_lines = analyze_api_routes(app, endpoint_start)
mddoc: str = "" mddoc: str = ""
categories: dict = {} categories: dict = {}
for value in doctable_lines.values(): for value in doctable_lines.values():
category = value["category"] category = value["category"].capitalize()
if category not in categories: if category not in categories:
categories[category] = [] categories[category] = []
categories[category].append(value) categories[category].append(value)
# sort categories by name # sort categories by name
categories: dict = dict(sorted(categories.items(), key=lambda x: x[0].capitalize())) categories: dict = dict(
sorted(categories.items(), key=lambda x: strip_accents(x[0]))
)
category: str category: str
routes: list[dict] routes: list[dict]
for category, routes in categories.items(): for category, routes in categories.items():
# sort routes by name # sort routes by name
routes.sort(key=lambda x: x["nom"]) routes.sort(key=lambda x: strip_accents(x["nom"]))
mddoc += f"### API {category.capitalize()}\n\n" mddoc += f"### API {category.capitalize()}\n\n"
for route in routes: for route in routes:
mddoc += doc_route(route) mddoc += doc_route(route)
mddoc += "\n\n" mddoc += "\n\n"
table_api = gen_api_map(app, endpoint_start=endpoint_start) table_api = gen_api_map(api_map, doctable_lines)
mdpage = render_template("doc/ScoDoc9API.j2", doc_api=mddoc, table_api=table_api) mdpage = render_template("doc/ScoDoc9API.j2", doc_api=mddoc, table_api=table_api)
_gen_csv(list(doctable_lines.values()))
fname = "/tmp/ScoDoc9API.md" fname = "/tmp/ScoDoc9API.md"
with open(fname, "w", encoding="utf-8") as f: with open(fname, "w", encoding="utf-8") as f: