From d37ce3f8d9826bbce7e0ebfd016f290d57cfaabb Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 24 Jul 2024 17:34:30 +0200 Subject: [PATCH] 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 160b00b7..e5d0a536 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 ea479b34..26e3ec43 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 8b4db4d3..92211542 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 3eafaffb..6e936b9b 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 93f17248..94b984a4 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 6feba7d6..1a1fd9e9 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 ef00cb84..aeec7982 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 1ec747a1..42f10513 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 e7999f71..1771927a 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 ef3ec149..61aaca6f 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 981c5a09..7822cc58 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 a243e500..261dcbe5 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 8bc96ffa..a323ff19 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 c6bb93c8..64127f6e 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,