From 9ca86e7900be224d75aa3029a39c357d350fa3f1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 23 Jul 2024 07:07:29 +0200 Subject: [PATCH 01/16] =?UTF-8?q?G=C3=A9n=C3=A9ration=20tableau=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/assiduites.py | 3 +- tools/create_api_map.py | 183 ++++++++++++++++++++++++++++++++++------ 2 files changed, 160 insertions(+), 26 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 8bf50805e..6b75b0998 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -19,7 +19,8 @@ import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app.api import api_bp as bp from app.api import api_web_bp, get_model_api_object, tools -from app.decorators import permission_required, scodoc +from app.api import api_permission_required as permission_required +from app.decorators import scodoc from app.models import ( Assiduite, Evaluation, diff --git a/tools/create_api_map.py b/tools/create_api_map.py index 2db5996ab..fd1fcdcc1 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -7,6 +7,8 @@ Script permettant de générer une carte SVG de l'API de ScoDoc import xml.etree.ElementTree as ET import re +from app.auth.models import Permission + class COLORS: """ @@ -437,15 +439,20 @@ def _create_question_mark_group(coords, href): return group +# point d'entrée de la commande `flask gen-api-map` def gen_api_map(app, endpoint_start="api"): """ 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 """ + + print("DEBUG", app.view_functions["apiweb.user_info"].scodoc_permission) # Création du token racine api_map = Token("") + doctable_lines: dict[str, dict] = {} + # Parcours de toutes les routes de l'application for rule in app.url_map.iter_rules(): # On ne garde que les routes de l'API / APIWEB @@ -500,6 +507,28 @@ def gen_api_map(app, endpoint_start="api"): child.method = method current_token.add_child(child) + # Gestion de doctable + doctable = parse_doctable_doc(func.__doc__ or "") + href = func_name.replace("_", "-") + if child.query and not href.endswith("-query"): + href += "-query" + + permissions: str + try: + permissions: str = ", ".join( + sorted(Permission.permissions_names(func.scodoc_permission)) + ) + except AttributeError: + permissions = "Aucune permission requise" + + doctable_lines[func_name] = { + "doctable": doctable, + "method": method, + "nom": func_name, + "href": href, + "permission": permissions, + } + # On met à jour le token courant pour le prochain segment current_token = child @@ -510,6 +539,9 @@ def gen_api_map(app, endpoint_start="api"): + "Vous pouvez la consulter à l'adresse suivante : /tmp/api_map.svg" ) + # On génère le tableau à partir de doctable_lines + _gen_table(sorted(doctable_lines.values(), key=lambda x: x["nom"])) + def _get_bbox(element, x_offset=0, y_offset=0): """ @@ -617,6 +649,7 @@ def generate_svg(element, fname): def _get_doc_lines(keyword, doc) -> list[str]: """ 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: @@ -638,10 +671,18 @@ def _get_doc_lines(keyword, doc) -> list[str]: return [] # On récupère les lignes de la doc qui correspondent au keyword (enfin on espère) 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 -def parse_doc_name(doc): +def parse_doc_name(doc) -> str: """ renvoie le nom de la route à partir de la docstring @@ -658,7 +699,7 @@ def parse_doc_name(doc): return name_lines[0] if name_lines else None -def parse_query_doc(doc): +def parse_query_doc(doc) -> dict[str, str]: """ renvoie un dictionnaire {param: } (ex: {assiduite_id : }) @@ -691,33 +732,125 @@ def parse_query_doc(doc): return query +def parse_doctable_doc(doc) -> dict[str, str]: + """ + 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) + + # On crée un dictionnaire + 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): + """ + Génère une ligne de tableau markdown + + | nom de la route| methode HTTP| Permission | + """ + lien: str = href + if "href" in doctable: + lien: str = doctable.get("href") + 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], filename: str = "/tmp/api_table.md") -> str: + """ + Génère un tableau markdown à partir d'une liste de lignes + + lines : liste de dictionnaire au format : + + - 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 += "\n".join([_gen_table_line(**line) for line in lines]) + + 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}" + ) + + 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": "", - "moduleimpl_id": "", - "count": "", - "formsemestre_id": "", - }, - ) - child3 = Token("justificatifs", "POST") - child3.func_name = "justificatifs_post" + # root = Token("api") + # child1 = Token("assiduites", leaf=True) + # child1.func_name = "assiduites_get" + # child2 = Token("count") + # child22 = Token("all") + # child23 = Token( + # "query", + # query={ + # "etat": "", + # "moduleimpl_id": "", + # "count": "", + # "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) + # 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() + # group_element = root.to_svg_group() - generate_svg(group_element, "/tmp/api_map.svg") + # 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])) From 71639606faed3322661910e4ce7608d4bbc58562 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 23 Jul 2024 16:49:11 +0200 Subject: [PATCH 02/16] =?UTF-8?q?G=C3=A9n=C3=A9ration=20doc=20API=20(WIP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/assiduites.py | 22 ++--- app/api/etudiants.py | 8 +- app/api/formsemestres.py | 2 + app/models/assiduites.py | 4 +- scodoc.py | 15 +++- tools/__init__.py | 2 +- tools/create_api_map.py | 170 +++++++++++++++++++++++++++++++-------- 7 files changed, 176 insertions(+), 47 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 6b75b0998..f7be35949 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -48,6 +48,8 @@ def assiduite(assiduite_id: int = None): """Retourne un objet assiduité à partir de son id Exemple de résultat: + + ```json { "assiduite_id": 1, "etudid": 2, @@ -61,6 +63,7 @@ def assiduite(assiduite_id: int = None): "user_nom_complet": "Marie Dupont" "est_just": False or True, } + ``` """ return get_model_api_object(Assiduite, assiduite_id, Identite) @@ -78,15 +81,18 @@ def assiduite(assiduite_id: int = None): @permission_required(Permission.ScoView) @as_json def assiduite_justificatifs(assiduite_id: int = None, long: bool = False): - """Retourne la liste des justificatifs qui justifie cette assiduitée + """Retourne la liste des justificatifs qui justifient cette assiduité. Exemple de résultat: + + ```json [ 1, 2, 3, ... ] + ``` """ return get_assiduites_justif(assiduite_id, long) @@ -124,22 +130,20 @@ def assiduites_count( etudid: int = None, nip: str = None, ine: str = None, with_query: bool = False ): """ - Retourne le nombre d'assiduités d'un étudiant - chemin : /assiduites//count + Retourne le nombre d'assiduités d'un étudiant. - Un filtrage peut être donné avec une query - chemin : /assiduites//count/query? + Un filtrage peut être donné avec une `query`. Les différents filtres : - Type (type de comptage -> journee, demi, heure, nombre d'assiduite): + - 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 -] + - 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 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 -] diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 829b06e96..8b4db4d3b 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -414,13 +414,14 @@ def bulletin( """ Retourne le bulletin d'un étudiant dans un formsemestre. + PARAMS + ------ formsemestre_id : l'id d'un formsemestre code_type : "etudid", "nip" ou "ine" code : valeur du code INE, NIP ou etudid, selon code_type. version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt pdf : si spécifié, bulletin au format PDF (et non JSON). - Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin """ if version == "pdf": version = "long" @@ -599,7 +600,10 @@ def etudiant_edit( code_type: str = "etudid", code: str = None, ): - """Edition des données étudiant (identité, admission, adresses)""" + """Édition des données étudiant (identité, admission, adresses). + + `code_type`: `etudid`, `ine` ou `nip`. + """ ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) if not ok: return etud # json error diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 641a70df8..6feba7d68 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -104,6 +104,8 @@ def formsemestres_query(): Retourne les formsemestres filtrés par étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant + PARAMS + ------ etape_apo : un code étape apogée annee_scolaire : année de début de l'année scolaire dept_acronym : acronyme du département (eg "RT") diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 297a4014a..679a770c3 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -348,7 +348,7 @@ class Assiduite(ScoDocModel): """ Retourne le module associé à l'assiduité Si traduire est vrai, retourne le titre du module précédé du code - Sinon rentourne l'objet Module ou None + Sinon retourne l'objet Module ou None """ if self.moduleimpl_id is not None: @@ -358,7 +358,7 @@ class Assiduite(ScoDocModel): return f"{mod.code} {mod.titre}" 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 ( "Autre module (pas dans la liste)" if self.external_data["module"] == "Autre" diff --git a/scodoc.py b/scodoc.py index ebe83a931..8c198e236 100755 --- a/scodoc.py +++ b/scodoc.py @@ -748,6 +748,19 @@ def generate_ens_calendars(): # generate-ens-calendars help="Endpoint à partir duquel générer la carte des routes", ) @with_appcontext -def gen_api_map(endpoint): +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() +@click.option( + "-e", + "--endpoint", + default="api.", + help="Endpoint à partir duquel générer la documentation des routes", +) +@with_appcontext +def gen_api_doc(endpoint): # gen-api-map + """Génère la documentation des routes de l'API.""" + tools.gen_api_doc(app, endpoint_start=endpoint) diff --git a/tools/__init__.py b/tools/__init__.py index c7c13e6bd..2b8d11bbb 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -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_abs_to_assiduites import migrate_abs_to_assiduites 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 diff --git a/tools/create_api_map.py b/tools/create_api_map.py index fd1fcdcc1..771f34f19 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -439,21 +439,15 @@ def _create_question_mark_group(coords, href): return group -# point d'entrée de la commande `flask gen-api-map` -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 + analyse docstrings """ - 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 - """ - - print("DEBUG", app.view_functions["apiweb.user_info"].scodoc_permission) # Création du token racine api_map = Token("") doctable_lines: dict[str, dict] = {} - # Parcours de toutes les routes de l'application for rule in app.url_map.iter_rules(): # On ne garde que les routes de l'API / APIWEB if not rule.endpoint.lower().startswith(endpoint_start.lower()): @@ -468,8 +462,8 @@ def gen_api_map(app, endpoint_start="api"): # Récupération de la fonction associée à la route 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 for i, segment in enumerate(segments): # On cherche si le segment est déjà un enfant du token courant @@ -521,16 +515,37 @@ def gen_api_map(app, endpoint_start="api"): except AttributeError: permissions = "Aucune permission requise" - doctable_lines[func_name] = { - "doctable": doctable, - "method": method, - "nom": func_name, - "href": href, - "permission": permissions, - } + if func_name not in doctable_lines: + doctable_lines[func_name] = { + "doctable": doctable, + "method": method, + "nom": func_name, + "href": href, + "permission": permissions, + "description": doc_dict.get("", ""), + "params": doc_dict.get("PARAMS", ""), + } # On met à jour le token courant pour le prochain segment 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(app, endpoint_start="api."): + """ + 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 + """ + + 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 generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg") @@ -540,7 +555,8 @@ def gen_api_map(app, endpoint_start="api"): ) # On génère le tableau à partir de doctable_lines - _gen_table(sorted(doctable_lines.values(), key=lambda x: x["nom"])) + table = _gen_table(sorted(doctable_lines.values(), key=lambda x: x["nom"])) + _write_gen_table(table) def _get_bbox(element, x_offset=0, y_offset=0): @@ -646,7 +662,44 @@ def generate_svg(element, fname): 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[A-Z_\-]+)$\n^\s*-+\n(?P(^(?!\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 Attention : s'arrête à la première ligne vide @@ -659,7 +712,7 @@ def _get_doc_lines(keyword, doc) -> list[str]: """ # 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 "-----" # Si ce n'est pas le cas, on renvoie un dictionnaire vide try: @@ -682,7 +735,7 @@ def _get_doc_lines(keyword, doc) -> list[str]: return kw_lines -def parse_doc_name(doc) -> str: +def parse_doc_name(doc_string: str) -> str: """ renvoie le nom de la route à partir de la docstring @@ -695,11 +748,11 @@ def parse_doc_name(doc) -> str: Il ne peut y avoir qu'une seule ligne suivant ----- """ - name_lines: list[str] = _get_doc_lines("DOC_ANCHOR", doc) + 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) -> dict[str, str]: +def parse_query_doc(doc_string: str) -> dict[str, str]: """ renvoie un dictionnaire {param: } (ex: {assiduite_id : }) @@ -714,7 +767,7 @@ def parse_query_doc(doc) -> dict[str, str]: 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 """ - query_lines: list[str] = _get_doc_lines("QUERY", doc) + query_lines: list[str] = _get_doc_lines("QUERY", doc_string) query = {} regex = re.compile(r"^(\w+):(<.+>)$") @@ -732,7 +785,7 @@ def parse_query_doc(doc) -> dict[str, str]: return query -def parse_doctable_doc(doc) -> dict[str, str]: +def parse_doctable_doc(doc_string: str) -> dict[str, str]: """ Retourne un dictionnaire représentant les informations du tableau d'api à partir de la doc (DOC-TABLE) @@ -747,9 +800,7 @@ def parse_doctable_doc(doc) -> dict[str, str]: href: une-fonction """ - doc_lines: list[str] = _get_doc_lines("DOC-TABLE", doc) - - # On crée un dictionnaire + doc_lines: list[str] = _get_doc_lines("DOC-TABLE", doc_string) table = {} # on parcourt les lignes de la doc @@ -762,7 +813,9 @@ def parse_doctable_doc(doc) -> dict[str, str]: return table -def _gen_table_line(nom, href, method, permission, doctable: dict): +def _gen_table_line( + nom="", href="", method="", permission="", doctable: dict = None, **kwargs +): """ Génère une ligne de tableau markdown @@ -791,7 +844,7 @@ def _gen_table_head() -> str: return f"{headers}\n{line}\n" -def _gen_table(lines: list[dict], filename: str = "/tmp/api_table.md") -> str: +def _gen_table(lines: list[dict]) -> str: """ Génère un tableau markdown à partir d'une liste de lignes @@ -805,10 +858,13 @@ def _gen_table(lines: list[dict], filename: str = "/tmp/api_table.md") -> str: """ table = _gen_table_head() table += "\n".join([_gen_table_line(**line) for line in lines]) + return table + +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}" ) @@ -854,3 +910,53 @@ if __name__ == "__main__": "href": hf, } print(_gen_table([doc])) + + +def doc_route(doctable: dict) -> str: + """Generate markdown doc for a route""" + doc = f""" +#### **`{doctable['nom']}`** + +""" + 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"): + for param in doctable["params"]: + frags = param.split(":", maxsplit=1) + if len(frags) == 2: + name, descr = frags + 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 + + +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"]) + ) + + fname = "/tmp/apidoc.md" + with open(fname, "w", encoding="utf-8") as f: + f.write(mddoc) + print( + "La documentation API a été générée avec succès. " + f"Vous pouvez la consulter à l'adresse suivante : {fname}" + ) From 17233fb8c11850bc5762524bdb008fe687d0e68f Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 24 Jul 2024 10:38:56 +0200 Subject: [PATCH 03/16] APIDoc utilisation template jinja + lien samples --- app/templates/apidoc.j2 | 31 +++++++++++++++++ tools/create_api_map.py | 77 +++++++++-------------------------------- 2 files changed, 47 insertions(+), 61 deletions(-) create mode 100644 app/templates/apidoc.j2 diff --git a/app/templates/apidoc.j2 b/app/templates/apidoc.j2 new file mode 100644 index 000000000..36d923c94 --- /dev/null +++ b/app/templates/apidoc.j2 @@ -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 %} \ No newline at end of file diff --git a/tools/create_api_map.py b/tools/create_api_map.py index 771f34f19..56f9dd238 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET import re from app.auth.models import Permission +from flask import render_template class COLORS: @@ -870,79 +871,33 @@ 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": "", - # "moduleimpl_id": "", - # "count": "", - # "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."): From c12bc778bb4397eebdcb8a39fcce4a40f0502b93 Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 24 Jul 2024 11:07:57 +0200 Subject: [PATCH 04/16] Maj Markdown docstring assiduites + justificatifs --- app/api/assiduites.py | 207 ++++++++++++++++++--------------------- app/api/justificatifs.py | 85 ++++++++++------ 2 files changed, 148 insertions(+), 144 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index f7be35949..2c1c991cc 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -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: @@ -180,6 +144,18 @@ def assiduites_count( metric: split: + PARAMS + ----- + user_id:l'id de l'auteur de l'assiduité + est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) + moduleimpl_id:l'id du module concerné par l'assiduité + date_debut:date de début de l'assiduité (supérieur ou égal) + date_fin:date de fin de l'assiduité (inférieur ou égal) + etat:etat de l'étudiant → absent, present ou retard + formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité + metric: la/les métriques de comptage (journee, demi, heure, compte) + split: divise le comptage par état + """ # 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/ - - Un filtrage peut être donné avec une query - chemin : /assiduites//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: 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 → 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 { "" : [ { @@ -381,6 +340,7 @@ def evaluation_assiduites(evaluation_id): }, ] } + ``` """ # Récupération de l'évaluation try: @@ -409,37 +369,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 +378,20 @@ def assiduites_group(with_query: bool = False): date_debut: date_fin: etat: - etudids: 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 → 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 +451,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: @@ -519,6 +460,16 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False): date_debut: date_fin: etat: + + PARAMS + ----- + user_id:l'id de l'auteur de l'assiduité + est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) + moduleimpl_id:l'id du module concerné par l'assiduité + date_debut:date de début de l'assiduité (supérieur ou égal) + date_fin:date de fin de l'assiduité (inférieur ou égal) + etat:etat de l'étudiant → absent, present ou retard + formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité """ # Récupération du formsemestre à partir du formsemestre_id @@ -582,6 +533,18 @@ def assiduites_formsemestre_count( formsemestre_id: metric: split: + + PARAMS + ----- + user_id:l'id de l'auteur de l'assiduité + est_just:si l'assiduité est justifiée (fait aussi filtre abs/retard) + moduleimpl_id:l'id du module concerné par l'assiduité + date_debut:date de début de l'assiduité (supérieur ou égal) + date_fin:date de fin de l'assiduité (inférieur ou égal) + etat:etat de l'étudiant → absent, present ou retard + formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité + metric: la/les métriques de comptage (journee, demi, heure, compte) + split: divise le comptage par état """ # Récupération du formsemestre à partir du formsemestre_id @@ -633,7 +596,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 +615,7 @@ def assiduite_create(etudid: int = None, nip=None, ine=None): } ... ] + ``` """ # Récupération de l'étudiant @@ -702,7 +669,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 +685,12 @@ def assiduites_create(): "date_fin": str, "etat": str, "etudid":int, - "moduleimpl_id": int, "desc":str, } ... ] + ``` """ @@ -891,13 +861,14 @@ def assiduite_delete(): """ Suppression d'une assiduité à partir de son id - Forme des données envoyées : - + DATA + ---- + ```json [ , ... ] - + ``` """ # Récupération des ids envoyés dans la liste @@ -972,13 +943,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 +995,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 +1008,7 @@ def assiduites_edit(): "est_just"?: bool } ] + ``` """ edit_list: list[object] = request.get_json(force=True) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index eacd6778c..09f467ccc 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -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/ - Un filtrage peut être donné avec une query - chemin : /justificatifs//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: - est_just: date_debut: date_fin: etat: order: courant: group_id: + + PARAMS + ----- + user_id:l'id de l'auteur du justificatif + date_debut:date de début du justificatif (supérieur ou égal) + date_fin:date de fin du justificatif (inférieur ou égal) + etat:etat du justificatif → valide, non_valide, attente, modifie + order:retourne les justificatifs dans l'ordre décroissant (non vide = True) + courant:retourne les justificatifs de l'année courante (bool : v/t ou f) + group_id: """ # 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: courant: group_id: + + PARAMS + ----- + user_id:l'id de l'auteur du justificatif + date_debut:date de début du justificatif (supérieur ou égal) + date_fin:date de fin du justificatif (inférieur ou égal) + etat:etat du justificatif → valide, non_valide, attente, modifie + order:retourne les justificatifs dans l'ordre décroissant (non vide = True) + courant:retourne les justificatifs de l'année courante (bool : v/t ou f) + group_id: """ # 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: courant: group_id: + + PARAMS + ----- + user_id:l'id de l'auteur du justificatif + date_debut:date de début du justificatif (supérieur ou égal) + date_fin:date de fin du justificatif (inférieur ou égal) + etat:etat du justificatif → valide, non_valide, attente, modifie + order:retourne les justificatifs dans l'ordre décroissant (non vide = True) + courant:retourne les justificatifs de l'année courante (bool : v/t ou f) + group_id: """ # 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 [ , ... ] - - + ``` """ # 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"?: [ , ... ] } + ``` """ # On récupère le dictionnaire From d37ce3f8d9826bbce7e0ebfd016f290d57cfaabb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 24 Jul 2024 17:34:30 +0200 Subject: [PATCH 05/16] docstrings API --- app/api/billets_absences.py | 22 ++- app/api/departements.py | 54 ++++--- app/api/etudiants.py | 72 +++++++-- app/api/evaluations.py | 73 +++++---- app/api/formations.py | 239 +++++++++++++++------------- app/api/formsemestres.py | 305 ++++++++++++++++++++++-------------- app/api/jury.py | 27 +++- app/api/logos.py | 25 ++- app/api/moduleimpl.py | 83 +++++----- app/api/partitions.py | 99 ++++++++---- app/api/semset.py | 1 + app/api/tokens.py | 10 +- app/api/users.py | 61 ++++++-- app/models/departements.py | 11 ++ 14 files changed, 697 insertions(+), 385 deletions(-) diff --git a/app/api/billets_absences.py b/app/api/billets_absences.py index 160b00b73..e5d0a5362 100644 --- a/app/api/billets_absences.py +++ b/app/api/billets_absences.py @@ -6,6 +6,11 @@ """ API : billets d'absences + +CATEGORY +-------- +Billets d'absence + """ from flask import g, request @@ -29,7 +34,7 @@ from app.scodoc.sco_permissions import Permission @permission_required(Permission.ScoView) @as_json def billets_absence_etudiant(etudid: int): - """Liste des billets d'absence pour cet étudiant""" + """Liste des billets d'absence pour cet étudiant.""" billets = sco_abs_billets.query_billets_etud(etudid) return [billet.to_dict() for billet in billets] @@ -41,7 +46,20 @@ def billets_absence_etudiant(etudid: int): @permission_required(Permission.AbsAddBillet) @as_json 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 + } + ``` + """ data = request.get_json(force=True) # may raise 400 Bad Request etudid = data.get("etudid") abs_begin = data.get("abs_begin") diff --git a/app/api/departements.py b/app/api/departements.py index ea479b344..26e3ec43c 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -9,6 +9,11 @@ Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/), mais évidemment pas sur l'API web (/ScoDoc//api). + +CATEGORY +-------- +Département + """ from datetime import datetime @@ -27,24 +32,13 @@ from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error -def get_departement(dept_ident: str) -> Departement: - "Le departement, par id ou acronyme. Erreur 404 si pas trouvé." - try: - dept_id = int(dept_ident) - except ValueError: - dept_id = None - if dept_id is None: - return Departement.query.filter_by(acronym=dept_ident).first_or_404() - return Departement.query.get_or_404(dept_id) - - @bp.route("/departements") @login_required @scodoc @permission_required(Permission.ScoView) @as_json def departements_list(): - """Liste les départements""" + """Liste tous les départements.""" return [dept.to_dict(with_dept_name=True) for dept in Departement.query] @@ -54,7 +48,7 @@ def departements_list(): @permission_required(Permission.ScoView) @as_json def departements_ids(): - """Liste des ids de départements""" + """Liste des ids de tous les départements.""" return [dept.id for dept in Departement.query] @@ -68,6 +62,7 @@ def departement_by_acronym(acronym: str): Info sur un département. Accès par acronyme. Exemple de résultat : + ```json { "id": 1, "acronym": "TAPI", @@ -76,6 +71,7 @@ def departement_by_acronym(acronym: str): "visible": true, "date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" } + ``` """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() return dept.to_dict(with_dept_name=True) @@ -102,11 +98,15 @@ def departement_by_id(dept_id: int): def departement_create(): """ 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, - "visible":bool, + "visible": bool, } + ``` """ data = request.get_json(force=True) # may raise 400 Bad Request acronym = str(data.get("acronym", "")) @@ -130,10 +130,12 @@ def departement_create(): @as_json def departement_edit(acronym): """ - Edition d'un département: seul visible peut être modifié - The request content type should be "application/json": + Édition d'un département: seul le champ `visible` peut être modifié. + + DATA + ---- { - "visible":bool, + "visible": bool, } """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() @@ -155,7 +157,7 @@ def departement_edit(acronym): @permission_required(Permission.ScoSuperAdmin) 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() acronym = dept.acronym @@ -172,11 +174,14 @@ def departement_delete(acronym): @as_json 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 Exemple de résultat : + ```json [ { "civilite": "M", @@ -191,6 +196,7 @@ def departement_etudiants(acronym: str): }, ... ] + ``` """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() return [etud.to_dict_short() for etud in dept.etudiants] @@ -215,7 +221,7 @@ def departement_etudiants_by_id(dept_id: int): @permission_required(Permission.ScoView) @as_json def departement_formsemestres_ids(acronym: str): - """liste des ids formsemestre du département""" + """Liste des ids de tous les formsemestres du département.""" dept = Departement.query.filter_by(acronym=acronym).first_or_404() return [formsemestre.id for formsemestre in dept.formsemestres] @@ -226,7 +232,7 @@ def departement_formsemestres_ids(acronym: str): @permission_required(Permission.ScoView) @as_json 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.""" dept = Departement.query.get_or_404(dept_id) return [formsemestre.id for formsemestre in dept.formsemestres] @@ -239,7 +245,7 @@ def departement_formsemestres_ids_by_id(dept_id: int): @as_json 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 (au format ISO). diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 8b4db4d3b..92211542d 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -6,6 +6,10 @@ """ API : accès aux étudiants + + CATEGORY + -------- + Étudiants """ from datetime import datetime 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 import sco_etud from app.scodoc.sco_permissions import Permission +from app.scodoc import sco_photos from app.scodoc.sco_utils import json_error, suppress_accents - -import app.scodoc.sco_photos as sco_photos import app.scodoc.sco_utils as scu # Un exemple: @@ -103,6 +106,7 @@ def etudiants_courants(long: bool = False): date_courante: Exemple de résultat : + ```json [ { "id": 1234, @@ -115,6 +119,7 @@ def etudiants_courants(long: bool = False): } ... ] + ``` En format "long": voir documentation. @@ -160,10 +165,13 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None): """ Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé. + PARAMS + ------ etudid : l'etudid de l'étudiant nip : le code nip de l'étudiant ine : le code ine de l'étudiant + `etudid` est unique dans la base (tous départements). Les codes INE et NIP sont uniques au sein d'un département. Si plusieurs objets ont le même code, on ramène le plus récemment inscrit. """ @@ -197,6 +205,8 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non ----- size: + PARAMS + ------ etudid : l'etudid de l'étudiant nip : le code nip de l'étudiant ine : le code ine de l'étudiant @@ -269,9 +279,12 @@ def etudiant_set_photo_image(etudid: int = None): @as_json def etudiants(etudid: int = None, nip: str = None, ine: str = None): """ - Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie - toujours une liste. + Info sur le ou les étudiants correspondants. + + Comme `/etudiant` mais renvoie toujours une liste. + Si non trouvé, liste vide, pas d'erreur. + Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.). """ @@ -304,8 +317,9 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None): @permission_required(Permission.ScoView) @as_json def etudiants_by_name(start: str = "", min_len=3, limit=32): - """Liste des étudiants dont le nom débute par start. - Si start fait moins de min_len=3 caractères, liste vide. + """Liste des étudiants dont le nom débute par `start`. + + Si `start` fait moins de `min_len=3` caractères, liste vide. La casse et les accents sont ignorés. """ if len(start) < min_len: @@ -340,13 +354,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32): @as_json def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): """ - Liste des 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. - 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`. - 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: q_etud = Identite.query.filter_by(id=etudid) @@ -475,10 +489,13 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): """ Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué + PARAMS + ------ formsemestre_id : l'id d'un formsemestre etudid : l'etudid d'un étudiant Exemple de résultat : + ```json [ { "partition_id": 1, @@ -503,6 +520,7 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): "group_name": "A" } ] + ``` """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -530,9 +548,12 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): @permission_required(Permission.EtudInscrit) @as_json def etudiant_create(force=False): - """Création d'un nouvel étudiant + """Création d'un nouvel étudiant. + Si force, crée même si homonymie détectée. + L'étudiant créé n'est pas inscrit à un semestre. + Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme) """ args = request.get_json(force=True) # may raise 400 Bad Request @@ -602,7 +623,10 @@ def etudiant_edit( ): """Édition des données étudiant (identité, admission, adresses). - `code_type`: `etudid`, `ine` ou `nip`. + PARAMS + ------ + `code_type`: le type du code, `etudid`, `ine` ou `nip`. + `code`: la valeur du code """ ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) if not ok: @@ -642,7 +666,23 @@ def etudiant_annotation( code_type: str = "etudid", 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 + } + ``` + """ if not current_user.has_permission(Permission.ViewEtudData): return json_error(403, "non autorisé (manque ViewEtudData)") ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) @@ -679,7 +719,13 @@ def etudiant_annotation_delete( 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) if not ok: diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 3eafaffb9..6e936b9b9 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -6,6 +6,10 @@ """ ScoDoc 9 API : accès aux évaluations + + CATEGORY + -------- + Évaluations """ from flask import g, request from flask_json import as_json @@ -32,24 +36,28 @@ import app.scodoc.sco_utils as scu def get_evaluation(evaluation_id: int): """Description d'une évaluation. + DATA + ---- + ```json { - 'coefficient': 1.0, - 'date_debut': '2016-01-04T08:30:00', - 'date_fin': '2016-01-04T12:30:00', - 'description': 'TP NI9219 Température', - 'evaluation_type': 0, - 'id': 15797, - 'moduleimpl_id': 1234, - 'note_max': 20.0, - 'numero': 3, - 'poids': { - 'UE1.1': 1.0, - 'UE1.2': 1.0, - 'UE1.3': 1.0 - }, - 'publish_incomplete': False, - 'visibulletin': True - } + 'coefficient': 1.0, + 'date_debut': '2016-01-04T08:30:00', + 'date_fin': '2016-01-04T12:30:00', + 'description': 'TP Température', + 'evaluation_type': 0, + 'id': 15797, + 'moduleimpl_id': 1234, + 'note_max': 20.0, + 'numero': 3, + 'poids': { + 'UE1.1': 1.0, + 'UE1.2': 1.0, + 'UE1.3': 1.0 + }, + 'publish_incomplete': False, + 'visibulletin': True + } + ``` """ query = Evaluation.query.filter_by(id=evaluation_id) if g.scodoc_dept: @@ -70,11 +78,13 @@ def get_evaluation(evaluation_id: int): @as_json def moduleimpl_evaluations(moduleimpl_id: int): """ - Retourne la liste des évaluations d'un moduleimpl + Retourne la liste des évaluations d'un moduleimpl. + PARAMS + ------ moduleimpl_id : l'id d'un moduleimpl - Exemple de résultat : voir /evaluation + Exemple de résultat : voir `/evaluation`. """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) return [evaluation.to_dict_api() for evaluation in modimpl.evaluations] @@ -88,8 +98,10 @@ def moduleimpl_evaluations(moduleimpl_id: int): @as_json def evaluation_notes(evaluation_id: int): """ - Retourne la liste des notes de l'évaluation + Retourne la liste des notes de l'évaluation. + PARAMS + ------ evaluation_id : l'id de l'évaluation Exemple de résultat : @@ -145,13 +157,18 @@ def evaluation_notes(evaluation_id: int): @as_json def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set """Écriture de notes dans une évaluation. - The request content type should be "application/json", - and contains: + + DATA + ---- + ```json { 'notes' : [ [etudid, value], ... ], 'comment' : optional string } - Result: + ``` + + Résultat: + - nb_changed: nombre de notes changées - nb_suppress: nombre de notes effacées - etudids_with_decision: liste des etudiants dont la note a changé @@ -186,8 +203,9 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set @as_json def evaluation_create(moduleimpl_id: int): """Création d'une évaluation. - The request content type should be "application/json", - and contains: + + DATA + ---- { "description" : str, "evaluation_type" : int, // {0,1,2} default 0 (normale) @@ -200,7 +218,8 @@ def evaluation_create(moduleimpl_id: int): "coefficient" : float, // si non spécifié, 1.0 "poids" : { ue_id : poids } // optionnel } - Result: l'évaluation créée. + + Résultat: l'évaluation créée. """ moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) if not moduleimpl.can_edit_evaluation(current_user): @@ -250,7 +269,7 @@ def evaluation_create(moduleimpl_id: int): @as_json def evaluation_delete(evaluation_id: int): """Suppression d'une évaluation. - Efface aussi toutes ses notes + Efface aussi toutes ses notes. """ query = Evaluation.query.filter_by(id=evaluation_id) if g.scodoc_dept: diff --git a/app/api/formations.py b/app/api/formations.py index 93f17248f..94b984a4b 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -6,6 +6,10 @@ """ ScoDoc 9 API : accès aux formations + + CATEGORY + -------- + Formations """ from flask import flash, g, request @@ -38,7 +42,8 @@ from app.scodoc.sco_permissions import Permission @as_json 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). """ query = Formation.query if g.scodoc_dept: @@ -58,7 +63,7 @@ def formations_ids(): Retourne la liste de toutes les id de formations (tous départements, ou du département indiqué dans la route) - Exemple de résultat : [ 17, 99, 32 ] + Exemple de résultat : `[ 17, 99, 32 ]`. """ query = Formation.query if g.scodoc_dept: @@ -74,24 +79,26 @@ def formations_ids(): @as_json def formation_by_id(formation_id: int): """ - La formation d'id donné + La formation d'id donné. - formation_id : l'id d'une formation Exemple de résultat : - { - "id": 1, - "acronyme": "BUT R&T", - "titre_officiel": "Bachelor technologique réseaux et télécommunications", - "formation_code": "V1RET", - "code_specialite": null, - "dept_id": 1, - "titre": "BUT R&T", - "version": 1, - "type_parcours": 700, - "referentiel_competence_id": null, - "formation_id": 1 - } + + ```json + { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique réseaux et télécommunications", + "formation_code": "V1RET", + "code_specialite": null, + "dept_id": 1, + "titre": "BUT R&T", + "version": 1, + "type_parcours": 700, + "referentiel_competence_id": null, + "formation_id": 1 + } + ``` """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: @@ -123,97 +130,102 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): """ Retourne la formation, avec UE, matières, modules + PARAMS + ------ formation_id : l'id d'une formation - export_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 : + + ```json + { + "id": 1, + "acronyme": "BUT R&T", + "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", + "formation_code": "V1RET", + "code_specialite": null, + "dept_id": 1, + "titre": "BUT R&T", + "version": 1, + "type_parcours": 700, + "referentiel_competence_id": null, + "formation_id": 1, + "ue": [ { - "id": 1, - "acronyme": "BUT R&T", - "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", - "formation_code": "V1RET", - "code_specialite": null, - "dept_id": 1, - "titre": "BUT R&T", - "version": 1, - "type_parcours": 700, - "referentiel_competence_id": null, - "formation_id": 1, - "ue": [ + "acronyme": "RT1.1", + "numero": 1, + "titre": "Administrer les r\u00e9seaux et l\u2019Internet", + "type": 0, + "ue_code": "UCOD11", + "ects": 12.0, + "is_external": false, + "code_apogee": "", + "coefficient": 0.0, + "semestre_idx": 1, + "color": "#B80004", + "reference": 1, + "matiere": [ { - "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": "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": [ { - "titre": "Initiation aux r\u00e9seaux informatiques", - "abbrev": "Init aux r\u00e9seaux informatiques", - "code": "R101", - "heures_cours": 0.0, - "heures_td": 0.0, - "heures_tp": 0.0, - "coefficient": 1.0, - "ects": "", - "semestre_id": 1, - "numero": 10, - "code_apogee": "", - "module_type": 2, - "coefficients": [ - { - "ue_reference": "1", - "coef": "12.0" - }, - { - "ue_reference": "2", - "coef": "4.0" - }, - { - "ue_reference": "3", - "coef": "4.0" - } - ] + "ue_reference": "1", + "coef": "12.0" }, { - "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...", - "abbrev": "Hygi\u00e8ne informatique", - "code": "SAE11", - "heures_cours": 0.0, - "heures_td": 0.0, - "heures_tp": 0.0, - "coefficient": 1.0, - "ects": "", - "semestre_id": 1, - "numero": 10, - "code_apogee": "", - "module_type": 3, - "coefficients": [ - { - "ue_reference": "1", - "coef": "16.0" - } - ] + "ue_reference": "2", + "coef": "4.0" }, - ... - ] + { + "ue_reference": "3", + "coef": "4.0" + } + ] + }, + { + "titre": "Se sensibiliser \u00e0 l'hygi\u00e8ne informatique...", + "abbrev": "Hygi\u00e8ne informatique", + "code": "SAE11", + "heures_cours": 0.0, + "heures_td": 0.0, + "heures_tp": 0.0, + "coefficient": 1.0, + "ects": "", + "semestre_id": 1, + "numero": 10, + "code_apogee": "", + "module_type": 3, + "coefficients": [ + { + "ue_reference": "1", + "coef": "16.0" + } + ] }, ... - ] + ] }, - ] - } + ... + ] + }, + ] + } + ``` """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: @@ -236,11 +248,8 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): @as_json def referentiel_competences(formation_id: int): """ - Retourne le référentiel de compétences - - formation_id : l'id d'une formation - - return null si pas de référentiel associé. + Retourne le référentiel de compétences de la formation + ou null si pas de référentiel associé. """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: @@ -259,8 +268,14 @@ def referentiel_competences(formation_id: int): @as_json def ue_set_parcours(ue_id: int): """Associe UE et parcours BUT. + 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) if g.scodoc_dept: @@ -293,7 +308,7 @@ def ue_set_parcours(ue_id: int): @permission_required(Permission.EditFormation) @as_json 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) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) @@ -323,7 +338,7 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int): @as_json def ue_desassoc_niveau(ue_id: int): """Désassocie cette UE de son niveau de compétence - (si elle n'est pas associée, ne fait rien) + (si elle n'est pas associée, ne fait rien). """ query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: @@ -345,7 +360,7 @@ def ue_desassoc_niveau(ue_id: int): @scodoc @permission_required(Permission.ScoView) def get_ue(ue_id: int): - """Renvoie l'UE""" + """Renvoie l'UE.""" query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) @@ -359,7 +374,7 @@ def get_ue(ue_id: int): @scodoc @permission_required(Permission.ScoView) def formation_module_get(module_id: int): - """Renvoie le module""" + """Renvoie le module.""" query = Module.query.filter_by(id=module_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) @@ -390,13 +405,15 @@ def formation_module_get(module_id: int): @permission_required(Permission.EditFormation) def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""): """Change le code Apogée de l'UE. + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur formation verrouillée) + + Ce changement peut être fait sur formation verrouillée. Si ue_id n'est pas spécifié, utilise l'argument oid du POST. Si code_apogee n'est pas spécifié ou vide, - utilise l'argument value du POST + utilise l'argument value du POST. Le retour est une chaîne (le code enregistré), pas json. """ @@ -444,9 +461,11 @@ def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""): @permission_required(Permission.EditFormation) def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""): """Change le code Apogée du RCUE de l'UE. + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur formation verrouillée) + + Ce changement peut être fait sur formation verrouillée. Si code_apogee n'est pas spécifié ou vide, utilise l'argument value du POST (utilisé par jinplace.js) @@ -497,9 +516,11 @@ def formation_module_set_code_apogee( module_id: int | None = None, code_apogee: str = "" ): """Change le code Apogée du module. + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur formation verrouillée) + + Ce changement peut être fait sur formation verrouillée. Si module_id n'est pas spécifié, utilise l'argument oid du POST. Si code_apogee n'est pas spécifié ou vide, diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 6feba7d68..1a1fd9e9b 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -6,6 +6,12 @@ """ ScoDoc 9 API : accès aux formsemestres + + + CATEGORY + -------- + FormSemestre + """ from operator import attrgetter, itemgetter @@ -55,36 +61,37 @@ def formsemestre_infos(formsemestre_id: int): formsemestre_id : l'id du formsemestre Exemple de résultat : - { - "block_moyennes": false, - "bul_bgcolor": "white", - "bul_hide_xml": false, - "date_debut_iso": "2021-09-01", - "date_debut": "01/09/2021", - "date_fin_iso": "2022-08-31", - "date_fin": "31/08/2022", - "dept_id": 1, - "elt_annee_apo": null, - "elt_passage_apo" : null, - "elt_sem_apo": null, - "ens_can_edit_eval": false, - "etat": true, - "formation_id": 1, - "formsemestre_id": 1, - "gestion_compensation": false, - "gestion_semestrielle": false, - "id": 1, - "modalite": "FI", - "resp_can_change_ens": true, - "resp_can_edit": false, - "responsables": [1, 99], // uids - "scodoc7_id": null, - "semestre_id": 1, - "titre_formation" : "BUT GEA", - "titre_num": "BUT GEA semestre 1", - "titre": "BUT GEA", - } - + ```json + { + "block_moyennes": false, + "bul_bgcolor": "white", + "bul_hide_xml": false, + "date_debut_iso": "2021-09-01", + "date_debut": "01/09/2021", + "date_fin_iso": "2022-08-31", + "date_fin": "31/08/2022", + "dept_id": 1, + "elt_annee_apo": null, + "elt_passage_apo" : null, + "elt_sem_apo": null, + "ens_can_edit_eval": false, + "etat": true, + "formation_id": 1, + "formsemestre_id": 1, + "gestion_compensation": false, + "gestion_semestrielle": false, + "id": 1, + "modalite": "FI", + "resp_can_change_ens": true, + "resp_can_edit": false, + "responsables": [1, 99], // uids + "scodoc7_id": null, + "semestre_id": 1, + "titre_formation" : "BUT GEA", + "titre_num": "BUT GEA semestre 1", + "titre": "BUT GEA", + } + ``` """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -101,8 +108,8 @@ def formsemestre_infos(formsemestre_id: int): @as_json def formsemestres_query(): """ - Retourne les formsemestres filtrés par - étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant + Retourne les formsemestres filtrés par étape Apogée ou année scolaire + ou département (acronyme ou id) ou état ou code étudiant. PARAMS ------ @@ -192,7 +199,36 @@ def formsemestres_query(): @permission_required(Permission.EditFormSemestre) @as_json 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) args = request.get_json(force=True) # may raise 400 Bad Request editable_keys = { @@ -230,13 +266,19 @@ def formsemestre_edit(formsemestre_id: int): @permission_required(Permission.EditApogee) def formsemestre_set_apo_etapes(): """Change les codes étapes du semestre indiqué. + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur un semestre verrouillé) - Args: - oid=int, le formsemestre_id - value=chaine "V1RT, V1RT2", codes séparés par des virgules + Ce changement peut être fait sur un semestre verrouillé + + DATA + ---- + ```json + { + oid : int, le formsemestre_id + value : string, eg "V1RT, V1RT2", codes séparés par des virgules + } """ formsemestre_id = int(request.form.get("oid")) etapes_apo_str = request.form.get("value") @@ -267,13 +309,20 @@ def formsemestre_set_apo_etapes(): @permission_required(Permission.EditApogee) def formsemestre_set_elt_sem_apo(): """Change les codes étapes du semestre indiqué. + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur un semestre verrouillé) - Args: - oid=int, le formsemestre_id - value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules + Ce changement peut être fait sur un semestre verrouillé. + + DATA + ---- + ```json + { + oid : int, le formsemestre_id + value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules + } + ``` """ oid = int(request.form.get("oid")) value = (request.form.get("value") or "").strip() @@ -295,13 +344,20 @@ def formsemestre_set_elt_sem_apo(): @permission_required(Permission.EditApogee) def formsemestre_set_elt_annee_apo(): """Change les codes étapes du semestre indiqué (par le champ oid). + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur un semestre verrouillé) - Args: - oid=int, le formsemestre_id - value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules + Ce changement peut être fait sur un semestre verrouillé. + + DATA + ---- + ```json + { + oid : int, le formsemestre_id + value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules + } + ``` """ oid = int(request.form.get("oid")) value = (request.form.get("value") or "").strip() @@ -323,13 +379,20 @@ def formsemestre_set_elt_annee_apo(): @permission_required(Permission.EditApogee) def formsemestre_set_elt_passage_apo(): """Change les codes apogée de passage du semestre indiqué (par le champ oid). + Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - (Ce changement peut être fait sur un semestre verrouillé) - Args: - oid=int, le formsemestre_id - value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules + Ce changement peut être fait sur un semestre verrouillé. + + DATA + ---- + ```json + { + oid : int, le formsemestre_id + value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules + } + ``` """ oid = int(request.form.get("oid")) value = (request.form.get("value") or "").strip() @@ -355,9 +418,12 @@ def formsemestre_set_elt_passage_apo(): @as_json 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 """ @@ -389,66 +455,67 @@ def formsemestre_programme(formsemestre_id: int): """ Retourne la liste des UEs, ressources et SAEs d'un semestre - formsemestre_id : l'id d'un formsemestre Exemple de résultat : + ```json + { + "ues": [ { - "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, + "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, - "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 - }, + "matiere_id": 3, "module_id": 15, - "moduleimpl_id": 15, - "responsable_id": 2 - }, + "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": [ + { ... - ], - "saes": [ - { - ... - }, - ... - ], - "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] - } + }, + ... + ], + "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] + } + ``` """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -567,9 +634,9 @@ def formsemestre_etat_evaluations(formsemestre_id: int): """ Informations sur l'état des évaluations d'un formsemestre. - formsemestre_id : l'id d'un semestre - Exemple de résultat : + + ```json [ { "id": 1, // moduleimpl_id @@ -597,6 +664,7 @@ def formsemestre_etat_evaluations(formsemestre_id: int): ] }, ] + ``` """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) @@ -671,7 +739,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json def formsemestre_resultat(formsemestre_id: int): - """Tableau récapitulatif des résultats + """Tableau récapitulatif des résultats. + Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules. Si `format=raw`, ne converti pas les valeurs. @@ -726,7 +795,7 @@ def formsemestre_resultat(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json 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) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) @@ -747,12 +816,17 @@ def groups_get_auto_assignment(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json 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) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) + if not formsemestre.can_change_groups(): + return json_error(403, "non autorisé (can_change_groups)") + if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX: return json_error(413, "data too large") formsemestre.groups_auto_assignment_data = request.data @@ -767,17 +841,16 @@ def groups_save_auto_assignment(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json def formsemestre_edt(formsemestre_id: int): - """l'emploi du temps du semestre. + """L'emploi du temps du semestre. + Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur. - group_ids permet de filtrer sur les groupes ScoDoc. - show_modules_titles affiche le titre complet du module (défaut), sinon juste le code. + Expérimental, ne pas utiliser hors ScoDoc. QUERY ----- - group_ids: - show_modules_titles: - + group_ids : string (optionnel) filtre sur les groupes ScoDoc. + show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code. """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: diff --git a/app/api/jury.py b/app/api/jury.py index ef00cb84d..aeec79829 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -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 @@ -91,7 +96,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""): @permission_required(Permission.ScoView) @as_json def validation_ue_delete(etudid: int, validation_id: int): - "Efface cette validation" + "Efface cette validation d'UE." return _validation_ue_delete(etudid, validation_id) @@ -108,7 +113,7 @@ def validation_ue_delete(etudid: int, validation_id: int): @permission_required(Permission.ScoView) @as_json def validation_formsemestre_delete(etudid: int, validation_id: int): - "Efface cette validation" + "Efface cette validation de semestre." # c'est la même chose (formations classiques) return _validation_ue_delete(etudid, validation_id) @@ -160,7 +165,7 @@ def _validation_ue_delete(etudid: int, validation_id: int): @permission_required(Permission.EtudInscrit) @as_json def autorisation_inscription_delete(etudid: int, validation_id: int): - "Efface cette validation" + "Efface cette autorisation d'inscription." etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 @@ -189,8 +194,13 @@ def autorisation_inscription_delete(etudid: int, validation_id: int): @as_json def validation_rcue_record(etudid: int): """Enregistre une validation de RCUE. + Si une validation existe déjà pour ce RCUE, la remplace. - The request content type should be "application/json": + + DATA + ---- + + ```json { "code" : str, "ue1_id" : int, @@ -200,6 +210,7 @@ def validation_rcue_record(etudid: int): "date" : date_iso, // si non spécifié, now() "parcours_id" :int, } + ``` """ etud = tools.get_etud(etudid) if etud is None: @@ -314,7 +325,7 @@ def validation_rcue_record(etudid: int): @permission_required(Permission.EtudInscrit) @as_json def validation_rcue_delete(etudid: int, validation_id: int): - "Efface cette validation" + "Efface cette validation de RCUE." etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 @@ -342,7 +353,7 @@ def validation_rcue_delete(etudid: int, validation_id: int): @permission_required(Permission.EtudInscrit) @as_json def validation_annee_but_delete(etudid: int, validation_id: int): - "Efface cette validation" + "Efface cette validation d'année BUT." etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 @@ -371,7 +382,7 @@ def validation_annee_but_delete(etudid: int, validation_id: int): @permission_required(Permission.EtudInscrit) @as_json def validation_dut120_delete(etudid: int, validation_id: int): - "Efface cette validation" + "Efface cette validation de DUT120." etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 diff --git a/app/api/logos.py b/app/api/logos.py index 1ec747a11..42f10513a 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -50,7 +50,7 @@ from app.scodoc.sco_utils import json_error @permission_required(Permission.ScoSuperAdmin) @as_json def logo_list_globals(): - """Liste tous les logos""" + """Liste des noms des logos définis pour le site ScoDoc.""" logos = list_logos()[None] return list(logos.keys()) @@ -59,6 +59,11 @@ def logo_list_globals(): @scodoc @permission_required(Permission.ScoSuperAdmin) def logo_get_global(logoname): + """Renvoie le logo global de nom donné. + + L'image est au format png ou jpg; le format retourné dépend du format sous lequel + l'image a été initialement enregistrée. + """ logo = find_logo(logoname=logoname) if logo is None: return json_error(404, message="logo not found") @@ -80,6 +85,9 @@ def _core_get_logos(dept_id) -> list: @permission_required(Permission.ScoSuperAdmin) @as_json def logo_get_local_by_acronym(departement): + """Liste des noms des logos définis pour le département + désigné par son acronyme. + """ dept_id = Departement.from_acronym(departement).id return _core_get_logos(dept_id) @@ -89,6 +97,9 @@ def logo_get_local_by_acronym(departement): @permission_required(Permission.ScoSuperAdmin) @as_json def logo_get_local_by_id(dept_id): + """Liste des noms des logos définis pour le département + désigné par son id. + """ return _core_get_logos(dept_id) @@ -108,6 +119,12 @@ def _core_get_logo(dept_id, logoname) -> Response: @scodoc @permission_required(Permission.ScoSuperAdmin) def logo_get_local_dept_by_acronym(departement, logoname): + """Le logo: image (format png ou jpg). + + **Exemple d'utilisation:** + + * `/ScoDoc/api/departement/MMI/logo/header` + """ dept_id = Departement.from_acronym(departement).id return _core_get_logo(dept_id, logoname) @@ -116,4 +133,10 @@ def logo_get_local_dept_by_acronym(departement, logoname): @scodoc @permission_required(Permission.ScoSuperAdmin) def logo_get_local_dept_by_id(dept_id, logoname): + """Le logo: image (format png ou jpg). + + **Exemple d'utilisation:** + + * `/ScoDoc/api/departement/id/3/logo/header` + """ return _core_get_logo(dept_id, logoname) diff --git a/app/api/moduleimpl.py b/app/api/moduleimpl.py index e7999f71a..1771927ac 100644 --- a/app/api/moduleimpl.py +++ b/app/api/moduleimpl.py @@ -6,6 +6,10 @@ """ ScoDoc 9 API : accès aux moduleimpl + + CATEGORY + -------- + ModuleImpl """ from flask_json import as_json @@ -28,38 +32,43 @@ from app.scodoc.sco_permissions import Permission @as_json def moduleimpl(moduleimpl_id: int): """ - Retourne un moduleimpl en fonction de son id + Retourne le moduleimpl. + PARAMS + ------ moduleimpl_id : l'id d'un moduleimpl Exemple de résultat : - { + + ```json + { + "id": 1, + "formsemestre_id": 1, + "module_id": 1, + "responsable_id": 2, + "moduleimpl_id": 1, + "ens": [], + "module": { + "heures_tp": 0, + "code_apogee": "", + "titre": "Initiation aux réseaux informatiques", + "coefficient": 1, + "module_type": 2, "id": 1, - "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 - } + "ects": null, + "abbrev": "Init aux réseaux informatiques", + "ue_id": 1, + "code": "R101", + "formation_id": 1, + "heures_cours": 0, + "matiere_id": 1, + "heures_td": 0, + "semestre_id": 1, + "numero": 10, + "module_id": 1 } + } + ``` """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) return modimpl.to_dict(convert_objects=True) @@ -72,16 +81,20 @@ def moduleimpl(moduleimpl_id: int): @permission_required(Permission.ScoView) @as_json def moduleimpl_inscriptions(moduleimpl_id: int): - """Liste des inscriptions à ce moduleimpl + """Liste des inscriptions à ce moduleimpl. + Exemple de résultat : - [ - { - "id": 1, - "etudid": 666, - "moduleimpl_id": 1234, - }, - ... - ] + + ```json + [ + { + "id": 1, + "etudid": 666, + "moduleimpl_id": 1234, + }, + ... + ] + ``` """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) return [i.to_dict() for i in modimpl.inscriptions] diff --git a/app/api/partitions.py b/app/api/partitions.py index ef3ec1499..61aaca6fc 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -6,6 +6,11 @@ """ ScoDoc 9 API : partitions + + CATEGORY + -------- + Groupes et Partitions + """ from operator import attrgetter @@ -41,7 +46,8 @@ def partition_info(partition_id: int): """Info sur une partition. Exemple de résultat : - ``` + + ```json { 'bul_show_rank': False, 'formsemestre_id': 39, @@ -71,10 +77,11 @@ def partition_info(partition_id: int): @permission_required(Permission.ScoView) @as_json def formsemestre_partitions(formsemestre_id: int): - """Liste de toutes les partitions d'un formsemestre + """Liste de toutes les partitions d'un formsemestre. - formsemestre_id : l'id d'un formsemestre + Exemple de résultat : + ```json { partition_id : { "bul_show_rank": False, @@ -88,7 +95,7 @@ def formsemestre_partitions(formsemestre_id: int): }, ... } - + ``` """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -112,9 +119,14 @@ def group_etudiants(group_id: int): """ Retourne la liste des étudiants dans un groupe (inscrits au groupe et inscrits au semestre). + + PARAMS + ------ group_id : l'id d'un groupe Exemple de résultat : + + ```json [ { 'civilite': 'M', @@ -127,6 +139,7 @@ def group_etudiants(group_id: int): }, ... ] + ``` """ query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: @@ -152,11 +165,11 @@ def group_etudiants(group_id: int): @permission_required(Permission.ScoView) @as_json 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 ----- - etat: + etat : string """ etat = request.args.get("etat") @@ -186,7 +199,7 @@ def group_etudiants_query(group_id: int): @permission_required(Permission.ScoView) @as_json 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) query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: @@ -248,7 +261,8 @@ def group_remove_etud(group_id: int, etudid: int): @permission_required(Permission.ScoView) @as_json def partition_remove_etud(partition_id: int, etudid: int): - """Enlève l'étudiant de tous les groupes de cette partition + """Enlève l'étudiant de tous les groupes de cette partition. + (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition) """ etud = Identite.query.get_or_404(etudid) @@ -293,12 +307,15 @@ def partition_remove_etud(partition_id: int, etudid: int): @permission_required(Permission.ScoView) @as_json def group_create(partition_id: int): # partition-group-create - """Création d'un groupe dans une partition + """Création d'un groupe dans une partition. - The request content type should be "application/json": + DATA + ---- + ```json { "group_name" : nom_du_groupe, } + ``` """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: @@ -345,7 +362,7 @@ def group_create(partition_id: int): # partition-group-create @permission_required(Permission.ScoView) @as_json def group_delete(group_id: int): - """Suppression d'un groupe""" + """Suppression d'un groupe.""" query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( @@ -374,7 +391,7 @@ def group_delete(group_id: int): @permission_required(Permission.ScoView) @as_json def group_edit(group_id: int): - """Edit a group""" + """Édition d'un groupe.""" query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( @@ -415,9 +432,10 @@ def group_edit(group_id: int): @permission_required(Permission.ScoView) @as_json def group_set_edt_id(group_id: int, edt_id: str): - """Set edt_id for this group. - Contrairement à /edit, peut-être changé pour toute partition - ou formsemestre non verrouillé. + """Set edt_id du groupe. + + Contrairement à `/edit`, peut-être changé pour toute partition + d'un formsemestre non verrouillé. """ query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: @@ -443,16 +461,19 @@ def group_set_edt_id(group_id: int, edt_id: str): @permission_required(Permission.ScoView) @as_json def partition_create(formsemestre_id: int): - """Création d'une partition dans un semestre + """Création d'une partition dans un semestre. - The request content type should be "application/json": + DATA + ---- + ```json { "partition_name": str, - "numero":int, - "bul_show_rank":bool, - "show_in_lists":bool, - "groups_editable":bool + "numero": int, + "bul_show_rank": bool, + "show_in_lists": bool, + "groups_editable": bool } + ``` """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -508,8 +529,13 @@ def partition_create(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json def formsemestre_set_partitions_order(formsemestre_id: int): - """Modifie l'ordre des partitions du formsemestre - JSON args: [partition_id1, partition_id2, ...] + """Modifie l'ordre des partitions du formsemestre. + + DATA + ---- + ```json + [ partition_id1, partition_id2, ... ] + ``` """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -520,7 +546,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int): if not formsemestre.can_change_groups(): return json_error(401, "opération non autorisée") partition_ids = request.get_json(force=True) # may raise 400 Bad Request - if not isinstance(partition_ids, int) and not all( + if not isinstance(partition_ids, list) and not all( isinstance(x, int) for x in partition_ids ): return json_error( @@ -549,8 +575,13 @@ def formsemestre_set_partitions_order(formsemestre_id: int): @permission_required(Permission.ScoView) @as_json def partition_order_groups(partition_id: int): - """Modifie l'ordre des groupes de la partition - JSON args: [group_id1, group_id2, ...] + """Modifie l'ordre des groupes de la partition. + + DATA + ---- + ```json + [ group_id1, group_id2, ... ] + ``` """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: @@ -561,7 +592,7 @@ def partition_order_groups(partition_id: int): if not partition.formsemestre.can_change_groups(): return json_error(401, "opération non autorisée") group_ids = request.get_json(force=True) # may raise 400 Bad Request - if not isinstance(group_ids, int) and not all( + if not isinstance(group_ids, list) and not all( isinstance(x, int) for x in group_ids ): return json_error( @@ -586,10 +617,13 @@ def partition_order_groups(partition_id: int): @permission_required(Permission.ScoView) @as_json def partition_edit(partition_id: int): - """Modification d'une partition dans un semestre + """Modification d'une partition dans un semestre. - The request content type should be "application/json" - All fields are optional: + Tous les champs sont optionnels. + + DATA + ---- + ```json { "partition_name": str, "numero":int, @@ -597,6 +631,7 @@ def partition_edit(partition_id: int): "show_in_lists":bool, "groups_editable":bool } + ``` """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: @@ -660,9 +695,9 @@ def partition_edit(partition_id: int): def partition_delete(partition_id: int): """Suppression d'une partition (et de tous ses groupes). - Note 1: La partition par défaut (tous les étudiants du sem.) ne peut + * Note 1: La partition par défaut (tous les étudiants du sem.) ne peut pas être supprimée. - Note 2: Si la partition de parcours est supprimée, les étudiants + * Note 2: Si la partition de parcours est supprimée, les étudiants sont désinscrits des parcours. """ query = Partition.query.filter_by(id=partition_id) diff --git a/app/api/semset.py b/app/api/semset.py index 981c5a095..7822cc581 100644 --- a/app/api/semset.py +++ b/app/api/semset.py @@ -6,6 +6,7 @@ """ ScoDoc 9 API : accès aux formsemestres + """ # from flask import g, jsonify, request # from flask_login import login_required diff --git a/app/api/tokens.py b/app/api/tokens.py index a243e5008..261dcbe52 100644 --- a/app/api/tokens.py +++ b/app/api/tokens.py @@ -3,12 +3,18 @@ from app import db, log from app.api import api_bp as bp from app.auth.logic import basic_auth, token_auth +""" +CATEGORY +-------- +Authentification API +""" + @bp.route("/tokens", methods=["POST"]) @basic_auth.login_required @as_json def token_get(): - "renvoie un jeton jwt pour l'utilisateur courant" + "Renvoie un jeton jwt pour l'utilisateur courant." token = basic_auth.current_user().get_token() log(f"API: giving token to {basic_auth.current_user()}") db.session.commit() @@ -18,7 +24,7 @@ def token_get(): @bp.route("/tokens", methods=["DELETE"]) @token_auth.login_required 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.revoke_token() db.session.commit() diff --git a/app/api/users.py b/app/api/users.py index 8bc96ffa5..a323ff195 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -6,6 +6,10 @@ """ ScoDoc 9 API : accès aux utilisateurs + + CATEGORY + -------- + Utilisateurs """ from flask import g, request @@ -32,7 +36,7 @@ from app.scodoc.sco_utils import json_error @as_json def user_info(uid: int): """ - Info sur un compte utilisateur scodoc + Info sur un compte utilisateur ScoDoc. """ user: User = db.session.get(User, uid) if user is None: @@ -53,7 +57,11 @@ def user_info(uid: int): @as_json def users_info_query(): """Utilisateurs, filtrés par dept, active ou début nom + + Exemple: + ``` /users/query?departement=dept_acronym&active=1&starts_with= + ``` 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 @@ -61,9 +69,9 @@ def users_info_query(): QUERY ----- - active: - departement: - starts_with: + active: bool + departement: string + starts_with: string """ query = User.query @@ -113,7 +121,10 @@ def _is_allowed_user_edit(args: dict) -> tuple[bool, str]: @as_json def user_create(): """Création d'un utilisateur - The request content type should be "application/json": + + DATA + ---- + ```json { "active":bool (default True), "dept": str or null, @@ -122,6 +133,7 @@ def user_create(): "user_name": str, ... } + ``` """ args = request.get_json(force=True) # may raise 400 Bad Request user_name = args.get("user_name") @@ -158,8 +170,10 @@ def user_create(): @permission_required(Permission.UsersAdmin) @as_json def user_edit(uid: int): - """Modification d'un utilisateur + """Modification d'un utilisateur. + Champs modifiables: + ```json { "dept": str or null, "nom": str, @@ -167,6 +181,7 @@ def user_edit(uid: int): "active":bool ... } + ``` """ args = request.get_json(force=True) # may raise 400 Bad Request user: User = User.query.get_or_404(uid) @@ -205,11 +220,15 @@ def user_edit(uid: int): @permission_required(Permission.UsersAdmin) @as_json def user_password(uid: int): - """Modification du mot de passe d'un utilisateur + """Modification du mot de passe d'un utilisateur. + Champs modifiables: + ```json { "password": str } + ```. + Si le mot de passe ne convient pas, erreur 400. """ data = request.get_json(force=True) # may raise 400 Bad Request @@ -243,7 +262,7 @@ def user_password(uid: int): @permission_required(Permission.ScoSuperAdmin) @as_json 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) role: Role = Role.query.filter_by(name=role_name).first_or_404() if dept is not None: # check @@ -272,7 +291,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None): @permission_required(Permission.ScoSuperAdmin) @as_json def user_role_remove(uid: int, role_name: str, dept: str = None): - """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) role: Role = Role.query.filter_by(name=role_name).first_or_404() if dept is not None: # check @@ -299,7 +318,7 @@ def user_role_remove(uid: int, role_name: str, dept: str = None): @permission_required(Permission.UsersView) @as_json def permissions_list(): - """Liste des noms de permissions définies""" + """Liste des noms de permissions définies.""" return list(Permission.permission_by_name.keys()) @@ -321,7 +340,7 @@ def role_get(role_name: str): @permission_required(Permission.UsersView) @as_json def roles_list(): - """Tous les rôles définis""" + """Tous les rôles définis.""" return [role.to_dict() for role in Role.query] @@ -338,7 +357,7 @@ def roles_list(): @permission_required(Permission.ScoSuperAdmin) @as_json 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() permission = Permission.get_by_name(perm_name) if permission is None: @@ -363,7 +382,7 @@ def role_permission_add(role_name: str, perm_name: str): @permission_required(Permission.ScoSuperAdmin) @as_json def role_permission_remove(role_name: str, perm_name: str): - """Remove permission from role""" + """Retire une permission d'un rôle.""" role: Role = Role.query.filter_by(name=role_name).first_or_404() permission = Permission.get_by_name(perm_name) if permission is None: @@ -382,10 +401,15 @@ def role_permission_remove(role_name: str, perm_name: str): @permission_required(Permission.ScoSuperAdmin) @as_json 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', ... ] } + ``` """ role: Role = Role.query.filter_by(name=role_name).first() if role: @@ -410,11 +434,16 @@ def role_create(role_name: str): @permission_required(Permission.ScoSuperAdmin) @as_json 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 "permissions" : [ 'ScoView', ... ] } + ``` """ role: Role = Role.query.filter_by(name=role_name).first_or_404() data = request.get_json(force=True) # may raise 400 Bad Request @@ -442,7 +471,7 @@ def role_edit(role_name: str): @permission_required(Permission.ScoSuperAdmin) @as_json def role_delete(role_name: str): - """Delete a role""" + """Suprression d'un rôle.""" role: Role = Role.query.filter_by(name=role_name).first_or_404() db.session.delete(role) db.session.commit() diff --git a/app/models/departements.py b/app/models/departements.py index c6bb93c8c..64127f6e3 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -52,6 +52,17 @@ class Departement(db.Model): def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>" + @classmethod + def get_departement(cls, dept_ident: str | int) -> "Departement": + "Le département, par id ou acronyme. Erreur 404 si pas trouvé." + try: + dept_id = int(dept_ident) + except ValueError: + dept_id = None + if dept_id is None: + return cls.query.filter_by(acronym=dept_ident).first_or_404() + return cls.query.get_or_404(dept_id) + def to_dict(self, with_dept_name=True, with_dept_preferences=False): data = { "id": self.id, From 0b7be5d08ae9cefb83300504a25d5bdb378fc51e Mon Sep 17 00:00:00 2001 From: Iziram Date: Wed, 24 Jul 2024 17:50:16 +0200 Subject: [PATCH 06/16] =?UTF-8?q?gen=20api=20doc=20:=20cat=C3=A9gories?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/assiduites.py | 4 ++++ tools/create_api_map.py | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 2c1c991cc..e31cb0f7b 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -341,6 +341,10 @@ def evaluation_assiduites(evaluation_id): ] } ``` + + CATEGORY + -------- + evaluations """ # Récupération de l'évaluation try: diff --git a/tools/create_api_map.py b/tools/create_api_map.py index 56f9dd238..963cf3946 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -4,6 +4,7 @@ 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 @@ -508,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( @@ -525,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 @@ -544,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 @@ -903,10 +911,29 @@ def doc_route(doctable: dict) -> str: 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: From 188534819be120770670550f06e8a281240b3e5f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 25 Jul 2024 10:42:49 +0200 Subject: [PATCH 07/16] =?UTF-8?q?Documentation=20API:=20correctifs=20+=20g?= =?UTF-8?q?=C3=A9n=C3=A9ration=20page=20compl=C3=A8te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/evaluations.py | 24 +++++++++++++--------- app/api/formations.py | 16 +++++++-------- app/api/jury.py | 1 - app/api/moduleimpl.py | 34 +++++++++++++++++-------------- app/api/semset.py | 1 - app/templates/{ => doc}/apidoc.j2 | 5 +++-- tools/create_api_map.py | 14 ++++++++----- 7 files changed, 53 insertions(+), 42 deletions(-) rename app/templates/{ => doc}/apidoc.j2 (81%) diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 6e936b9b9..86509059e 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -105,25 +105,29 @@ def evaluation_notes(evaluation_id: int): evaluation_id : l'id de l'évaluation Exemple de résultat : - { - "11": { + ```json + { + "11": { "etudid": 11, "evaluation_id": 1, "value": 15.0, + "note_max" : 20.0, "comment": "", - "date": "Wed, 20 Apr 2022 06:49:05 GMT", + "date": "2024-07-19T19:08:44+02:00", "uid": 2 - }, - "12": { + }, + "12": { "etudid": 12, "evaluation_id": 1, - "value": 12.0, + "value": "ABS", + "note_max" : 20.0, "comment": "", - "date": "Wed, 20 Apr 2022 06:49:06 GMT", + "date": "2024-07-19T19:08:44+02:00", "uid": 2 - }, - ... - } + }, + ... + } + ``` """ query = Evaluation.query.filter_by(id=evaluation_id) if g.scodoc_dept: diff --git a/app/api/formations.py b/app/api/formations.py index 94b984a4b..7f8146cc0 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -411,11 +411,11 @@ def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""): Ce changement peut être fait sur formation verrouillée. - Si ue_id n'est pas spécifié, utilise l'argument oid du POST. - Si code_apogee n'est pas spécifié ou vide, + 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 json. + Le retour est une chaîne (le code enregistré), pas du json. """ if ue_id is None: ue_id = request.form.get("oid") @@ -468,9 +468,9 @@ def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""): Ce changement peut être fait sur formation verrouillée. Si code_apogee n'est pas spécifié ou vide, - utilise l'argument value du POST (utilisé par jinplace.js) + 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: code_apogee = request.form.get("value", "") @@ -522,11 +522,11 @@ def formation_module_set_code_apogee( Ce changement peut être fait sur formation verrouillée. - Si module_id n'est pas spécifié, utilise l'argument oid du POST. - Si code_apogee n'est pas spécifié ou vide, + Si `module_id` n'est pas spécifié, utilise l'argument `oid` du POST. + Si `code_apogee` n'est pas spécifié ou vide, utilise l'argument value du POST (utilisé par jinplace.js) - Le retour est une chaîne (le code enregistré), pas json. + Le retour est une chaîne (le code enregistré), pas du json. """ if module_id is None: module_id = request.form.get("oid") diff --git a/app/api/jury.py b/app/api/jury.py index aeec79829..29ed2dc76 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -199,7 +199,6 @@ def validation_rcue_record(etudid: int): DATA ---- - ```json { "code" : str, diff --git a/app/api/moduleimpl.py b/app/api/moduleimpl.py index 1771927ac..26cafa034 100644 --- a/app/api/moduleimpl.py +++ b/app/api/moduleimpl.py @@ -106,22 +106,26 @@ def moduleimpl_inscriptions(moduleimpl_id: int): @scodoc @permission_required(Permission.ScoView) def moduleimpl_notes(moduleimpl_id: int): - """Liste des notes dans ce moduleimpl + """Liste des notes dans ce moduleimpl. + Exemple de résultat : - [ - { - "etudid": 17776, // code de l'étudiant - "nom": "DUPONT", - "prenom": "Luz", - "38411": 16.0, // Note dans l'évaluation d'id 38411 - "38410": 15.0, - "moymod": 15.5, // Moyenne INDICATIVE module - "moy_ue_2875": 15.5, // Moyenne vers l'UE 2875 - "moy_ue_2876": 15.5, // Moyenne vers l'UE 2876 - "moy_ue_2877": 15.5 // Moyenne vers l'UE 2877 - }, - ... - ] + + ```json + [ + { + "etudid": 17776, // code de l'étudiant + "nom": "DUPONT", + "prenom": "Luz", + "38411": 16.0, // Note dans l'évaluation d'id 38411 + "38410": 15.0, + "moymod": 15.5, // Moyenne INDICATIVE module + "moy_ue_2875": 15.5, // Moyenne vers l'UE 2875 + "moy_ue_2876": 15.5, // Moyenne vers l'UE 2876 + "moy_ue_2877": 15.5 // Moyenne vers l'UE 2877 + }, + ... + ] + ``` """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) app.set_sco_dept(modimpl.formsemestre.departement.acronym) diff --git a/app/api/semset.py b/app/api/semset.py index 7822cc581..981c5a095 100644 --- a/app/api/semset.py +++ b/app/api/semset.py @@ -6,7 +6,6 @@ """ ScoDoc 9 API : accès aux formsemestres - """ # from flask import g, jsonify, request # from flask_login import login_required diff --git a/app/templates/apidoc.j2 b/app/templates/doc/apidoc.j2 similarity index 81% rename from app/templates/apidoc.j2 rename to app/templates/doc/apidoc.j2 index 36d923c94..d5825f83d 100644 --- a/app/templates/apidoc.j2 +++ b/app/templates/doc/apidoc.j2 @@ -1,3 +1,4 @@ +{# Template pour la doc mardown d'un point d'entrée de l'API #} #### **`{{doc.nom}}`** {% if doc.routes %} @@ -6,7 +7,7 @@ {% else %} * **Routes:** {% for route in doc.routes %} - * `{{route|safe}}` + * `{{route|safe}}` {% endfor %} {% endif %} {% endif %} @@ -15,7 +16,7 @@ {% if doc.params %} * **Paramètres:** {% for param in doc.params %} - * `{{param.nom|safe}}` : {{param.description|safe}} + * `{{param.nom|safe}}` : {{param.description|safe}} {% endfor %} {% endif %} {% if doc.description %} diff --git a/tools/create_api_map.py b/tools/create_api_map.py index 963cf3946..fdc798d84 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -547,7 +547,7 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple: # point d'entrée de la commande `flask gen-api-map` -def gen_api_map(app, endpoint_start="api."): +def gen_api_map(app, endpoint_start="api.") -> 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 @@ -566,6 +566,7 @@ def gen_api_map(app, endpoint_start="api."): # 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): @@ -905,7 +906,7 @@ def doc_route(doctable: dict) -> str: "href": f"{jinja_obj['nom'].replace('_', '-')}.json.md", } - return render_template("apidoc.j2", doc=jinja_obj) + return render_template("doc/apidoc.j2", doc=jinja_obj) def gen_api_doc(app, endpoint_start="api."): @@ -922,7 +923,7 @@ def gen_api_doc(app, endpoint_start="api."): categories[category].append(value) # sort categories by name - categories: dict = dict(sorted(categories.items(), key=lambda x: x[0])) + categories: dict = dict(sorted(categories.items(), key=lambda x: x[0].capitalize())) category: str routes: list[dict] @@ -935,9 +936,12 @@ def gen_api_doc(app, endpoint_start="api."): mddoc += doc_route(route) mddoc += "\n\n" - fname = "/tmp/apidoc.md" + table_api = gen_api_map(app, endpoint_start=endpoint_start) + mdpage = render_template("doc/ScoDoc9API.j2", doc_api=mddoc, table_api=table_api) + + fname = "/tmp/ScoDoc9API.md" with open(fname, "w", encoding="utf-8") as f: - f.write(mddoc) + 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}" From c0d2f66081fa322dc7a7c1657f9efb8fd804440d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 25 Jul 2024 10:55:30 +0200 Subject: [PATCH 08/16] Doc API: template + fix _ vs - --- app/templates/doc/ScoDoc9API.j2 | 280 ++++++++++++++++++++++++++++++++ tools/create_api_map.py | 4 +- 2 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 app/templates/doc/ScoDoc9API.j2 diff --git a/app/templates/doc/ScoDoc9API.j2 b/app/templates/doc/ScoDoc9API.j2 new file mode 100644 index 000000000..102c00e46 --- /dev/null +++ b/app/templates/doc/ScoDoc9API.j2 @@ -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/`. +( à 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/`, et aussi via les *routes +départementales* de la forme +`https://scodoc.monsite.tld/ScoDoc//api/` pour un accès +avec des droits restreints au département indiqué. La liste des `` 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 + +
+
+ ![carte_syntaxique](img/API_Chart.svg) +
+
+ +(carte générée avec `flask gen-api-map -e "api."`) + +### Tableau récapitulatif des entrées de l'API + +{{table_api|safe}} + +(table générée avec `flask gen-api-map -e "api."`) + +#### 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) diff --git a/tools/create_api_map.py b/tools/create_api_map.py index fdc798d84..1bd65df6d 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -136,7 +136,7 @@ class Token: element, x_offset, y_offset ) # 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"): href += "-query" question_mark_group = _create_question_mark_group(current_end_coords, href) @@ -505,7 +505,7 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple: # Gestion de doctable doctable = parse_doctable_doc(func.__doc__ or "") - href = func_name.replace("_", "-") + href = func_name if child.query and not href.endswith("-query"): href += "-query" From 5cefe1a3377e61e32b20f32197962535737ca0a0 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 25 Jul 2024 11:08:06 +0200 Subject: [PATCH 09/16] Doc API: typos --- app/api/justificatifs.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 09f467ccc..2c129f7e8 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -38,9 +38,11 @@ from app.scodoc.sco_groups import get_group_members @scodoc @permission_required(Permission.ScoView) def justificatif(justif_id: int = None): - """Retourne un objet justificatif à partir de son id + """Retourne un objet justificatif à partir de son id. Exemple de résultat: + + ```json { "justif_id": 1, "etudid": 2, @@ -52,7 +54,7 @@ def justificatif(justif_id: int = None): "entry_date": "2022-10-31T08:00+01:00", "user_id": 1 or null, } - + ``` """ return get_model_api_object( From f7a8c1d2dbd9909673ea172c734e5632ad1450ea Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 25 Jul 2024 11:45:26 +0200 Subject: [PATCH 10/16] API Doc : lien query OK --- tools/create_api_map.py | 80 +++++++++++++---------------------------- 1 file changed, 24 insertions(+), 56 deletions(-) diff --git a/tools/create_api_map.py b/tools/create_api_map.py index 1bd65df6d..a1489e698 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -503,8 +503,6 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple: child.method = method current_token.add_child(child) - # Gestion de doctable - doctable = parse_doctable_doc(func.__doc__ or "") href = func_name if child.query and not href.endswith("-query"): href += "-query" @@ -527,10 +525,10 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple: if func_name not in doctable_lines: doctable_lines[func_name] = { - "doctable": doctable, "method": method, "nom": func_name, "href": href, + "query": doc_dict.get("QUERY", "") != "", "permission": permissions, "description": doc_dict.get("", ""), "params": doc_dict.get("PARAMS", ""), @@ -745,23 +743,6 @@ def _get_doc_lines(keyword, doc_string: str) -> list[str]: 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]: """ renvoie un dictionnaire {param: } (ex: {assiduite_id : }) @@ -795,45 +776,28 @@ def parse_query_doc(doc_string: str) -> dict[str, str]: return query -def parse_doctable_doc(doc_string: str) -> dict[str, str]: - """ - 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 -): +def _gen_table_line(doctable: dict = None): """ Génère une ligne de tableau markdown | nom de la route| methode HTTP| Permission | """ - lien: str = href - if "href" in doctable: - lien: str = doctable.get("href") + + nom, method, permission = ( + 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})" table: str = "|" @@ -867,7 +831,7 @@ def _gen_table(lines: list[dict]) -> str: """ 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 @@ -886,6 +850,10 @@ def doc_route(doctable: dict) -> str: jinja_obj.update(doctable) jinja_obj["nom"] = doctable["nom"].strip() # on retire les caractères blancs + jinja_obj["query"]: bool + if jinja_obj["query"]: + jinja_obj["nom"] += "(-query)" + if doctable.get("params"): jinja_obj["params"] = [] for param in doctable["params"]: @@ -903,7 +871,7 @@ def doc_route(doctable: dict) -> str: jinja_obj["sample"] = { "nom": f"{jinja_obj['nom']}.json", - "href": f"{jinja_obj['nom'].replace('_', '-')}.json.md", + "href": f"{jinja_obj['nom']}.json.md", } return render_template("doc/apidoc.j2", doc=jinja_obj) From b542e7dab5ee3b1f1bf3e513d78440cda3d1289d Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 25 Jul 2024 12:00:40 +0200 Subject: [PATCH 11/16] API Doc : meilleur tri + optimisations simples --- app/api/assiduites.py | 2 +- app/templates/doc/ScoDoc9API.j2 | 4 ++-- scodoc.py | 13 ------------- tools/create_api_map.py | 24 ++++++++++++++++-------- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index e31cb0f7b..54dd45156 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -344,7 +344,7 @@ def evaluation_assiduites(evaluation_id): CATEGORY -------- - evaluations + Évaluations """ # Récupération de l'évaluation try: diff --git a/app/templates/doc/ScoDoc9API.j2 b/app/templates/doc/ScoDoc9API.j2 index 102c00e46..032c2280f 100644 --- a/app/templates/doc/ScoDoc9API.j2 +++ b/app/templates/doc/ScoDoc9API.j2 @@ -244,13 +244,13 @@ permet de rechercher une entrée à partir du résultat attendu. -(carte générée avec `flask gen-api-map -e "api."`) +(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-map -e "api."`) +(table générée avec `flask gen-api-doc`) #### Note sur les exemples d'utilisation diff --git a/scodoc.py b/scodoc.py index 8c198e236..955113991 100755 --- a/scodoc.py +++ b/scodoc.py @@ -740,19 +740,6 @@ def generate_ens_calendars(): # 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() @click.option( "-e", diff --git a/tools/create_api_map.py b/tools/create_api_map.py index a1489e698..2f4531e07 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -7,6 +7,7 @@ Script permettant de générer une carte SVG de l'API de ScoDoc import sys import xml.etree.ElementTree as ET import re +import unicodedata from app.auth.models import Permission from flask import render_template @@ -270,6 +271,13 @@ class Token: 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)"): """ Fonction générale pour créer un élément SVG simple @@ -545,15 +553,13 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple: # 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 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 """ - api_map, doctable_lines = analyze_api_routes(app, endpoint_start) - # On génère le SVG à partir de l'arbre de Token generate_svg(api_map.to_svg_group(), "/tmp/api_map.svg") print( @@ -879,32 +885,34 @@ def doc_route(doctable: dict) -> str: def gen_api_doc(app, endpoint_start="api."): "commande gen-api-doc" - _, doctable_lines = analyze_api_routes(app, endpoint_start) + api_map, doctable_lines = analyze_api_routes(app, endpoint_start) mddoc: str = "" categories: dict = {} for value in doctable_lines.values(): - category = value["category"] + 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: x[0].capitalize())) + 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: x["nom"]) + 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(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) fname = "/tmp/ScoDoc9API.md" From 3b2888cd5b0e2e2ebf30ccb9c7b0c85b6248f441 Mon Sep 17 00:00:00 2001 From: Iziram Date: Thu, 25 Jul 2024 13:03:10 +0200 Subject: [PATCH 12/16] =?UTF-8?q?APIDoc=20:=20g=C3=A9n=C3=A9ration=20fichi?= =?UTF-8?q?er=20samples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/assiduites.py | 78 +++++++++++++++++++ app/api/justificatifs.py | 45 +++++++++++ .../ressources/samples/assiduites_samples.csv | 26 ++++--- tools/create_api_map.py | 73 +++++++++++++---- 4 files changed, 195 insertions(+), 27 deletions(-) diff --git a/app/api/assiduites.py b/app/api/assiduites.py index 54dd45156..5471d0a83 100644 --- a/app/api/assiduites.py +++ b/app/api/assiduites.py @@ -64,6 +64,11 @@ def assiduite(assiduite_id: int = None): "est_just": False or True, } ``` + + SAMPLES + ------- + /assiduite/1; + """ 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) @@ -156,6 +166,13 @@ def assiduites_count( 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 @@ -221,6 +238,7 @@ def assiduites(etudid: int = None, nip=None, ine=None, with_query: bool = False) date_fin: etat: formsemestre_id: + with_justifs: 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) etat:etat de l'étudiant → absent, present ou retard formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité + with_justif:ajoute les justificatifs liés à l'assiduité + + 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; + ``` """ @@ -384,6 +415,7 @@ def assiduites_group(with_query: bool = False): etat: etudids: formsemestre_id: + with_justif: PARAMS ----- @@ -395,6 +427,11 @@ def assiduites_group(with_query: bool = False): etat:etat de l'étudiant → absent, present ou retard etudids:liste des ids des étudiants concernés par la recherche formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité + with_justifs:ajoute les justificatifs liés à l'assiduité + + SAMPLES + ------- + /assiduites/group/query?etudids=1,2,3; """ @@ -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) etat:etat de l'étudiant → absent, present ou retard formsemestre_id:l'identifiant du formsemestre concerné par l'assiduité + + SAMPLES + ------- + /assiduites/formsemestre/1; + /assiduites/formsemestre/1/query?etat=retard; + /assiduites/formsemestre/1/query?moduleimpl_id=1; + """ # 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é 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 @@ -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 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) @@ -874,6 +935,10 @@ def assiduite_delete(): ] ``` + SAMPLES + ------- + /assiduite/delete;[2,2,3] + """ # Récupération des ids envoyés dans la liste assiduites_list: list[int] = request.get_json(force=True) @@ -958,6 +1023,13 @@ def assiduite_edit(assiduite_id: int): "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 @@ -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) diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index 2c129f7e8..cde11c050 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -55,6 +55,11 @@ def justificatif(justif_id: int = None): "user_id": 1 or null, } ``` + + SAMPLES + ------- + /justificatif/1; + """ return get_model_api_object( @@ -114,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) courant:retourne les justificatifs de l'année courante (bool : v/t ou f) group_id: + + SAMPLES + ------- + /justificatifs/1; + /justificatifs/1/query?etat=attente; + """ # Récupération de l'étudiant etud: Identite = tools.get_etud(etudid, nip, ine) @@ -176,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) courant:retourne les justificatifs de l'année courante (bool : v/t ou f) group_id: + + SAMPLES + ------- + /justificatifs/dept/1; + """ # Récupération du département et des étudiants du département @@ -269,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) courant:retourne les justificatifs de l'année courante (bool : v/t ou f) group_id: + + SAMPLES + ------- + /justificatifs/formsemestre/1; + """ # Récupération du formsemestre @@ -336,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""}] """ @@ -477,6 +501,12 @@ def justif_edit(justif_id: int): "date_fin"?: str } ``` + + SAMPLES + ------- + /justificatif/1/edit;{""etat"":""valide""} + /justificatif/1/edit;{""raison"":""MEDIC""} + """ # Récupération du justificatif à modifier @@ -591,6 +621,11 @@ def justif_delete(): ... ] ``` + + SAMPLES + ------- + /justificatif/delete;[2, 2, 3] + """ # Récupération des justif_ids @@ -836,6 +871,11 @@ def justif_remove(justif_id: int = None): def justif_list(justif_id: int = None): """ Liste les fichiers du justificatif + + SAMPLES + ------- + /justificatif/1/list; + """ # Récupération du justificatif concerné @@ -878,6 +918,11 @@ def justif_list(justif_id: int = None): def justif_justifies(justif_id: int = None): """ Liste assiduite_id justifiées par le justificatif + + SAMPLES + ------- + /justificatif/1/justifies; + """ # On récupère le justificatif concerné diff --git a/tests/ressources/samples/assiduites_samples.csv b/tests/ressources/samples/assiduites_samples.csv index 039c4c91c..6e772510a 100644 --- a/tests/ressources/samples/assiduites_samples.csv +++ b/tests/ressources/samples/assiduites_samples.csv @@ -1,36 +1,38 @@ "entry_name";"url";"permission";"method";"content" "assiduite";"/assiduite/1";"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"; +"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/query?etat=retard";"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";"/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/query?etat=retard";"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/query?etat=retard";"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""}]" "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";"{""moduleimpl_id"":2}" "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"; "justificatifs";"/justificatifs/1";"ScoView";"GET"; "justificatifs";"/justificatifs/1/query?etat=attente";"ScoView";"GET"; "justificatifs_dept";"/justificatifs/dept/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""}]" -"justificatif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""etat"":""valide""}" -"justificatif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""raison"":""MEDIC""}" -"justificatif_delete";"/justificatif/delete";"UsersAdmin";"POST";"[2,2,3]" \ No newline at end of file +"justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""etat"":""valide""}" +"justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""raison"":""MEDIC""}" +"justif_delete";"/justificatif/delete";"UsersAdmin";"POST";"[2, 2, 3]" +"justif_list";"/justificatif/1/list";"ScoView";"GET"; +"justif_justifies";"/justificatif/1/justifies";"UsersAdmin";"GET"; \ No newline at end of file diff --git a/tools/create_api_map.py b/tools/create_api_map.py index 2f4531e07..cdf03c733 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -4,13 +4,14 @@ 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 +import sys import unicodedata +import xml.etree.ElementTree as ET + +from flask import render_template from app.auth.models import Permission -from flask import render_template class COLORS: @@ -541,6 +542,7 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple: "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 @@ -828,19 +830,58 @@ 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 : - - - 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) - + 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: + 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"): """Ecriture du fichier md avec la table""" with open(filename, "w", encoding="UTF-8") as f: @@ -856,6 +897,12 @@ def doc_route(doctable: dict) -> str: 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)" @@ -875,11 +922,6 @@ def doc_route(doctable: dict) -> str: descr = "\n".join(s for s in doctable["description"]) jinja_obj["description"] = descr.strip() - jinja_obj["sample"] = { - "nom": f"{jinja_obj['nom']}.json", - "href": f"{jinja_obj['nom']}.json.md", - } - return render_template("doc/apidoc.j2", doc=jinja_obj) @@ -914,6 +956,7 @@ def gen_api_doc(app, endpoint_start="api."): 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: From f87ed3bb6893ea8d772ee6b97bb2548a25348a57 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 27 Jul 2024 13:28:06 +0200 Subject: [PATCH 13/16] Script test interactif API --- tests/api/api_shell.py | 40 ++++++++ tests/api/exemple-api-scodoc7.py | 170 ------------------------------- 2 files changed, 40 insertions(+), 170 deletions(-) create mode 100644 tests/api/api_shell.py delete mode 100644 tests/api/exemple-api-scodoc7.py diff --git a/tests/api/api_shell.py b/tests/api/api_shell.py new file mode 100644 index 000000000..560a1b097 --- /dev/null +++ b/tests/api/api_shell.py @@ -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]) diff --git a/tests/api/exemple-api-scodoc7.py b/tests/api/exemple-api-scodoc7.py deleted file mode 100644 index a73a36cef..000000000 --- a/tests/api/exemple-api-scodoc7.py +++ /dev/null @@ -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('') -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"] From 4824b333588a99c7b8f04838b3a855b7b465ab03 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 27 Jul 2024 13:28:55 +0200 Subject: [PATCH 14/16] Ameliore generation doc API --- tools/create_api_map.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/create_api_map.py b/tools/create_api_map.py index cdf03c733..924564206 100644 --- a/tools/create_api_map.py +++ b/tools/create_api_map.py @@ -854,7 +854,13 @@ def _gen_csv_line(doc_line: dict) -> str: samples: list[str] = doc_line.get("samples", []) csv_lines: list[str] = [] for sample in samples: - url, content = sample.split(";", maxsplit=1) + 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}"' From 75c10a4917cebdfa7519a36e164b7684dc158f94 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 27 Jul 2024 13:30:02 +0200 Subject: [PATCH 15/16] =?UTF-8?q?API:=20ajout=20samples=20+=20am=C3=A9lior?= =?UTF-8?q?ation=20des=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/billets_absences.py | 7 +- app/api/departements.py | 82 +++++--- app/api/etudiants.py | 65 ++---- app/api/evaluations.py | 44 ++-- app/api/formations.py | 145 +++---------- app/api/formsemestres.py | 150 ++------------ app/api/jury.py | 7 +- app/api/justificatifs.py | 25 +-- app/api/logos.py | 23 ++- app/api/moduleimpl.py | 70 +------ app/api/partitions.py | 88 ++++---- app/api/users.py | 47 ++++- app/models/groups.py | 10 + tests/api/dump_all_results.py | 2 +- tests/api/essai_tous_semestres.py | 2 +- tests/api/exemple-api-basic.py | 34 ++- tests/api/exemple-api-list-modules.py | 2 +- tests/api/make_samples.py | 35 ++-- tests/api/setup_test_api.py | 18 +- tests/api/test_api_assiduites.py | 26 +-- tests/api/test_api_billets.py | 10 +- tests/api/test_api_departements.py | 8 +- tests/api/test_api_etudiants.py | 22 +- tests/api/test_api_evaluations.py | 10 +- tests/api/test_api_formations.py | 22 +- tests/api/test_api_jury.py | 4 +- tests/api/test_api_justificatifs.py | 34 ++- tests/api/test_api_partitions.py | 24 +-- tests/api/test_api_users.py | 44 ++-- tests/ressources/samples/samples.csv | 194 ++++++++---------- .../fakedatabase/create_test_api_database.py | 9 +- 31 files changed, 522 insertions(+), 741 deletions(-) diff --git a/app/api/billets_absences.py b/app/api/billets_absences.py index e5d0a5362..1cb24cb0c 100644 --- a/app/api/billets_absences.py +++ b/app/api/billets_absences.py @@ -25,6 +25,7 @@ from app.models import BilletAbsence from app.models.etudiants import Identite from app.scodoc import sco_abs_billets from app.scodoc.sco_permissions import Permission +from app.scodoc import sco_utils as scu @bp.route("/billets_absence/etudiant/") @@ -59,13 +60,17 @@ def billets_absence_create(): "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 etudid = data.get("etudid") abs_begin = data.get("abs_begin") abs_end = data.get("abs_end") 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): return json_error( 404, message="Paramètre manquant: etudid, abs_begin, abs_end requis" diff --git a/app/api/departements.py b/app/api/departements.py index 26e3ec43c..787193e23 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -38,7 +38,13 @@ from app.scodoc.sco_utils import json_error @permission_required(Permission.ScoView) @as_json def departements_list(): - """Liste tous les départements.""" + """Liste tous les départements. + + SAMPLES + ------- + /departements; + + """ return [dept.to_dict(with_dept_name=True) for dept in Departement.query] @@ -48,7 +54,13 @@ def departements_list(): @permission_required(Permission.ScoView) @as_json def departements_ids(): - """Liste des ids de tous les départements.""" + """Liste des ids de tous les départements. + + SAMPLES + ------- + /departements_ids; + + """ return [dept.id for dept in Departement.query] @@ -61,17 +73,10 @@ def departement_by_acronym(acronym: str): """ Info sur un département. Accès par acronyme. - Exemple de résultat : - ```json - { - "id": 1, - "acronym": "TAPI", - "dept_name" : "TEST", - "description": null, - "visible": true, - "date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" - } - ``` + SAMPLES + ------- + /departement/TAPI; + """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() return dept.to_dict(with_dept_name=True) @@ -82,9 +87,14 @@ def departement_by_acronym(acronym: str): @scodoc @permission_required(Permission.ScoView) @as_json -def departement_by_id(dept_id: int): +def departement_get(dept_id: int): """ Info sur un département. Accès par id. + + SAMPLES + ------- + /departement/id/1; + """ dept = Departement.query.get_or_404(dept_id) return dept.to_dict() @@ -107,6 +117,10 @@ def departement_create(): "visible": bool, } ``` + + SAMPLES + ------- + /departement/create;{""acronym"":""MYDEPT"",""visible"":""1""} """ data = request.get_json(force=True) # may raise 400 Bad Request acronym = str(data.get("acronym", "")) @@ -180,23 +194,10 @@ def departement_etudiants(acronym: str): ------ acronym : l'acronyme d'un département - Exemple de résultat : - ```json - [ - { - "civilite": "M", - "code_ine": "7899X61616", - "code_nip": "F6777H88", - "date_naissance": null, - "email": "toto@toto.fr", - "emailperso": null, - "etudid": 18, - "nom": "MOREL", - "prenom": "JACQUES" - }, - ... - ] - ``` + SAMPLES + ------- + /departement/TAPI/etudiants; + """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() return [etud.to_dict_short() for etud in dept.etudiants] @@ -221,7 +222,13 @@ def departement_etudiants_by_id(dept_id: int): @permission_required(Permission.ScoView) @as_json def departement_formsemestres_ids(acronym: str): - """Liste des ids de tous les formsemestres 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() return [formsemestre.id for formsemestre in dept.formsemestres] @@ -232,7 +239,13 @@ def departement_formsemestres_ids(acronym: str): @permission_required(Permission.ScoView) @as_json def departement_formsemestres_ids_by_id(dept_id: int): - """Liste des ids de tous les formsemestres du département.""" + """Liste des ids de tous les formsemestres du département. + + SAMPLES + ------- + /departement/id/1/formsemestres_ids; + + """ dept = Departement.query.get_or_404(dept_id) return [formsemestre.id for formsemestre in dept.formsemestres] @@ -253,6 +266,9 @@ def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = ----- date_courante: + SAMPLES + ------- + /departement/id/1/formsemestres_courants?date_courante=2022-01-01 """ dept = ( Departement.query.filter_by(acronym=acronym).first_or_404() diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 92211542d..476cdbe83 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -101,27 +101,16 @@ def etudiants_courants(long: bool = False): et les formsemestres contenant la date courante, ou à défaut celle indiquée en argument (au format ISO). + En format "long": voir l'exemple. + QUERY ----- date_courante: - Exemple de résultat : - ```json - [ - { - "id": 1234, - "code_nip": "12345678", - "code_ine": null, - "nom": "JOHN", - "nom_usuel": None, - "prenom": "DEUF", - "civilite": "M", - } - ... - ] - ``` - - En format "long": voir documentation. + SAMPLES + ------- + /etudiants/courants?date_courante=2022-05-01; + /etudiants/courants/long?date_courante=2022-05-01; """ allowed_depts = current_user.get_depts_with_permission(Permission.ScoView) @@ -436,6 +425,10 @@ def bulletin( version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt pdf : si spécifié, bulletin au format PDF (et non JSON). + SAMPLES + ------- + /etudiant/etudid/1/formsemestre/1/bulletin + """ if version == "pdf": version = "long" @@ -494,33 +487,9 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): formsemestre_id : l'id d'un formsemestre etudid : l'etudid d'un étudiant - Exemple de résultat : - ```json - [ - { - "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" - } - ] - ``` + SAMPLES + ------- + /etudiant/etudid/1/formsemestre/1/groups """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -627,6 +596,10 @@ def etudiant_edit( ------ `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) if not ok: @@ -682,6 +655,10 @@ def etudiant_annotation( "comment" : string } ``` + + SAMPLES + ------- + /etudiant/etudid/1/annotation;{""comment"":""une annotation sur l'étudiant""} """ if not current_user.has_permission(Permission.ViewEtudData): return json_error(403, "non autorisé (manque ViewEtudData)") diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 86509059e..498d886cc 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -84,7 +84,9 @@ def moduleimpl_evaluations(moduleimpl_id: int): ------ moduleimpl_id : l'id d'un moduleimpl - Exemple de résultat : voir `/evaluation`. + SAMPLES + ------- + /moduleimpl/1/evaluations """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) return [evaluation.to_dict_api() for evaluation in modimpl.evaluations] @@ -104,30 +106,9 @@ def evaluation_notes(evaluation_id: int): ------ evaluation_id : l'id de l'évaluation - Exemple de résultat : - ```json - { - "11": { - "etudid": 11, - "evaluation_id": 1, - "value": 15.0, - "note_max" : 20.0, - "comment": "", - "date": "2024-07-19T19:08:44+02:00", - "uid": 2 - }, - "12": { - "etudid": 12, - "evaluation_id": 1, - "value": "ABS", - "note_max" : 20.0, - "comment": "", - "date": "2024-07-19T19:08:44+02:00", - "uid": 2 - }, - ... - } - ``` + SAMPLES + ------- + /evaluation/2/notes; """ query = Evaluation.query.filter_by(id=evaluation_id) if g.scodoc_dept: @@ -173,10 +154,14 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set Résultat: - - nb_changed: nombre de notes changées - - nb_suppress: nombre de notes effacées + - etudids_changed: étudiants dont la note est modifiée - etudids_with_decision: liste des etudiants dont la note a changé 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) if g.scodoc_dept: @@ -224,6 +209,11 @@ def evaluation_create(moduleimpl_id: int): } Résultat: l'évaluation créée. + + SAMPLES + ------- + /moduleimpl/1/evaluation/create;{""description"":""Exemple éval.""} + """ moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) if not moduleimpl.can_edit_evaluation(current_user): diff --git a/app/api/formations.py b/app/api/formations.py index 7f8146cc0..f675acc32 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -44,6 +44,11 @@ def formations(): """ Retourne la liste de toutes les formations (tous départements, sauf si route départementale). + + SAMPLES + ------- + /formations; + """ query = Formation.query if g.scodoc_dept: @@ -64,6 +69,11 @@ def formations_ids(): (tous départements, ou du département indiqué dans la route) Exemple de résultat : `[ 17, 99, 32 ]`. + + SAMPLES + ------- + /formations_ids; + """ query = Formation.query if g.scodoc_dept: @@ -77,28 +87,14 @@ def formations_ids(): @scodoc @permission_required(Permission.ScoView) @as_json -def formation_by_id(formation_id: int): +def formation_get(formation_id: int): """ La formation d'id donné. + SAMPLES + ------- + /formation/1; - Exemple de résultat : - - ```json - { - "id": 1, - "acronyme": "BUT R&T", - "titre_officiel": "Bachelor technologique réseaux et télécommunications", - "formation_code": "V1RET", - "code_specialite": null, - "dept_id": 1, - "titre": "BUT R&T", - "version": 1, - "type_parcours": 700, - "referentiel_competence_id": null, - "formation_id": 1 - } - ``` """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: @@ -135,97 +131,9 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): formation_id : l'id d'une formation export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation. - Exemple de résultat : - - ```json - { - "id": 1, - "acronyme": "BUT R&T", - "titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", - "formation_code": "V1RET", - "code_specialite": null, - "dept_id": 1, - "titre": "BUT R&T", - "version": 1, - "type_parcours": 700, - "referentiel_competence_id": null, - "formation_id": 1, - "ue": [ - { - "acronyme": "RT1.1", - "numero": 1, - "titre": "Administrer les r\u00e9seaux et l\u2019Internet", - "type": 0, - "ue_code": "UCOD11", - "ects": 12.0, - "is_external": false, - "code_apogee": "", - "coefficient": 0.0, - "semestre_idx": 1, - "color": "#B80004", - "reference": 1, - "matiere": [ - { - "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'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" - } - ] - }, - ... - ] - }, - ... - ] - }, - ] - } - ``` + SAMPLES + ------- + /formation/1/export """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: @@ -250,6 +158,11 @@ def referentiel_competences(formation_id: int): """ Retourne le référentiel de compétences de la formation ou null si pas de référentiel associé. + + SAMPLES + ------- + /formation/1/referentiel_competences; + """ query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: @@ -360,7 +273,12 @@ def ue_desassoc_niveau(ue_id: int): @scodoc @permission_required(Permission.ScoView) def get_ue(ue_id: int): - """Renvoie l'UE.""" + """Renvoie l'UE. + + SAMPLES + ------- + /formation/ue/1; + """ query = UniteEns.query.filter_by(id=ue_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) @@ -374,7 +292,12 @@ def get_ue(ue_id: int): @scodoc @permission_required(Permission.ScoView) 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) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 1a1fd9e9b..7dc8ccf17 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -54,44 +54,15 @@ from app.tables.recap import TableRecap, RowRecap @scodoc @permission_required(Permission.ScoView) @as_json -def formsemestre_infos(formsemestre_id: int): +def formsemestre_get(formsemestre_id: int): """ Information sur le formsemestre indiqué. formsemestre_id : l'id du formsemestre - Exemple de résultat : - ```json - { - "block_moyennes": false, - "bul_bgcolor": "white", - "bul_hide_xml": false, - "date_debut_iso": "2021-09-01", - "date_debut": "01/09/2021", - "date_fin_iso": "2022-08-31", - "date_fin": "31/08/2022", - "dept_id": 1, - "elt_annee_apo": null, - "elt_passage_apo" : null, - "elt_sem_apo": null, - "ens_can_edit_eval": false, - "etat": true, - "formation_id": 1, - "formsemestre_id": 1, - "gestion_compensation": false, - "gestion_semestrielle": false, - "id": 1, - "modalite": "FI", - "resp_can_change_ens": true, - "resp_can_edit": false, - "responsables": [1, 99], // uids - "scodoc7_id": null, - "semestre_id": 1, - "titre_formation" : "BUT GEA", - "titre_num": "BUT GEA semestre 1", - "titre": "BUT GEA", - } - ``` + SAMPLES + ------- + /formsemestre/1 """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -270,7 +241,7 @@ def formsemestre_set_apo_etapes(): Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. - Ce changement peut être fait sur un semestre verrouillé + Ce changement peut être fait sur un semestre verrouillé. DATA ---- @@ -378,7 +349,7 @@ def formsemestre_set_elt_annee_apo(): @scodoc @permission_required(Permission.EditApogee) def formsemestre_set_elt_passage_apo(): - """Change les codes apogée de passage du semestre indiqué (par le champ oid). + """Change les codes Apogée de passage du semestre indiqué (par le champ oid). Le code est une chaîne, avec éventuellement plusieurs valeurs séparées par des virgules. @@ -425,7 +396,9 @@ def bulletins(formsemestre_id: int, version: str = "long"): 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) if g.scodoc_dept: @@ -455,67 +428,9 @@ def formsemestre_programme(formsemestre_id: int): """ Retourne la liste des UEs, ressources et SAEs d'un semestre - - Exemple de résultat : - ```json - { - "ues": [ - { - "type": 0, - "formation_id": 1, - "ue_code": "UCOD11", - "id": 1, - "ects": 12.0, - "acronyme": "RT1.1", - "is_external": false, - "numero": 1, - "code_apogee": "", - "titre": "Administrer les r\u00e9seaux et l\u2019Internet", - "coefficient": 0.0, - "semestre_idx": 1, - "color": "#B80004", - "ue_id": 1 - }, - ... - ], - "ressources": [ - { - "ens": [ 10, 18 ], - "formsemestre_id": 1, - "id": 15, - "module": { - "abbrev": "Programmer", - "code": "SAE15", - "code_apogee": "V7GOP", - "coefficient": 1.0, - "formation_id": 1, - "heures_cours": 0.0, - "heures_td": 0.0, - "heures_tp": 0.0, - "id": 15, - "matiere_id": 3, - "module_id": 15, - "module_type": 3, - "numero": 50, - "semestre_id": 1, - "titre": "Programmer en Python", - "ue_id": 3 - }, - "module_id": 15, - "moduleimpl_id": 15, - "responsable_id": 2 - }, - ... - ], - "saes": [ - { - ... - }, - ... - ], - "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] - } - ``` + SAMPLES + ------- + /formsemestre/1/programme """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -588,6 +503,10 @@ def formsemestre_etudiants( ----- etat: + SAMPLES + ------- + /formsemestre/1/etudiants/query; + """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: @@ -634,37 +553,9 @@ def formsemestre_etat_evaluations(formsemestre_id: int): """ Informations sur l'état des évaluations d'un formsemestre. - Exemple de résultat : - - ```json - [ - { - "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" - } - }, - ... - ] - }, - ] - ``` + SAMPLES + ------- + /formsemestre/1/etat_evals """ formsemestre = FormSemestre.get_formsemestre(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) @@ -749,6 +640,9 @@ def formsemestre_resultat(formsemestre_id: int): ----- format: + SAMPLES + ------- + /formsemestre/1/resultats; """ format_spec = request.args.get("format", None) if format_spec is not None and format_spec != "raw": diff --git a/app/api/jury.py b/app/api/jury.py index 29ed2dc76..66324ab34 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -53,7 +53,12 @@ from app.scodoc.sco_utils import json_error @permission_required(Permission.ScoView) @as_json 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: formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) if formsemestre is None: diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py index cde11c050..4a7f3649b 100644 --- a/app/api/justificatifs.py +++ b/app/api/justificatifs.py @@ -165,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): """ Renvoie tous les justificatifs d'un département - (en ajoutant un champ "formsemestre" si possible) + (en ajoutant un champ "`formsemestre`" si possible). QUERY ----- @@ -220,9 +220,9 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False): 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: justi (Justificatif): Le justificatif @@ -263,7 +263,7 @@ def _set_sems(justi: Justificatif, restrict: bool) -> dict: @as_json @permission_required(Permission.ScoView) def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): - """Retourne tous les justificatifs du formsemestre + """Retourne tous les justificatifs du formsemestre. QUERY ----- @@ -337,7 +337,7 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False): @permission_required(Permission.AbsChange) 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. DATA ---- @@ -489,7 +489,7 @@ def _create_one( @permission_required(Permission.AbsChange) def justif_edit(justif_id: int): """ - Edition d'un justificatif à partir de son id + Édition d'un justificatif à partir de son id. DATA ---- @@ -611,7 +611,7 @@ def justif_edit(justif_id: int): @permission_required(Permission.AbsChange) def justif_delete(): """ - Suppression d'un justificatif à partir de son id + Suppression d'un justificatif à partir de son id. DATA ---- @@ -699,7 +699,7 @@ def _delete_one(justif_id: int) -> tuple[int, str]: @permission_required(Permission.AbsChange) 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) """ @@ -752,7 +752,8 @@ def justif_import(justif_id: int = None): 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) + + 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) """ @@ -791,7 +792,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None): @permission_required(Permission.AbsChange) def justif_remove(justif_id: int = None): """ - Supression d'un fichier ou d'une archive + Supression d'un fichier ou d'une archive. > Procédure de suppression de fichier : [supprimer un justificatif](FichiersJustificatifs.md#supprimer-un-fichier) @@ -870,7 +871,7 @@ def justif_remove(justif_id: int = None): @permission_required(Permission.ScoView) def justif_list(justif_id: int = None): """ - Liste les fichiers du justificatif + Liste les fichiers du justificatif. SAMPLES ------- @@ -917,7 +918,7 @@ def justif_list(justif_id: int = None): @permission_required(Permission.AbsChange) def justif_justifies(justif_id: int = None): """ - Liste assiduite_id justifiées par le justificatif + Liste `assiduite_id` justifiées par le justificatif. SAMPLES ------- diff --git a/app/api/logos.py b/app/api/logos.py index 42f10513a..a97d782da 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -50,7 +50,12 @@ from app.scodoc.sco_utils import json_error @permission_required(Permission.ScoSuperAdmin) @as_json def logo_list_globals(): - """Liste des noms des logos définis pour le site ScoDoc.""" + """Liste des noms des logos définis pour le site ScoDoc. + + SAMPLES + ------- + /logos + """ logos = list_logos()[None] return list(logos.keys()) @@ -63,6 +68,10 @@ def logo_get_global(logoname): 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) if logo is None: @@ -80,15 +89,19 @@ def _core_get_logos(dept_id) -> list: return list(logos.keys()) -@bp.route("/departement//logos") +@bp.route("/departement//logos") @scodoc @permission_required(Permission.ScoSuperAdmin) @as_json -def logo_get_local_by_acronym(departement): +def departement_logos(dept_acronym: str): """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(departement).id + dept_id = Departement.from_acronym(dept_acronym).id return _core_get_logos(dept_id) @@ -96,7 +109,7 @@ def logo_get_local_by_acronym(departement): @scodoc @permission_required(Permission.ScoSuperAdmin) @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. """ diff --git a/app/api/moduleimpl.py b/app/api/moduleimpl.py index 26cafa034..b5dea29b8 100644 --- a/app/api/moduleimpl.py +++ b/app/api/moduleimpl.py @@ -38,37 +38,9 @@ def moduleimpl(moduleimpl_id: int): ------ moduleimpl_id : l'id d'un moduleimpl - Exemple de résultat : - - ```json - { - "id": 1, - "formsemestre_id": 1, - "module_id": 1, - "responsable_id": 2, - "moduleimpl_id": 1, - "ens": [], - "module": { - "heures_tp": 0, - "code_apogee": "", - "titre": "Initiation aux réseaux informatiques", - "coefficient": 1, - "module_type": 2, - "id": 1, - "ects": null, - "abbrev": "Init aux réseaux informatiques", - "ue_id": 1, - "code": "R101", - "formation_id": 1, - "heures_cours": 0, - "matiere_id": 1, - "heures_td": 0, - "semestre_id": 1, - "numero": 10, - "module_id": 1 - } - } - ``` + SAMPLES + ------- + /moduleimpl/1 """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) return modimpl.to_dict(convert_objects=True) @@ -83,18 +55,9 @@ def moduleimpl(moduleimpl_id: int): def moduleimpl_inscriptions(moduleimpl_id: int): """Liste des inscriptions à ce moduleimpl. - Exemple de résultat : - - ```json - [ - { - "id": 1, - "etudid": 666, - "moduleimpl_id": 1234, - }, - ... - ] - ``` + SAMPLES + ------- + /moduleimpl/1/inscriptions """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) return [i.to_dict() for i in modimpl.inscriptions] @@ -108,24 +71,9 @@ def moduleimpl_inscriptions(moduleimpl_id: int): def moduleimpl_notes(moduleimpl_id: int): """Liste des notes dans ce moduleimpl. - Exemple de résultat : - - ```json - [ - { - "etudid": 17776, // code de l'étudiant - "nom": "DUPONT", - "prenom": "Luz", - "38411": 16.0, // Note dans l'évaluation d'id 38411 - "38410": 15.0, - "moymod": 15.5, // Moyenne INDICATIVE module - "moy_ue_2875": 15.5, // Moyenne vers l'UE 2875 - "moy_ue_2876": 15.5, // Moyenne vers l'UE 2876 - "moy_ue_2877": 15.5 // Moyenne vers l'UE 2877 - }, - ... - ] - ``` + SAMPLES + ------- + /moduleimpl/1/notes """ modimpl = ModuleImpl.get_modimpl(moduleimpl_id) app.set_sco_dept(modimpl.formsemestre.departement.acronym) diff --git a/app/api/partitions.py b/app/api/partitions.py index 61aaca6fc..515615015 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -45,23 +45,9 @@ from app.scodoc import sco_utils as scu def partition_info(partition_id: int): """Info sur une partition. - Exemple de résultat : - - ```json - { - '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 - } - ``` + SAMPLES + ------- + /partition/1 """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: @@ -79,23 +65,9 @@ def partition_info(partition_id: int): def formsemestre_partitions(formsemestre_id: int): """Liste de toutes les partitions d'un formsemestre. - Exemple de résultat : - - ```json - { - 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) if g.scodoc_dept: @@ -124,22 +96,9 @@ def group_etudiants(group_id: int): ------ group_id : l'id d'un groupe - Exemple de résultat : - - ```json - [ - { - 'civilite': 'M', - 'id': 123456, - 'ine': None, - 'nip': '987654321', - 'nom': 'MARTIN', - 'nom_usuel': null, - 'prenom': 'JEAN'} - }, - ... - ] - ``` + SAMPLES + ------- + /group/1/etudiants """ query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: @@ -316,6 +275,10 @@ def group_create(partition_id: int): # partition-group-create "group_name" : nom_du_groupe, } ``` + + SAMPLES + ------- + /partition/1/group/create;{""group_name"" : ""Nouveau Groupe""} """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: @@ -391,7 +354,19 @@ def group_delete(group_id: int): @permission_required(Permission.ScoView) @as_json def group_edit(group_id: int): - """Édition d'un groupe.""" + """Édition d'un groupe. + + DATA + ---- + ```json + { + "group_name" : "A1" + } + + SAMPLES + ------- + /group/1/edit;{""group_name"":""A1""} + """ query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( @@ -436,6 +411,10 @@ def group_set_edt_id(group_id: int, edt_id: str): 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) if g.scodoc_dept: @@ -632,6 +611,10 @@ def partition_edit(partition_id: int): "groups_editable":bool } ``` + + SAMPLES + ------- + /partition/1/edit;{""bul_show_rank"":1} """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: @@ -666,9 +649,8 @@ def partition_edit(partition_id: int): for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"): 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 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(): return json_error( API_CLIENT_ERROR, f"can't change {scu.PARTITION_PARCOURS}" diff --git a/app/api/users.py b/app/api/users.py index a323ff195..f446c7279 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -37,6 +37,10 @@ from app.scodoc.sco_utils import json_error def user_info(uid: int): """ Info sur un compte utilisateur ScoDoc. + + SAMPLES + ------- + /user/2 """ user: User = db.session.get(User, uid) if user is None: @@ -222,14 +226,19 @@ def user_edit(uid: int): def user_password(uid: int): """Modification du mot de passe d'un utilisateur. - Champs modifiables: + Si le mot de passe ne convient pas, erreur 400. + + DATA + ---- ```json { "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 user: User = User.query.get_or_404(uid) @@ -318,7 +327,12 @@ def user_role_remove(uid: int, role_name: str, dept: str = None): @permission_required(Permission.UsersView) @as_json 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()) @@ -329,7 +343,12 @@ def permissions_list(): @permission_required(Permission.UsersView) @as_json 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() @@ -340,7 +359,12 @@ def role_get(role_name: str): @permission_required(Permission.UsersView) @as_json 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] @@ -410,6 +434,10 @@ def role_create(role_name: str): "permissions" : [ 'ScoView', ... ] } ``` + + SAMPLES + ------- + /role/create/customRole;{""permissions"": [""ScoView"", ""UsersView""]} """ role: Role = Role.query.filter_by(name=role_name).first() if role: @@ -471,7 +499,12 @@ def role_edit(role_name: str): @permission_required(Permission.ScoSuperAdmin) @as_json def role_delete(role_name: str): - """Suprression d'un rôle.""" + """Suppression d'un rôle. + + SAMPLES + ------- + /role/customRole/delete + """ role: Role = Role.query.filter_by(name=role_name).first_or_404() db.session.delete(role) db.session.commit() diff --git a/app/models/groups.py b/app/models/groups.py index 429989f4b..bc5943a96 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -242,6 +242,16 @@ class GroupDescr(ScoDocModel): 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: """Nom avec partition: 'TD A' Si groupe par défaut (tous), utilise default ou "-" diff --git a/tests/api/dump_all_results.py b/tests/api/dump_all_results.py index 65d58d617..9b1aee641 100644 --- a/tests/api/dump_all_results.py +++ b/tests/api/dump_all_results.py @@ -30,7 +30,7 @@ from setup_test_api import ( CHECK_CERTIFICATE, get_auth_headers, GET, - POST_JSON, + POST, SCODOC_URL, ) diff --git a/tests/api/essai_tous_semestres.py b/tests/api/essai_tous_semestres.py index d5cd3f56e..d158923ac 100644 --- a/tests/api/essai_tous_semestres.py +++ b/tests/api/essai_tous_semestres.py @@ -23,7 +23,7 @@ from setup_test_api import ( CHECK_CERTIFICATE, get_auth_headers, GET, - POST_JSON, + POST, SCODOC_URL, ) diff --git a/tests/api/exemple-api-basic.py b/tests/api/exemple-api-basic.py index 8b0abad35..ace7cb44f 100644 --- a/tests/api/exemple-api-basic.py +++ b/tests/api/exemple-api-basic.py @@ -39,7 +39,7 @@ from setup_test_api import ( CHECK_CERTIFICATE, get_auth_headers, GET, - POST_JSON, + POST, SCODOC_URL, ) @@ -127,14 +127,12 @@ group_id = 5315 POST(f"/group/{group_id}/set_etudiant/{etudid}", headers=HEADERS) -POST_JSON( - f"/partition/{pid}/group/create", data={"group_name": "Omega10"}, headers=HEADERS -) +POST(f"/partition/{pid}/group/create", data={"group_name": "Omega10"}, headers=HEADERS) partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=HEADERS) pp(partitions) -POST_JSON(f"/group/5559/delete", headers=HEADERS) -POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}, headers=HEADERS) +POST(f"/group/5559/delete", 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 formsemestre_id = 911 @@ -178,19 +176,19 @@ etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=HEADERS)[10] etudid = etud["id"] # 1- Crée une partition, puis la change de nom -js = POST_JSON( +js = POST( f"/formsemestre/{formsemestre_id}/partition/create", data={"partition_name": "PART"}, ) partition_id = js["id"] -POST_JSON( +POST( f"/partition/{partition_id}/edit", data={"partition_name": "PART1", "show_in_lists": True}, headers=HEADERS, ) # 2- Crée un groupe -js = POST_JSON( +js = POST( f"/partition/{partition_id}/group/create", data={"group_name": "G1"}, headers=HEADERS, @@ -198,28 +196,28 @@ js = POST_JSON( group_1 = js["id"] # 3- Crée deux autres groupes -js = POST_JSON( +js = POST( f"/partition/{partition_id}/group/create", data={"group_name": "G2"}, headers=HEADERS, ) -js = POST_JSON( +js = POST( f"/partition/{partition_id}/group/create", data={"group_name": "G3"}, headers=HEADERS, ) # 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 -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 partition = GET(f"/partition/{partition_id}") assert len(partition["groups"]) == 3 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 etuds_g2 = GET(f"/group/{group_2}/etudiants", headers=HEADERS) @@ -229,7 +227,7 @@ assert etuds_g2[0]["id"] == etudid # 8- Ordres des groupes 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", data=[group_2, group_1, group_3], headers=HEADERS, @@ -242,7 +240,7 @@ new_groups = [ assert new_groups == [group_2, group_1, group_3] # 9- Suppression -POST_JSON(f"/partition/{partition_id}/delete") +POST(f"/partition/{partition_id}/delete") # ------ # Tests accès API: @@ -260,13 +258,13 @@ POST_JSON(f"/partition/{partition_id}/delete") """ # -POST_JSON( +POST( "/partition/2264/groups/order", data=[5563, 5562, 5561, 5560, 5558, 5557, 5316, 5315], headers=HEADERS, ) -POST_JSON( +POST( "/formsemestre/1063/partitions/order", data=[2264, 2263, 2265, 2266, 2267, 2372, 2378], headers=HEADERS, diff --git a/tests/api/exemple-api-list-modules.py b/tests/api/exemple-api-list-modules.py index 2cc146593..ea5996c34 100644 --- a/tests/api/exemple-api-list-modules.py +++ b/tests/api/exemple-api-list-modules.py @@ -41,7 +41,7 @@ from setup_test_api import ( CHECK_CERTIFICATE, get_auth_headers, GET, - POST_JSON, + POST, SCODOC_URL, ) diff --git a/tests/api/make_samples.py b/tests/api/make_samples.py index 09cc88c59..047794c8d 100644 --- a/tests/api/make_samples.py +++ b/tests/api/make_samples.py @@ -10,15 +10,15 @@ Si entry_names est spécifié, la génération est restreinte aux exemples cités. 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) Modifer le /opt/scodoc/.env pour pointer sur la base test SCODOC_DATABASE_URI="postgresql:///SCODOC_TEST_API" -puis re-créer cette base +puis re-créer cette base tools/create_database.sh --drop SCODOC_TEST_API flask db upgrade flask sco-db-init --erase @@ -28,10 +28,10 @@ et lancer le serveur test: flask run --debug ``` -Cet utilitaire prend en argument le fichier de nom `samples.csv` contenant la description +Cet utilitaire prend en argument le fichier de nom `samples.csv` contenant la description des exemples (séparés par une tabulation (\t), une ligne par exemple) -* Le nom de l'exemple donne le nom du fichier généré (nom_exemple => nom_exemple.json.md). -Plusieurs lignes peuvent partager le même nom. dans ce cas le fichier contiendra +* Le nom de l'exemple donne le nom du fichier généré (nom_exemple => nom_exemple.json.md). +Plusieurs lignes peuvent partager le même nom. dans ce cas le fichier contiendra chacun des exemples * l'url utilisée * la permission nécessaire (par défaut ScoView) @@ -39,7 +39,7 @@ chacun des exemples * les arguments éventuel (en cas de POST): une chaîne de caractère selon json Implémentation: -Le code complète une structure de données (Samples) qui est un dictionnaire de set +Le code complète une structure de données (Samples) qui est un dictionnaire de set (indicé par le nom des exemples). Chacun des éléments du set est un exemple (Sample) Quand la structure est complète, on génére tous les fichiers textes @@ -48,7 +48,7 @@ Quand la structure est complète, on génére tous les fichiers textes - l'url utilisée - les arguments éventuels - le résultat -Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) +Le tout mis en forme au format markdown et rangé dans le répertoire DATA_DIR (/tmp/samples) qui est créé ou écrasé si déjà existant. """ import os @@ -71,7 +71,7 @@ from setup_test_api import ( CHECK_CERTIFICATE, get_auth_headers, GET, - POST_JSON, + POST, SCODOC_URL, ) @@ -98,18 +98,23 @@ class Sample: elif permission == "UsersAdmin": HEADERS = get_auth_headers("admin_api", "admin_api") else: - raise SampleException(f"Bad permission : {permission}") + raise SampleException(f"Bad permission : {permission}, url={self.url}") if self.method == "GET": self.result = GET(self.url, HEADERS) elif self.method == "POST": if self.content == "": - self.result = POST_JSON(self.url, headers=HEADERS) + self.result = POST(self.url, headers=HEADERS) else: 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] != "#": - error = f'Bad method : "{self.method}"' - raise SampleException(error) + raise SampleException(f'Bad method : "{self.method}", url={self.url}') self.shorten() with open("sample_TEST.json.md", "tw", encoding="utf-8") as f: self.dump(f) @@ -235,3 +240,5 @@ if not CHECK_CERTIFICATE: urllib3.disable_warnings() make_samples(SAMPLES_FILENAME) + +print(f"Fichiers samples générés dans {DATA_DIR}") diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 01547b2e0..3d7ca4616 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -79,6 +79,16 @@ if pytest: 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): """Get and optionaly returns as JSON 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 reply = requests.get( url, - headers=headers or {}, + headers=_DefaultHeaders.headers if headers is None else headers, verify=CHECK_CERTIFICATE, 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") -def POST_JSON( +def POST( path: str, data: dict = {}, headers: dict = None, errmsg=None, dept=None, raw=False ): """Post @@ -132,7 +142,7 @@ def POST_JSON( r = requests.post( url, json=data, - headers=headers or {}, + headers=_DefaultHeaders.headers if headers is None else headers, verify=CHECK_CERTIFICATE, timeout=SCO_TEST_API_TIMEOUT, ) @@ -200,7 +210,7 @@ def check_failure_post(path: str, headers: dict, data: dict, err: str = None): """ 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 except APIError as api_err: if err is not None: diff --git a/tests/api/test_api_assiduites.py b/tests/api/test_api_assiduites.py index 88475df48..a95f06cf3 100644 --- a/tests/api/test_api_assiduites.py +++ b/tests/api/test_api_assiduites.py @@ -10,7 +10,7 @@ from types import NoneType from tests.api.setup_test_api import ( GET, - POST_JSON, + POST, DEPT_ACRONYM, APIError, api_headers, @@ -260,7 +260,7 @@ def test_route_create(api_admin_headers): # Bon fonctionnement data = create_data("present", "03") - res = POST_JSON( + res = POST( f"/assiduite/{ETUDID}/create", [data], api_admin_headers, dept=DEPT_ACRONYM ) check_fields(res, BATCH_FIELD) @@ -275,7 +275,7 @@ def test_route_create(api_admin_headers): check_fields(data, fields=ASSIDUITES_FIELDS) data2 = create_data("absent", "04", MODULE, "desc") - res = POST_JSON( + res = POST( f"/assiduite/{ETUDID}/create", [data2], api_admin_headers, dept=DEPT_ACRONYM ) check_fields(res, BATCH_FIELD) @@ -286,7 +286,7 @@ def test_route_create(api_admin_headers): # Mauvais fonctionnement 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 ) 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" ) - res = POST_JSON( + res = POST( f"/assiduite/{ETUDID}/create", [create_data("absent", "05", FAUX)], api_admin_headers, @@ -316,7 +316,7 @@ def test_route_create(api_admin_headers): for d in range(randint(2, 4)) ] - res = POST_JSON( + res = POST( f"/assiduite/{ETUDID}/create", data, api_admin_headers, dept=DEPT_ACRONYM ) check_fields(res, BATCH_FIELD) @@ -334,7 +334,7 @@ def test_route_create(api_admin_headers): create_data("absent", "01"), ] - res = POST_JSON( + res = POST( f"/assiduite/{ETUDID}/create", data2, api_admin_headers, dept=DEPT_ACRONYM ) check_fields(res, BATCH_FIELD) @@ -359,13 +359,13 @@ def test_route_edit(api_admin_headers): # Bon fonctionnement data = {"etat": "retard", "moduleimpl_id": MODULE} - res = POST_JSON( + res = POST( f"/assiduite/{TO_REMOVE[0]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM ) assert res == {"OK": True} data["moduleimpl_id"] = None - res = POST_JSON( + res = POST( f"/assiduite/{TO_REMOVE[1]}/edit", data, api_admin_headers, dept=DEPT_ACRONYM ) assert res == {"OK": True} @@ -389,13 +389,13 @@ def test_route_delete(api_admin_headers): # Bon fonctionnement 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) for dat in res["success"]: assert dat["message"] == "OK" # 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) assert len(res["errors"]) == 1 @@ -405,7 +405,7 @@ def test_route_delete(api_admin_headers): 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) for dat in res["success"]: assert dat["message"] == "OK" @@ -418,7 +418,7 @@ def test_route_delete(api_admin_headers): 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) assert len(res["errors"]) == 3 diff --git a/tests/api/test_api_billets.py b/tests/api/test_api_billets.py index 389b336ef..a175bdf82 100644 --- a/tests/api/test_api_billets.py +++ b/tests/api/test_api_billets.py @@ -8,7 +8,7 @@ Utilisation : import datetime 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 @@ -28,7 +28,7 @@ def test_billets(api_headers): abs_end="2022-08-01", 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 datetime.datetime.fromisoformat(billet_r["abs_begin"]).replace( tzinfo=None @@ -43,12 +43,10 @@ def test_billets(api_headers): abs_end="2022-08-03", 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) assert len(billets) == 2 # Suppression for billet in billets: - reply = POST_JSON( - f"/billets_absence/{billet['id']}/delete", headers=api_headers - ) + reply = POST(f"/billets_absence/{billet['id']}/delete", headers=api_headers) assert reply["OK"] == True diff --git a/tests/api/test_api_departements.py b/tests/api/test_api_departements.py index ba5f07dd3..48e7a62cf 100644 --- a/tests/api/test_api_departements.py +++ b/tests/api/test_api_departements.py @@ -23,7 +23,7 @@ from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, api_headers, api_admin_headers, ) @@ -42,7 +42,7 @@ def test_create_dept(api_admin_headers): /departement//edit /departement//delete """ - dept = POST_JSON( + dept = POST( "/departement/create", {"acronym": "XTEST", "visible": True}, 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) assert dept["acronym"] == dept_r["acronym"] assert dept_r["visible"] is True - dept_e = POST_JSON( + dept_e = POST( f"/departement/{dept['acronym']}/edit", {"visible": False}, headers=api_admin_headers, ) dept_r = GET(f"/departement/{dept['acronym']}", headers=api_admin_headers) assert dept_r["visible"] is False - r = POST_JSON( + r = POST( f"/departement/{dept['acronym']}/delete", headers=api_admin_headers, ) diff --git a/tests/api/test_api_etudiants.py b/tests/api/test_api_etudiants.py index 27192e5f8..4558d16be 100644 --- a/tests/api/test_api_etudiants.py +++ b/tests/api/test_api_etudiants.py @@ -30,7 +30,7 @@ from tests.api.setup_test_api import ( CHECK_CERTIFICATE, DEPT_ACRONYM, GET, - POST_JSON, + POST, get_auth_headers, ) 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, "civilite": "X", } - _ = POST_JSON( + _ = POST( "/etudiant/create", args, headers=admin_header, @@ -292,7 +292,7 @@ def test_etudiant_annotations(api_headers): "dept": DEPT_ACRONYM, "civilite": "M", } - etud = POST_JSON( + etud = POST( "/etudiant/create", args, headers=admin_header, @@ -304,7 +304,7 @@ def test_etudiant_annotations(api_headers): assert etud["nom"] assert etud["annotations"] == [] # ajoute annotation - annotation = POST_JSON( + annotation = POST( f"/etudiant/etudid/{etudid}/annotation", {"comment": "annotation 1"}, headers=admin_header, @@ -318,7 +318,7 @@ def test_etudiant_annotations(api_headers): assert etud["annotations"][0]["comment"] == "annotation 1" assert etud["annotations"][0]["id"] == annotation_id # Supprime annotation - POST_JSON( + POST( f"/etudiant/etudid/{etudid}/annotation/{annotation_id}/delete", headers=admin_header, ) @@ -932,7 +932,7 @@ def test_etudiant_bulletin_semestre(api_headers): ### -------- Modifie publication bulletins 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 ) 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 # TODO voir forme utilisée par ScoDoc en interne: # formsemestre_bulletinetud?formsemestre_id=1263&etudid=16387 - formsemestre = POST_JSON( + formsemestre = POST( 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", args, headers=admin_header, @@ -1023,7 +1023,7 @@ def test_etudiant_create(api_headers): # assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] # Edition - etud = POST_JSON( + etud = POST( f"/etudiant/etudid/{etudid}/edit", { "civilite": "F", @@ -1039,7 +1039,7 @@ def test_etudiant_create(api_headers): assert len(etud["adresses"]) == 1 # assert etud["adresses"][0]["villedomicile"] == args["adresses"][0]["villedomicile"] # assert etud["adresses"][0]["emailperso"] == args["adresses"][0]["emailperso"] - etud = POST_JSON( + etud = POST( f"/etudiant/etudid/{etudid}/edit", { "adresses": [ @@ -1051,7 +1051,7 @@ def test_etudiant_create(api_headers): headers=admin_header, ) assert etud["adresses"][0]["villedomicile"] == "Barcelona" - etud = POST_JSON( + etud = POST( f"/etudiant/etudid/{etudid}/edit", { "admission": { diff --git a/tests/api/test_api_evaluations.py b/tests/api/test_api_evaluations.py index d297c0fb4..231a546cb 100644 --- a/tests/api/test_api_evaluations.py +++ b/tests/api/test_api_evaluations.py @@ -25,7 +25,7 @@ from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, api_admin_headers, api_headers, check_failure_post, @@ -114,7 +114,7 @@ def test_evaluation_create(api_admin_headers): ) nb_evals = len(evaluations) # - e = POST_JSON( + e = POST( f"/moduleimpl/{moduleimpl_id}/evaluation/create", {"description": "eval test"}, api_admin_headers, @@ -157,7 +157,7 @@ def test_evaluation_create(api_admin_headers): "publish_incomplete": True, "note_max": 100.0, } - e = POST_JSON( + e = POST( f"/moduleimpl/{moduleimpl_id}/evaluation/create", data, api_admin_headers, @@ -209,7 +209,7 @@ def test_evaluation_create(api_admin_headers): ue_ids = [ue["id"] for ue in ues] poids = {ue_id: float(i) + 0.5 for i, ue_id in enumerate(ue_ids)} data.update({"description": "eval avec poids", "poids": poids}) - e = POST_JSON( + e = POST( f"/moduleimpl/{moduleimpl_id}/evaluation/create", data, api_admin_headers, @@ -223,7 +223,7 @@ def test_evaluation_create(api_admin_headers): assert new_nb_evals == nb_evals + 1 nb_evals = new_nb_evals # Delete - ans = POST_JSON( + ans = POST( f"/evaluation/{e_ret['id']}/delete", headers=api_admin_headers, ) diff --git a/tests/api/test_api_formations.py b/tests/api/test_api_formations.py index 53daf27cb..ae7b1e49d 100644 --- a/tests/api/test_api_formations.py +++ b/tests/api/test_api_formations.py @@ -26,7 +26,7 @@ from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, ) from tests.api.tools_test_api import ( verify_fields, @@ -338,11 +338,11 @@ def test_api_ue_apo(api_admin_headers): ue_id = 1 ue = GET(path=f"/formation/ue/{ue_id}", headers=api_admin_headers) assert ue["id"] == ue_id - r = POST_JSON( + r = POST( f"/formation/ue/{ue_id}/set_code_apogee/APOUE", {}, api_admin_headers, raw=True ) assert r.text == "APOUE" - r = POST_JSON( + r = POST( f"/formation/ue/{ue_id}/set_code_apogee_rcue/APORCUE", {}, 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) assert module["id"] == module_id assert module["code_apogee"] == "" - r = POST_JSON( + r = POST( f"/formation/module/{module_id}/set_code_apogee/APOMOD", {}, api_admin_headers, @@ -379,7 +379,7 @@ def test_api_module_edit(api_admin_headers): /formation/module//edit """ module_id = 1 - module = POST_JSON( + module = POST( f"/formation/module/{module_id}/edit", {"heures_cours": 16, "semestre_id": 1, "abbrev": "ALLO"}, api_admin_headers, @@ -390,7 +390,7 @@ def test_api_module_edit(api_admin_headers): assert module["abbrev"] == "ALLO" # tente de changer l'UE: ne devrait rien faire: ue_id = module["ue_id"] - module = POST_JSON( + module = POST( f"/formation/module/{module_id}/edit", {"ue_id": 666}, api_admin_headers, @@ -398,7 +398,7 @@ def test_api_module_edit(api_admin_headers): assert module["ue_id"] == ue_id # tente de changer la formation: ne devrait rien faire: formation_id = module["formation_id"] - module = POST_JSON( + module = POST( f"/formation/module/{module_id}/edit", {"formation_id": 666}, api_admin_headers, @@ -406,7 +406,7 @@ def test_api_module_edit(api_admin_headers): assert module["formation_id"] == formation_id # -- assignation de parcours (ce test suppose que les parcours 1, 2, 3 existent) assert module["parcours"] == [] - module = POST_JSON( + module = POST( f"/formation/module/{module_id}/edit", {"parcours": [1, 2, 3]}, api_admin_headers, @@ -419,7 +419,7 @@ def test_api_ue_edit(api_admin_headers): /formation/ue//edit """ ue_id = 1 - ue = POST_JSON( + ue = POST( f"/formation/ue/{ue_id}/edit", {"titre": "formation test modifiée", "numero": 22}, 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: niveau_competence_id = ue["niveau_competence_id"] - ue = POST_JSON( + ue = POST( f"/formation/ue/{ue_id}/edit", {"niveau_competence_id": 666}, api_admin_headers, @@ -438,7 +438,7 @@ def test_api_ue_edit(api_admin_headers): assert ue["niveau_competence_id"] == niveau_competence_id # tente de changer la formation: ne devrait rien faire: formation_id = ue["formation_id"] - ue = POST_JSON( + ue = POST( f"/formation/ue/{ue_id}/edit", {"formation_id": 666}, api_admin_headers, diff --git a/tests/api/test_api_jury.py b/tests/api/test_api_jury.py index a0bd8a32a..d3f00ebe2 100644 --- a/tests/api/test_api_jury.py +++ b/tests/api/test_api_jury.py @@ -23,7 +23,7 @@ from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, api_headers, ) @@ -54,7 +54,7 @@ def test_jury_decisions(api_headers): # ues = export_formation["ue"] # # Enregistre une validation d'RCUE # etudid = etudiants[0]["id"] - # validation = POST_JSON( + # validation = POST( # f"/etudiant/{etudid}/jury/validation_rcue/record", # data={ # "code": "ADM", diff --git a/tests/api/test_api_justificatifs.py b/tests/api/test_api_justificatifs.py index 5ae86528b..66344851d 100644 --- a/tests/api/test_api_justificatifs.py +++ b/tests/api/test_api_justificatifs.py @@ -12,7 +12,7 @@ from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, APIError, api_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) """ try: - data = POST_JSON(path=path, headers=headers, data=data) + data = POST(path=path, headers=headers, data=data) # ^ Renvoi un 404 except APIError as api_err: if err is not None: @@ -204,14 +204,14 @@ def test_route_create(api_admin_headers): # Bon fonctionnement 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) assert len(res["success"]) == 1 TO_REMOVE.append(res["success"][0]["message"]["justif_id"]) 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) assert len(res["success"]) == 1 @@ -220,7 +220,7 @@ def test_route_create(api_admin_headers): # Mauvais fonctionnement check_failure_post(f"/justificatif/{FAUX}/create", api_admin_headers, [data]) - res = POST_JSON( + res = POST( f"/justificatif/{ETUDID}/create", [create_data("absent", "03")], api_admin_headers, @@ -239,7 +239,7 @@ def test_route_create(api_admin_headers): 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) for dat in res["success"]: check_fields(dat["message"], CREATE_FIELD) @@ -253,7 +253,7 @@ def test_route_create(api_admin_headers): 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) assert len(res["errors"]) == 3 @@ -270,11 +270,11 @@ def test_route_edit(api_admin_headers): # Bon fonctionnement 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() 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() # Mauvais fonctionnement @@ -296,13 +296,13 @@ def test_route_delete(api_admin_headers): # Bon fonctionnement 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) for dat in res["success"]: assert dat["message"] == "OK" # Mauvais fonctionnement - res = POST_JSON("/justificatif/delete", [data], api_admin_headers) + res = POST("/justificatif/delete", [data], api_admin_headers) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 1 @@ -312,7 +312,7 @@ def test_route_delete(api_admin_headers): 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) for dat in res["success"]: assert dat["message"] == "OK" @@ -325,7 +325,7 @@ def test_route_delete(api_admin_headers): FAUX + 2, ] - res = POST_JSON("/justificatif/delete", data2, api_admin_headers) + res = POST("/justificatif/delete", data2, api_admin_headers) check_fields(res, BATCH_FIELD) assert len(res["errors"]) == 3 @@ -473,15 +473,13 @@ def test_remove_justificatif(api_admin_headers): filename: str = "tests/api/test_api_justificatif2.txt" _send_file(2, filename, api_admin_headers) - res: dict = POST_JSON( - "/justificatif/1/remove", {"remove": "all"}, api_admin_headers - ) + res: dict = POST("/justificatif/1/remove", {"remove": "all"}, api_admin_headers) assert res == {"response": "removed"} l = GET("/justificatif/1/list", api_admin_headers) assert isinstance(l, dict) assert l["total"] == 0 - res: dict = POST_JSON( + res: dict = POST( "/justificatif/2/remove", {"remove": "list", "filenames": ["test_api_justificatif2.txt"]}, api_admin_headers, @@ -491,7 +489,7 @@ def test_remove_justificatif(api_admin_headers): assert isinstance(l, dict) assert l["total"] == 1 - res: dict = POST_JSON( + res: dict = POST( "/justificatif/2/remove", {"remove": "list", "filenames": ["test_api_justificatif.txt"]}, api_admin_headers, diff --git a/tests/api/test_api_partitions.py b/tests/api/test_api_partitions.py index e505a31a8..90f475eec 100644 --- a/tests/api/test_api_partitions.py +++ b/tests/api/test_api_partitions.py @@ -21,7 +21,7 @@ from tests.api.setup_test_api import ( API_URL, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, api_headers, ) from tests.api.tools_test_api import ( @@ -73,7 +73,7 @@ def test_formsemestre_partition(api_headers): "bul_show_rank": True, "show_in_lists": True, } - partition_r = POST_JSON( + partition_r = POST( f"/formsemestre/{formsemestre_id}/partition/create", partition_d, headers=headers, @@ -83,7 +83,7 @@ def test_formsemestre_partition(api_headers): assert partition_r["groups"] == {} # --- Création Groupe group_d = {"group_name": "Aé-&"} - group_r = POST_JSON( + group_r = POST( f"/partition/{partition_r['id']}/group/create", group_d, headers=headers, @@ -102,14 +102,14 @@ def test_formsemestre_partition(api_headers): # --- Ajout d'un groupe avec edt_id group_d = {"group_name": "extra", "edt_id": "GEDT"} - group_r = POST_JSON( + group_r = POST( f"/partition/{partition_r['id']}/group/create", group_d, headers=headers, ) assert group_r["edt_id"] == "GEDT" # Edit edt_id - group_r = POST_JSON( + group_r = POST( f"/group/{group_r['id']}/edit", {"edt_id": "GEDT2"}, headers=headers, @@ -121,7 +121,7 @@ def test_formsemestre_partition(api_headers): assert group["edt_id"] == "GEDT2" # Change edt_id via route dédiée: - group_t = POST_JSON( + group_t = POST( f"/group/{group_r['id']}/set_edt_id/GEDT3", headers=headers, ) @@ -130,7 +130,7 @@ def test_formsemestre_partition(api_headers): # Place un étudiant dans le groupe 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 repl["group_id"] == group["id"] assert repl["etudid"] == etud["id"] @@ -139,21 +139,17 @@ def test_formsemestre_partition(api_headers): assert len(etuds) == 1 assert etuds[0]["id"] == etud["id"] # Retire l'étudiant du groupe - repl = POST_JSON( - f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers - ) + repl = POST(f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers) assert len(GET(f"/group/{group['id']}/etudiants", headers=headers)) == 0 # Le retire à nouveau ! (bug #465) - repl = POST_JSON( - f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers - ) + repl = POST(f"/group/{group['id']}/remove_etudiant/{etud['id']}", headers=headers) assert repl["group_id"] == group["id"] # Avec partition (vérifie encodeur JSON) partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=headers) assert partitions # 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 diff --git a/tests/api/test_api_users.py b/tests/api/test_api_users.py index 778565539..b04298cf3 100644 --- a/tests/api/test_api_users.py +++ b/tests/api/test_api_users.py @@ -11,7 +11,7 @@ from tests.api.setup_test_api import ( APIError, CHECK_CERTIFICATE, GET, - POST_JSON, + POST, api_headers, api_admin_headers, get_auth_headers, @@ -76,7 +76,7 @@ def test_edit_users(api_admin_headers): """ admin_h = api_admin_headers nb_users = len(GET("/users/query", headers=admin_h)) - user = POST_JSON( + user = POST( "/user/create", {"user_name": "test_edit_users", "nom": "Toto"}, headers=admin_h, @@ -86,7 +86,7 @@ def test_edit_users(api_admin_headers): assert user["active"] is True assert (nb_users + 1) == len(GET("/users/query", headers=admin_h)) # Change le dept et rend inactif - user = POST_JSON( + user = POST( f"/user/{user['id']}/edit", {"active": False, "dept": "TAPI", "edt_id": "GGG"}, headers=admin_h, @@ -107,37 +107,37 @@ def test_roles(api_admin_headers): /user//edit """ admin_h = api_admin_headers - user = POST_JSON( + user = POST( "/user/create", {"user_name": "test_roles", "nom": "Role", "prenom": "Test"}, headers=admin_h, ) 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" - 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["permissions"] == [] role = GET("/role/Test_X", headers=admin_h) assert role["role_name"] == "Test_X" 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" role = GET("/role/Test_Y", headers=admin_h) assert role["role_name"] == "Test_Y" - role = POST_JSON( + role = POST( "/role/Test_Y/edit", {"permissions": ["ScoView", "AbsChange"]}, headers=admin_h, ) 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"} role = GET("/role/Test_Y", headers=admin_h) 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"} - ans = POST_JSON("/role/Test_Y/delete", headers=admin_h) + ans = POST("/role/Test_Y/delete", headers=admin_h) 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 crée un nouvel utilisateur "chef2", admin dans les 2 premiers dept # puis un utilisateur lambda, dans le dept 2 (AA) - chef2 = POST_JSON( + chef2 = POST( "/user/create", { "user_name": "chef2", @@ -163,23 +163,23 @@ def test_modif_users_depts(api_admin_headers): }, headers=admin_h, ) - role_chef = POST_JSON( + role_chef = POST( "/role/create/chef", {"permissions": ["ScoView", "UsersAdmin", "UsersView"]}, headers=admin_h, ) - _ = POST_JSON( + _ = POST( f"/user/{chef2['id']}/role/chef/add/departement/{dept1['acronym']}", headers=admin_h, ) - _ = POST_JSON( + _ = POST( f"/user/{chef2['id']}/role/chef/add/departement/{dept2['acronym']}", headers=admin_h, ) # Un mot de passe trop simple: ok = False try: - _ = POST_JSON( + _ = POST( f"/user/{chef2['id']}/password", {"password": "123456"}, headers=admin_h, @@ -190,13 +190,13 @@ def test_modif_users_depts(api_admin_headers): assert ok # Un "vrai" mot de passe: chef2_password = "17HIOPpYhabb8qw'E:/jd7FFddjd" - _ = POST_JSON( + _ = POST( f"/user/{chef2['id']}/password", {"password": chef2_password}, headers=admin_h, ) # Création user lambda: - u_lambda = POST_JSON( + u_lambda = POST( "/user/create", { "user_name": "lambda", @@ -210,7 +210,7 @@ def test_modif_users_depts(api_admin_headers): # Le chef va modifier u_lambda: 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: - u = POST_JSON( + u = POST( f"/user/{u_lambda['id']}/edit", {"nom": "toto"}, headers=chef_h, @@ -218,7 +218,7 @@ def test_modif_users_depts(api_admin_headers): ) assert u["nom"] == "toto" # Dans l'autre ? - u = POST_JSON( + u = POST( f"/user/{u_lambda['id']}/edit", {"nom": "toto"}, headers=chef_h, @@ -227,7 +227,7 @@ def test_modif_users_depts(api_admin_headers): # mais pas dans le troisième: ok = False try: - u = POST_JSON( + u = POST( f"/user/{u_lambda['id']}/edit", {"nom": "toto"}, headers=chef_h, @@ -240,7 +240,7 @@ def test_modif_users_depts(api_admin_headers): # Nettoyage: # on ne peut pas supprimer l'utilisateur lambda, mais on # le rend inactif et on le retire de son département - u = POST_JSON( + u = POST( f"/user/{u_lambda['id']}/edit", {"active": False, "dept": None}, headers=admin_h, diff --git a/tests/ressources/samples/samples.csv b/tests/ressources/samples/samples.csv index 3a4eb23ca..fb45d3153 100644 --- a/tests/ressources/samples/samples.csv +++ b/tests/ressources/samples/samples.csv @@ -1,120 +1,90 @@ "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_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/query?etat=present,retard&metric=compte,heure";"ScoView";"GET"; "assiduites_count";"/assiduites/1/count/query?etat=retard";"ScoView";"GET"; -"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count";"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=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_count";"/assiduites/1/count/query?split";"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"; -"departement-create";"/departement/create";"ScoSuperAdmin";"POST";"{""acronym"": ""NEWONE"" , ""visible"": true}" -"departement-delete";"/departement/NEWONE/delete";"ScoSuperAdmin";"POST"; -"departement-edit";"/departement/NEWONE/edit";"ScoSuperAdmin";"POST";"{""visible"": false}" -"departement-etudiants";"/departement/id/1/etudiants";"ScoView";"GET"; -"departement-etudiants";"/departement/TAPI/etudiants";"ScoView";"GET"; -"departement-formsemestres_ids";"/departement/id/1/formsemestres_ids";"ScoView";"GET"; -"departement-formsemestres_ids";"/departement/TAPI/formsemestres_ids";"ScoView";"GET"; -"departement-formsemestres-courants";"/departement/id/1/formsemestres_courants";"ScoView";"GET"; -"departement-formsemestres-courants";"/departement/TAPI/formsemestres_courants";"ScoView";"GET"; -"departement-logo";"/departement/id/1/logo/D";"ScoSuperAdmin";"GET"; -"departement-logo";"/departement/TAPI/logo/D";"ScoSuperAdmin";"GET"; -"departement-logos";"/departement/id/1/logos";"ScoSuperAdmin";"GET"; -"departement-logos";"/departement/TAPI/logos";"ScoSuperAdmin";"GET"; -"departement";"/departement/id/1";"ScoView";"GET"; -"departement";"/departement/TAPI";"ScoView";"GET"; -"departements-ids";"/departements_ids";"ScoView";"GET"; -"departements";"/departements";"ScoView";"GET"; -"etudiant_formsemestres";"/etudiant/nip/11/formsemestres";"ScoView";"GET"; -"etudiant-formsemestre-bulletin";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET"; -#"etudiant-formsemestre-bulletin";"/etudiant/ine/INE11/formsemestre/1/bulletin";"ScoView";"GET"; -#"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin";"ScoView";"GET"; -#"etudiant-formsemestre-bulletin";"/etudiant/nip/11/formsemestre/1/bulletin/short/pdf";"ScoView";"GET"; -"etudiant-formsemestre-groups";"/etudiant/etudid/11/formsemestre/1/groups";"ScoView";"GET"; -"etudiant-formsemestres";"/etudiant/etudid/11/formsemestres";"ScoView";"GET"; -"etudiant-formsemestres";"/etudiant/ine/INE11/formsemestres";"ScoView";"GET"; -"etudiant";"/etudiant/etudid/11";"ScoView";"GET"; -"etudiant";"/etudiant/ine/INE11";"ScoView";"GET"; -"etudiant";"/etudiant/nip/11";"ScoView";"GET"; -"etudiants-clef";"/etudiants/etudid/11";"ScoView";"GET"; -"etudiants-clef";"/etudiants/ine/INE11";"ScoView";"GET"; -"etudiants-clef";"/etudiants/nip/11";"ScoView";"GET"; -"etudiants-courants";"/etudiants/courants?date_courante=2022-07-20";"ScoView";"GET"; -"etudiants-courants";"/etudiants/courants/long?date_courante=2022-07-20";"ScoView";"GET"; -"evaluation";"/evaluation/1";"ScoView";"GET"; -"evaluation-notes";"/evaluation/1/notes";"ScoView";"GET"; -"formation-export";"/formation/1/export_with_ids";"ScoView";"GET"; -"formation-export";"/formation/1/export";"ScoView";"GET"; -"formation-referentiel_competences";"/formation/1/referentiel_competences";"ScoView";"GET"; -"formation";"/formation/1";"ScoView";"GET"; -"formations_ids";"/formations_ids";"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/query?etat=retard";"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/query?etat=retard";"ScoView";"GET"; +"assiduites_formsemestre_count";"/assiduites/formsemestre/1/count/query?etat=present,retard&metric=compte,heure";"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""}]" +"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";"{""moduleimpl_id"":2}" +"assiduite_edit";"/assiduite/1/edit";"UsersAdmin";"POST";"{""etat"": ""retard"",""moduleimpl_id"":3}" +"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""}" +"departements_list";"/departements";"ScoView";"GET"; +"departements_ids";"/departements_ids";"ScoView";"GET"; +"departement_by_acronym";"/departement/TAPI";"ScoView";"GET"; +"departement_get";"/departement/id/1";"ScoView";"GET"; +"departement_create";"/departement/create";"UsersAdmin";"POST";"{""acronym"":""MYDEPT"",""visible"":""1""}" +"departement_etudiants";"/departement/TAPI/etudiants";"ScoView";"GET"; +"departement_formsemestres_ids";"/departement/TAPI/formsemestres_ids";"ScoView";"GET"; +"departement_formsemestres_ids_by_id";"/departement/id/1/formsemestres_ids";"ScoView";"GET"; +"departement_formsemestres_courants";"/departement/id/1/formsemestres_courants?date_courante=2022-01-01";"ScoView";"GET"; +"etudiants_courants";"/etudiants/courants?date_courante=2022-05-01";"ScoView";"GET"; +"etudiants_courants";"/etudiants/courants/long?date_courante=2022-05-01";"ScoView";"GET"; +"bulletin";"/etudiant/etudid/1/formsemestre/1/bulletin";"ScoView";"GET"; +"etudiant_groups";"/etudiant/etudid/1/formsemestre/1/groups";"ScoView";"GET"; +"etudiant_edit";"/etudiant/ine/INE1/edit";"UsersAdmin";"POST";"{""prenom"":""Nouveau Prénom"", ""adresses"":[{""email"":""nouvelle@adresse.fr""}]}" +"etudiant_annotation";"/etudiant/etudid/1/annotation";"UsersAdmin";"POST";"{""comment"":""une annotation sur l'étudiant""}" +"moduleimpl_evaluations";"/moduleimpl/1/evaluations";"ScoView";"GET"; +"evaluation_notes";"/evaluation/2/notes";"ScoView";"GET"; +"evaluation_set_notes";"/evaluation/1/notes/set";"UsersAdmin";"POST";"{""notes"": [[1, 17], [2, ""SUPR""]], ""comment"" : ""sample test""}" +"evaluation_create";"/moduleimpl/1/evaluation/create";"UsersAdmin";"POST";"{""description"":""Exemple éval.""}" "formations";"/formations";"ScoView";"GET"; -"formsemestre-bulletins";"/formsemestre/1/bulletins";"ScoView";"GET"; -"formsemestre-decisions_jury";"/formsemestre/1/decisions_jury";"ScoView";"GET"; -"formsemestre-etat_evals";"/formsemestre/1/etat_evals";"ScoView";"GET"; -"formsemestre-etudiants-query";"/formsemestre/1/etudiants/query?etat=D";"ScoView";"GET"; -"formsemestre-etudiants";"/formsemestre/1/etudiants";"ScoView";"GET"; -"formsemestre-etudiants";"/formsemestre/1/etudiants/long";"ScoView";"GET"; -"formsemestre-partition-create";"/formsemestre/1/partition/create";"ScoSuperAdmin";"POST";"{""partition_name"": ""PART""} " -"formsemestre-partitions-order";"/formsemestre/1/partitions/order";"ScoSuperAdmin";"POST";"[ 1 ]" -"formsemestre-partitions";"/formsemestre/1/partitions";"ScoView";"GET"; -"formsemestre-programme";"/formsemestre/1/programme";"ScoView";"GET"; -"formsemestre-resultats";"/formsemestre/1/resultats";"ScoView";"GET"; -"formsemestre";"/formsemestre/1";"ScoView";"GET"; -"formsemestres-query";"/formsemestres/query?annee_scolaire=2022&etape_apo=A2";"ScoView";"GET"; -"formsemestres-query";"/formsemestres/query?nip=11";"ScoView";"GET"; -"group-delete";"/group/2/delete";"ScoSuperAdmin";"POST"; -"group-edit";"/group/2/edit";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP2""}" -"group-etudiants-query";"/group/1/etudiants/query?etat=D";"ScoView";"GET"; -"group-etudiants";"/group/1/etudiants";"ScoView";"GET"; -"group-remove_etudiant";"/group/1/remove_etudiant/10";"ScoSuperAdmin";"POST"; -"group-set_etudiant";"/group/1/set_etudiant/10";"ScoSuperAdmin";"POST"; -"logo";"/logo/B";"ScoSuperAdmin";"GET"; -"logos";"/logos";"ScoSuperAdmin";"GET"; -"moduleimpl-evaluations";"/moduleimpl/1/evaluations";"ScoView";"GET"; -"moduleimpl";"/moduleimpl/1";"ScoView";"GET"; -"partition-delete";"/partition/2/delete";"ScoSuperAdmin";"POST"; -"partition-edit";"/partition/1/edit";"ScoSuperAdmin";"POST";"{""partition_name"":""P2BIS"", ""numero"":3,""bul_show_rank"":true,""show_in_lists"":false, ""groups_editable"":true}" -"partition-group-create";"/partition/1/group/create";"ScoSuperAdmin";"POST";"{""group_name"": ""NEW_GROUP""}" -"partition-groups-order";"/partition/1/groups/order";"ScoSuperAdmin";"POST";"[ 1 ]" -"partition-remove_etudiant";"/partition/2/remove_etudiant/10";"ScoSuperAdmin";"POST"; -"partition";"/partition/1";"ScoView";"GET"; -"permissions";"/permissions";"ScoView";"GET"; -"role-add_permission";"/role/customRole/add_permission/UsersView";"ScoSuperAdmin";"POST"; -"role-create";"/role/create/customRole";"ScoSuperAdmin";"POST";"{""permissions"": [""ScoView"", ""UsersView""]}" -"role-delete";"/role/customRole/delete";"ScoSuperAdmin";"POST"; -"role-edit";"/role/customRole/edit";"ScoSuperAdmin";"POST";"{ ""name"" : ""LaveurDeVitres"", ""permissions"" : [ ""ScoView"" ] }" -"role-remove_permission";"/role/customRole/remove_permission/UsersView";"ScoSuperAdmin";"POST"; -"role";"/role/Observateur";"ScoView";"GET"; -"roles";"/roles";"ScoView";"GET"; -"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin";"ScoView";"GET"; -"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/pdf";"ScoView";"GET"; -"test-pdf";"/etudiant/etudid/11/formsemestre/1/bulletin/short";"ScoView";"GET"; -"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"; +"formations_ids";"/formations_ids";"ScoView";"GET"; +"formation_get";"/formation/1";"ScoView";"GET"; +"formation_export_by_formation_id";"/formation/1/export";"ScoView";"GET"; +"referentiel_competences";"/formation/1/referentiel_competences";"ScoView";"GET"; +"formation_module_get";"/formation/module/1";"ScoView";"GET"; +"formsemestre_get";"/formsemestre/1";"ScoView";"GET"; +"bulletins";"/formsemestre/1/bulletins";"ScoView";"GET"; +"formsemestre_programme";"/formsemestre/1/programme";"ScoView";"GET"; +"formsemestre_etudiants";"/formsemestre/1/etudiants/query";"ScoView";"GET"; +"formsemestre_etat_evaluations";"/formsemestre/1/etat_evals";"ScoView";"GET"; +"formsemestre_resultat";"/formsemestre/1/resultats";"ScoView";"GET"; +"decisions_jury";"/formsemestre/1/decisions_jury";"ScoView";"GET"; +"justificatif";"/justificatif/1";"ScoView";"GET"; +"justificatifs";"/justificatifs/1";"ScoView";"GET"; +"justificatifs";"/justificatifs/1/query?etat=attente";"ScoView";"GET"; +"justificatifs_dept";"/justificatifs/dept/1";"ScoView";"GET"; +"justificatifs_formsemestre";"/justificatifs/formsemestre/1";"ScoView";"GET"; +"justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""etat"":""valide""}" +"justif_edit";"/justificatif/1/edit";"UsersAdmin";"POST";"{""raison"":""MEDIC""}" +"justif_delete";"/justificatif/delete";"UsersAdmin";"POST";"[2, 2, 3]" +"justif_list";"/justificatif/1/list";"ScoView";"GET"; +"justif_justifies";"/justificatif/1/justifies";"UsersAdmin";"GET"; +"logo_list_globals";"/logos";"UsersAdmin";"GET"; +"logo_get_global";"/logo/B";"UsersAdmin";"GET"; +"departement_logos";"/departement/TAPI/logos";"UsersAdmin";"GET"; +"moduleimpl_inscriptions";"/moduleimpl/1/inscriptions";"ScoView";"GET"; +"moduleimpl_notes";"/moduleimpl/1/notes";"ScoView";"GET"; +"partition_info";"/partition/1";"ScoView";"GET"; +"formsemestre_partitions";"/formsemestre/1/partitions";"ScoView";"GET"; +"group_etudiants";"/group/1/etudiants";"ScoView";"GET"; +"group_create";"/partition/1/group/create";"ScoView";"POST";"{""group_name"" : ""Nouveau Groupe""}" +"group_edit";"/group/1/edit";"ScoView";"POST";"{""group_name"":""A1""}" +"group_set_edt_id";"/group/1/set_edt_id/EDT_GR1";"ScoView";"POST"; +"partition_edit";"/partition/1/edit";"ScoView";"POST";"{""bul_show_rank"":1}" +"user_info";"/user/2";"UsersAdmin";"GET"; +"user_password";"/user/3/password";"UsersAdmin";"POST";"{""password"" : ""rePlaCemeNT456averylongandcomplicated""}" +"permissions_list";"/permissions";"UsersAdmin";"GET"; +"role_get";"/role/Ens";"UsersAdmin";"GET"; +"roles_list";"/roles";"UsersAdmin";"GET"; +"role_create";"/role/create/customRole";"UsersAdmin";"POST";"{""permissions"": [""ScoView"", ""UsersView""]}" +"role_delete";"/role/customRole/delete";"UsersAdmin";"POST"; \ No newline at end of file diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index adeaa7c33..f2e7463df 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -211,10 +211,17 @@ def create_formsemestre( ) db.session.add(modimpl) db.session.commit() + # Partition par défaut (requise): partition_id = sco_groups.partition_create( 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 From 64038687b774864a4a6b7143d401d36ddf3df4c1 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Sat, 27 Jul 2024 14:34:20 +0200 Subject: [PATCH 16/16] Fixes for unit tests --- tests/api/test_api_partitions.py | 4 +-- tests/conftest.py | 1 + .../results/formsemestre_resultat.json | 32 ++++++++++++++----- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/tests/api/test_api_partitions.py b/tests/api/test_api_partitions.py index 90f475eec..aa4681d4c 100644 --- a/tests/api/test_api_partitions.py +++ b/tests/api/test_api_partitions.py @@ -65,8 +65,8 @@ def test_formsemestre_partition(api_headers): headers = api_headers formsemestre_id = 1 partitions = GET(f"/formsemestre/{formsemestre_id}/partitions", headers=headers) - # au départ, pas de partitions - assert partitions == {} + # au départ, une partition (TD avec groupes A et B) + assert len(partitions) == 1 # --- Création partition partition_d = { "partition_name": "T&Dé", diff --git a/tests/conftest.py b/tests/conftest.py index 58ca1c111..24b00538f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ import pytest +import flask from flask import g from flask_login import login_user diff --git a/tests/ressources/results/formsemestre_resultat.json b/tests/ressources/results/formsemestre_resultat.json index 354324e7f..d6187aebc 100644 --- a/tests/ressources/results/formsemestre_resultat.json +++ b/tests/ressources/results/formsemestre_resultat.json @@ -50,8 +50,9 @@ "moy_sae_14_3": "17.83", "moy_sae_15_3": "~", "ues_validables": "3/3", - "nbabs": 0, + "nbabs": 1, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -109,8 +110,9 @@ "moy_sae_14_3": "10.74", "moy_sae_15_3": "~", "ues_validables": "3/3", - "nbabs": 2, + "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -170,6 +172,7 @@ "ues_validables": "2/3", "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -229,6 +232,7 @@ "ues_validables": "2/3", "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -286,8 +290,9 @@ "moy_sae_14_3": "11.09", "moy_sae_15_3": "~", "ues_validables": "1/3", - "nbabs": 3, + "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -347,6 +352,7 @@ "ues_validables": "2/3", "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -404,8 +410,9 @@ "moy_sae_14_3": "05.17", "moy_sae_15_3": "~", "ues_validables": "1/3", - "nbabs": 3, + "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -465,6 +472,7 @@ "ues_validables": "0/3", "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -524,6 +532,7 @@ "ues_validables": "0/3", "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -583,6 +592,7 @@ "ues_validables": "0/3", "nbabs": 0, "nbabsjust": 0, + "part_2": "", "code_cursus": "S1", "bac": "", "specialite": "", @@ -642,6 +652,7 @@ "ues_validables": "", "nbabs": 2, "nbabsjust": 0, + "part_2": "D\u00e9f.", "code_cursus": "S1", "bac": "", "specialite": "", @@ -699,8 +710,9 @@ "moy_sae_14_3": "", "moy_sae_15_3": "", "ues_validables": "", - "nbabs": 2, + "nbabs": 0, "nbabsjust": 0, + "part_2": "D\u00e9m.", "code_cursus": "S1", "bac": "", "specialite": "", @@ -758,8 +770,9 @@ "moy_sae_14_3": "", "moy_sae_15_3": "", "ues_validables": "", - "nbabs": 0, + "nbabs": 2, "nbabsjust": 0, + "part_2": "D\u00e9f.", "code_cursus": "S1", "bac": "", "specialite": "", @@ -817,8 +830,9 @@ "moy_sae_14_3": "", "moy_sae_15_3": "", "ues_validables": "", - "nbabs": 1, + "nbabs": 0, "nbabsjust": 0, + "part_2": "D\u00e9m.", "code_cursus": "S1", "bac": "", "specialite": "", @@ -878,6 +892,7 @@ "ues_validables": "", "nbabs": 0, "nbabsjust": 0, + "part_2": "D\u00e9f.", "code_cursus": "S1", "bac": "", "specialite": "", @@ -935,8 +950,9 @@ "moy_sae_14_3": "", "moy_sae_15_3": "", "ues_validables": "", - "nbabs": 0, + "nbabs": 1, "nbabsjust": 0, + "part_2": "D\u00e9m.", "code_cursus": "S1", "bac": "", "specialite": "",