Merge pull request 'Génération de la documentation API' (#969) from iziram/ScoDoc:master into master

Reviewed-on: ScoDoc/ScoDoc#969
This commit is contained in:
Emmanuel Viennet 2024-07-25 08:44:38 +02:00
commit b3111769a1
4 changed files with 232 additions and 211 deletions

View File

@ -58,9 +58,9 @@ def assiduite(assiduite_id: int = None):
"date_fin": "2022-10-31T10:00+01:00",
"etat": "retard",
"desc": "une description",
"user_id: 1 or null,
"user_name" : login scodoc or null
"user_nom_complet": "Marie Dupont"
"user_id": 1 or null,
"user_name" : login scodoc or null,
"user_nom_complet": "Marie Dupont",
"est_just": False or True,
}
```
@ -132,42 +132,6 @@ def assiduites_count(
"""
Retourne le nombre d'assiduités d'un étudiant.
Un filtrage peut être donné avec une `query`.
Les différents filtres :
- Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
ex: .../query?type=heure
Comportement par défaut : compte le nombre d'assiduité enregistrée
- Etat (etat de l'étudiant -> absent, present ou retard):
`query?etat=[- liste des états séparé par une virgule -]`
ex: .../query?etat=present,retard
- Date debut
(date de début de l'assiduité, sont affichés les assiduités
dont la date de début est supérieur ou égale à la valeur donnée):
query?date_debut=[- date au format iso -]
ex: query?date_debut=2022-11-03T08:00+01:00
Date fin
(date de fin de l'assiduité, sont affichés les assiduités
dont la date de fin est inférieure ou égale à la valeur donnée):
query?date_fin=[- date au format iso -]
ex: query?date_fin=2022-11-03T10:00+01:00
Moduleimpl_id (l'id du module concerné par l'assiduité):
query?moduleimpl_id=[- int ou vide -]
ex: query?moduleimpl_id=1234
query?moduleimpl_od=
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
query?formsemestre_id=[int]
ex query?formsemestre_id=3
user_id (l'id de l'auteur de l'assiduité)
query?user_id=[int]
ex query?user_id=3
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
query?est_just=[bool]
query?est_just=f
query?est_just=t
QUERY
-----
user_id:<int:user_id>
@ -180,6 +144,18 @@ def assiduites_count(
metric:<array[string]:metric>
split:<bool:split>
PARAMS
-----
user_id:l'id de l'auteur de l'assiduité
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
moduleimpl_id:l'id du module concerné par l'assiduité
date_debut:date de début de l'assiduité (supérieur ou égal)
date_fin:date de fin de l'assiduité (inférieur ou égal)
etat:etat de l'étudiant &rightarrow; absent, present ou retard
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
metric: la/les métriques de comptage (journee, demi, heure, compte)
split: divise le comptage par état
"""
# Récupération de l'étudiant
@ -235,39 +211,6 @@ def assiduites_count(
def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False):
"""
Retourne toutes les assiduités d'un étudiant
chemin : /assiduites/<int:etudid>
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
-----
@ -279,6 +222,16 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False)
etat:<array[string]:etat>
formsemestre_id:<int:formsemestre_id>
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é
"""
# Récupération de l'étudiant
@ -333,7 +286,9 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
Pour chaque évaluation, retourne la liste des objets assiduités
sur la plage de l'évaluation
Présentation du retour :
Exemple de résultat:
```json
[
{
"evaluation_id": 1234,
@ -345,6 +300,7 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
]
}
]
```
"""
# Récupération de l'étudiant
@ -372,7 +328,10 @@ def assiduites_evaluations(etudid: int = None, nip=None, ine=None):
def evaluation_assiduites(evaluation_id):
"""
Retourne les objets assiduités de chaque étudiant sur la plage de l'évaluation
Présentation du retour :
Exemple de résultat:
```json
{
"<etudid>" : [
{
@ -381,6 +340,11 @@ def evaluation_assiduites(evaluation_id):
},
]
}
```
CATEGORY
--------
evaluations
"""
# Récupération de l'évaluation
try:
@ -409,37 +373,6 @@ def assiduites_group(with_query: bool = False):
Retourne toutes les assiduités d'un groupe d'étudiants
chemin : /assiduites/group/query?etudids=1,2,3
Un filtrage peut être donné avec une query
chemin : /assiduites/group/query?etudids=1,2,3
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
-----
@ -449,9 +382,20 @@ def assiduites_group(with_query: bool = False):
date_debut:<string:date_debut_iso>
date_fin:<string:date_fin_iso>
etat:<array[string]:etat>
etudids:<array[int]:etudids
etudids:<array[int]:etudids>
formsemestre_id:<int:formsemestre_id>
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é
"""
# Récupération des étudiants dans la requête
@ -511,6 +455,7 @@ def assiduites_group(with_query: bool = False):
@permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne toutes les assiduités du formsemestre
QUERY
-----
user_id:<int:user_id>
@ -519,6 +464,16 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
date_debut:<string:date_debut_iso>
date_fin:<string:date_fin_iso>
etat:<array[string]:etat>
PARAMS
-----
user_id:l'id de l'auteur de l'assiduité
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
moduleimpl_id:l'id du module concerné par l'assiduité
date_debut:date de début de l'assiduité (supérieur ou égal)
date_fin:date de fin de l'assiduité (inférieur ou égal)
etat:etat de l'étudiant &rightarrow; absent, present ou retard
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
"""
# Récupération du formsemestre à partir du formsemestre_id
@ -582,6 +537,18 @@ def assiduites_formsemestre_count(
formsemestre_id:<int:formsemestre_id>
metric:<array[string]:metric>
split:<bool:split>
PARAMS
-----
user_id:l'id de l'auteur de l'assiduité
est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard)
moduleimpl_id:l'id du module concerné par l'assiduité
date_debut:date de début de l'assiduité (supérieur ou égal)
date_fin:date de fin de l'assiduité (inférieur ou égal)
etat:etat de l'étudiant &rightarrow; absent, present ou retard
formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité
metric: la/les métriques de comptage (journee, demi, heure, compte)
split: divise le comptage par état
"""
# Récupération du formsemestre à partir du formsemestre_id
@ -633,7 +600,10 @@ def assiduites_formsemestre_count(
def assiduite_create(etudid: int = None, nip=None, ine=None):
"""
Enregistrement d'assiduités pour un étudiant (etudid)
La requête doit avoir un content type "application/json":
DATA
----
```json
[
{
"date_debut": str,
@ -649,6 +619,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
}
...
]
```
"""
# Récupération de l'étudiant
@ -702,7 +673,10 @@ def assiduite_create(etudid: int = None, nip=None, ine=None):
def assiduites_create():
"""
Création d'une assiduité ou plusieurs assiduites
La requête doit avoir un content type "application/json":
DATA
----
```json
[
{
"date_debut": str,
@ -715,12 +689,12 @@ def assiduites_create():
"date_fin": str,
"etat": str,
"etudid":int,
"moduleimpl_id": int,
"desc":str,
}
...
]
```
"""
@ -891,13 +865,14 @@ def assiduite_delete():
"""
Suppression d'une assiduité à partir de son id
Forme des données envoyées :
DATA
----
```json
[
<assiduite_id:int>,
...
]
```
"""
# Récupération des ids envoyés dans la liste
@ -972,13 +947,17 @@ def _delete_one(assiduite_id: int) -> tuple[int, str]:
def assiduite_edit(assiduite_id: int):
"""
Edition d'une assiduité à partir de son id
La requête doit avoir un content type "application/json":
DATA
----
```json
{
"etat"?: str,
"moduleimpl_id"?: int
"desc"?: str
"est_just"?: bool
}
```
"""
# Récupération de l'assiduité à modifier
@ -1020,7 +999,10 @@ def assiduite_edit(assiduite_id: int):
def assiduites_edit():
"""
Edition de plusieurs assiduités
La requête doit avoir un content type "application/json":
DATA
----
```json
[
{
"assiduite_id" : int,
@ -1030,6 +1012,7 @@ def assiduites_edit():
"est_just"?: bool
}
]
```
"""
edit_list: list[object] = request.get_json(force=True)

View File

@ -92,38 +92,26 @@ def justificatif(justif_id: int = None):
def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = False):
"""
Retourne toutes les assiduités d'un étudiant
chemin : /justificatifs/<int:etudid>
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
-----
user_id:<int:user_id>
est_just:<bool:est_just>
date_debut:<string:date_debut_iso>
date_fin:<string:date_fin_iso>
etat:<array[string]:etat>
order:<bool:order>
courant:<bool:courant>
group_id:<int:group_id>
PARAMS
-----
user_id:l'id de l'auteur du justificatif
date_debut:date de début du justificatif (supérieur ou égal)
date_fin:date de fin du justificatif (inférieur ou égal)
etat:etat du justificatif &rightarrow; valide, non_valide, attente, modifie
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
group_id:<int:group_id>
"""
# Récupération de l'étudiant
etud: Identite = tools.get_etud(etudid, nip, ine)
@ -176,6 +164,16 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
order:<bool:order>
courant:<bool:courant>
group_id:<int:group_id>
PARAMS
-----
user_id:l'id de l'auteur du justificatif
date_debut:date de début du justificatif (supérieur ou égal)
date_fin:date de fin du justificatif (inférieur ou égal)
etat:etat du justificatif &rightarrow; valide, non_valide, attente, modifie
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
group_id:<int:group_id>
"""
# Récupération du département et des étudiants du département
@ -259,6 +257,16 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
order:<bool:order>
courant:<bool:courant>
group_id:<int:group_id>
PARAMS
-----
user_id:l'id de l'auteur du justificatif
date_debut:date de début du justificatif (supérieur ou égal)
date_fin:date de fin du justificatif (inférieur ou égal)
etat:etat du justificatif &rightarrow; valide, non_valide, attente, modifie
order:retourne les justificatifs dans l'ordre décroissant (non vide = True)
courant:retourne les justificatifs de l'année courante (bool : v/t ou f)
group_id:<int:group_id>
"""
# Récupération du formsemestre
@ -307,7 +315,10 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
def justif_create(etudid: int = None, nip=None, ine=None):
"""
Création d'un justificatif pour l'étudiant (etudid)
La requête doit avoir un content type "application/json":
DATA
----
```json
[
{
"date_debut": str,
@ -322,6 +333,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
}
...
]
```
"""
@ -452,14 +464,17 @@ def _create_one(
def justif_edit(justif_id: int):
"""
Edition d'un justificatif à partir de son id
La requête doit avoir un content type "application/json":
DATA
----
```json
{
"etat"?: str,
"raison"?: str
"date_debut"?: str
"date_fin"?: str
}
```
"""
# Récupération du justificatif à modifier
@ -566,14 +581,14 @@ def justif_delete():
"""
Suppression d'un justificatif à partir de son id
Forme des données envoyées :
DATA
----
```json
[
<justif_id:int>,
...
]
```
"""
# Récupération des justif_ids
@ -648,6 +663,8 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
def justif_import(justif_id: int = None):
"""
Importation d'un fichier (création d'archive)
> Procédure d'importation de fichier : [importer un justificatif](FichiersJustificatifs.md#importer-un-fichier)
"""
# On vérifie qu'un fichier a bien été envoyé
@ -699,6 +716,8 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
"""
Retourne un fichier d'une archive d'un justificatif.
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
> Procédure de téléchargement de fichier : [télécharger un justificatif](FichiersJustificatifs.md#télécharger-un-fichier)
"""
# On récupère le justificatif concerné
justificatif_unique = Justificatif.get_justificatif(justif_id)
@ -736,14 +755,20 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
def justif_remove(justif_id: int = None):
"""
Supression d'un fichier ou d'une archive
{
"remove": <"all"/"list">
> Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier)
DATA
----
```json
{
"remove": <"all"/"list">,
"filenames"?: [
<filename:str>,
...
]
}
```
"""
# On récupère le dictionnaire

31
app/templates/apidoc.j2 Normal file
View File

@ -0,0 +1,31 @@
#### **`{{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

@ -4,10 +4,12 @@ Script permettant de générer une carte SVG de l'API de ScoDoc
Écrit par Matthias HARTMANN
"""
import sys
import xml.etree.ElementTree as ET
import re
from app.auth.models import Permission
from flask import render_template
class COLORS:
@ -507,6 +509,14 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple:
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(
@ -524,6 +534,7 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple:
"permission": permissions,
"description": doc_dict.get("", ""),
"params": doc_dict.get("PARAMS", ""),
"category": doc_dict.get("CATEGORY", [False])[0] or category,
}
# On met à jour le token courant pour le prochain segment
@ -543,8 +554,6 @@ def gen_api_map(app, endpoint_start="api."):
puis génère un fichier SVG à partir de cet arbre
"""
print("DEBUG", app.view_functions["apiweb.user_info"].scodoc_permission)
api_map, doctable_lines = analyze_api_routes(app, endpoint_start)
# On génère le SVG à partir de l'arbre de Token
@ -870,88 +879,61 @@ def _write_gen_table(table: str, filename: str = "/tmp/api_table.md"):
)
if __name__ == "__main__":
# Exemple d'utilisation de la classe Token
# Exemple simple de création d'un arbre de Token
# root = Token("api")
# child1 = Token("assiduites", leaf=True)
# child1.func_name = "assiduites_get"
# child2 = Token("count")
# child22 = Token("all")
# child23 = Token(
# "query",
# 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)
# child1.add_child(child2)
# child2.add_child(child22)
# child2.add_child(child23)
# root.add_child(child3)
# group_element = root.to_svg_group()
# generate_svg(group_element, "/tmp/api_map.svg")
dt: dict = parse_doctable_doc(parse_doctable_doc.__doc__)
md: str = "POST"
hf: str = "assiduites-query"
doc: dict = {
"doctable": dt,
"method": md,
"href": hf,
}
print(_gen_table([doc]))
def doc_route(doctable: dict) -> str:
"""Generate markdown doc for a route"""
doc = f"""
#### **`{doctable['nom']}`**
jinja_obj: dict = {}
jinja_obj.update(doctable)
jinja_obj["nom"] = doctable["nom"].strip() # on retire les caractères blancs
"""
if doctable.get("routes"):
if len(doctable["routes"]) == 1:
doc += f"""* ** Route :** `{doctable["routes"][0]}`\n"""
else:
doc += "* ** Routes :**\n"
for route in doctable["routes"]:
doc += f""" * `{route}`\n"""
doc += f"""* **Méthode: {doctable['method']}**
* **Permission: `{doctable.get('permission', '')}`**
"""
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}")
name, descr = param, ""
doc += f""" * `{name}`: {descr}\n"""
if doctable.get("data"):
doc += f"""* **Data:** {doctable['data']}\n"""
if doctable.get("description"):
descr = "\n".join(s for s in doctable["description"])
doc += f"""* **Description:** {descr}\n"""
return doc
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("apidoc.j2", doc=jinja_obj)
def gen_api_doc(app, endpoint_start="api."):
"commande gen-api-doc"
_, doctable_lines = analyze_api_routes(app, endpoint_start)
mddoc = "\n".join(
doc_route(doctable)
for doctable in sorted(doctable_lines.values(), key=lambda x: x["nom"])
)
mddoc: str = ""
categories: dict = {}
for value in doctable_lines.values():
category = value["category"]
if category not in categories:
categories[category] = []
categories[category].append(value)
# sort categories by name
categories: dict = dict(sorted(categories.items(), key=lambda x: x[0]))
category: str
routes: list[dict]
for category, routes in categories.items():
# sort routes by name
routes.sort(key=lambda x: x["nom"])
mddoc += f"### API {category.capitalize()}\n\n"
for route in routes:
mddoc += doc_route(route)
mddoc += "\n\n"
fname = "/tmp/apidoc.md"
with open(fname, "w", encoding="utf-8") as f: