Compare commits

...

3 Commits

15 changed files with 748 additions and 425 deletions

View File

@ -6,6 +6,11 @@
""" """
API : billets d'absences API : billets d'absences
CATEGORY
--------
Billets d'absence
""" """
from flask import g, request from flask import g, request
@ -29,7 +34,7 @@ from app.scodoc.sco_permissions import Permission
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def billets_absence_etudiant(etudid: int): def billets_absence_etudiant(etudid: int):
"""Liste des billets d'absence pour cet étudiant""" """Liste des billets d'absence pour cet étudiant."""
billets = sco_abs_billets.query_billets_etud(etudid) billets = sco_abs_billets.query_billets_etud(etudid)
return [billet.to_dict() for billet in billets] return [billet.to_dict() for billet in billets]
@ -41,7 +46,20 @@ def billets_absence_etudiant(etudid: int):
@permission_required(Permission.AbsAddBillet) @permission_required(Permission.AbsAddBillet)
@as_json @as_json
def billets_absence_create(): def billets_absence_create():
"""Ajout d'un billet d'absence""" """Ajout d'un billet d'absence. Renvoie le billet créé en json.
DATA
----
```json
{
"etudid" : int,
"abs_begin" : date_iso,
"abs_end" : date_iso,
"description" : string,
"justified" : bool
}
```
"""
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
etudid = data.get("etudid") etudid = data.get("etudid")
abs_begin = data.get("abs_begin") abs_begin = data.get("abs_begin")

View File

@ -9,6 +9,11 @@
Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/), Note: les routes /departement[s] sont publiées sur l'API (/ScoDoc/api/),
mais évidemment pas sur l'API web (/ScoDoc/<dept>/api). mais évidemment pas sur l'API web (/ScoDoc/<dept>/api).
CATEGORY
--------
Département
""" """
from datetime import datetime from datetime import datetime
@ -27,24 +32,13 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
def get_departement(dept_ident: str) -> Departement:
"Le departement, par id ou acronyme. Erreur 404 si pas trouvé."
try:
dept_id = int(dept_ident)
except ValueError:
dept_id = None
if dept_id is None:
return Departement.query.filter_by(acronym=dept_ident).first_or_404()
return Departement.query.get_or_404(dept_id)
@bp.route("/departements") @bp.route("/departements")
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departements_list(): def departements_list():
"""Liste les départements""" """Liste tous les départements."""
return [dept.to_dict(with_dept_name=True) for dept in Departement.query] return [dept.to_dict(with_dept_name=True) for dept in Departement.query]
@ -54,7 +48,7 @@ def departements_list():
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departements_ids(): def departements_ids():
"""Liste des ids de départements""" """Liste des ids de tous les départements."""
return [dept.id for dept in Departement.query] 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. Info sur un département. Accès par acronyme.
Exemple de résultat : Exemple de résultat :
```json
{ {
"id": 1, "id": 1,
"acronym": "TAPI", "acronym": "TAPI",
@ -76,6 +71,7 @@ def departement_by_acronym(acronym: str):
"visible": true, "visible": true,
"date_creation": "Fri, 15 Apr 2022 12:19:28 GMT" "date_creation": "Fri, 15 Apr 2022 12:19:28 GMT"
} }
```
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return dept.to_dict(with_dept_name=True) return dept.to_dict(with_dept_name=True)
@ -102,11 +98,15 @@ def departement_by_id(dept_id: int):
def departement_create(): def departement_create():
""" """
Création d'un département. Création d'un département.
The request content type should be "application/json": Le content type doit être `application/json`.
DATA
----
```json
{ {
"acronym": str, "acronym": str,
"visible":bool, "visible": bool,
} }
```
""" """
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
acronym = str(data.get("acronym", "")) acronym = str(data.get("acronym", ""))
@ -130,10 +130,12 @@ def departement_create():
@as_json @as_json
def departement_edit(acronym): def departement_edit(acronym):
""" """
Edition d'un département: seul visible peut être modifié Édition d'un département: seul le champ `visible` peut être modifié.
The request content type should be "application/json":
DATA
----
{ {
"visible":bool, "visible": bool,
} }
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
@ -155,7 +157,7 @@ def departement_edit(acronym):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def departement_delete(acronym): def departement_delete(acronym):
""" """
Suppression d'un département. Suppression d'un département identifié par son acronyme.
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
acronym = dept.acronym acronym = dept.acronym
@ -172,11 +174,14 @@ def departement_delete(acronym):
@as_json @as_json
def departement_etudiants(acronym: str): def departement_etudiants(acronym: str):
""" """
Retourne la liste des étudiants d'un département Retourne la liste des étudiants d'un département.
acronym: l'acronyme d'un département PARAMS
------
acronym : l'acronyme d'un département
Exemple de résultat : Exemple de résultat :
```json
[ [
{ {
"civilite": "M", "civilite": "M",
@ -191,6 +196,7 @@ def departement_etudiants(acronym: str):
}, },
... ...
] ]
```
""" """
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [etud.to_dict_short() for etud in dept.etudiants] return [etud.to_dict_short() for etud in dept.etudiants]
@ -215,7 +221,7 @@ def departement_etudiants_by_id(dept_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departement_formsemestres_ids(acronym: str): def departement_formsemestres_ids(acronym: str):
"""liste des ids formsemestre du département""" """Liste des ids de tous les formsemestres du département."""
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
return [formsemestre.id for formsemestre in dept.formsemestres] return [formsemestre.id for formsemestre in dept.formsemestres]
@ -226,7 +232,7 @@ def departement_formsemestres_ids(acronym: str):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def departement_formsemestres_ids_by_id(dept_id: int): def departement_formsemestres_ids_by_id(dept_id: int):
"""liste des ids formsemestre du département""" """Liste des ids de tous les formsemestres du département."""
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
return [formsemestre.id for formsemestre in dept.formsemestres] return [formsemestre.id for formsemestre in dept.formsemestres]
@ -239,7 +245,7 @@ def departement_formsemestres_ids_by_id(dept_id: int):
@as_json @as_json
def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None): def departement_formsemestres_courants(acronym: str = "", dept_id: int | None = None):
""" """
Liste les semestres du département indiqué (par son acronyme ou son id) Liste les formsemestres du département indiqué (par son acronyme ou son id)
contenant la date courante, ou à défaut celle indiquée en argument contenant la date courante, ou à défaut celle indiquée en argument
(au format ISO). (au format ISO).

View File

@ -6,6 +6,10 @@
""" """
API : accès aux étudiants API : accès aux étudiants
CATEGORY
--------
Étudiants
""" """
from datetime import datetime from datetime import datetime
from operator import attrgetter from operator import attrgetter
@ -38,9 +42,8 @@ from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_photos
from app.scodoc.sco_utils import json_error, suppress_accents from app.scodoc.sco_utils import json_error, suppress_accents
import app.scodoc.sco_photos as sco_photos
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
# Un exemple: # Un exemple:
@ -103,6 +106,7 @@ def etudiants_courants(long: bool = False):
date_courante:<string:date_courante> date_courante:<string:date_courante>
Exemple de résultat : Exemple de résultat :
```json
[ [
{ {
"id": 1234, "id": 1234,
@ -115,6 +119,7 @@ def etudiants_courants(long: bool = False):
} }
... ...
] ]
```
En format "long": voir documentation. 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é. Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
PARAMS
------
etudid : l'etudid de l'étudiant etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant nip : le code nip de l'étudiant
ine : le code ine de l'étudiant ine : le code ine de l'étudiant
`etudid` est unique dans la base (tous départements).
Les codes INE et NIP sont uniques au sein d'un département. Les codes INE et NIP sont uniques au sein d'un département.
Si plusieurs objets ont le même code, on ramène le plus récemment inscrit. Si plusieurs objets ont le même code, on ramène le plus récemment inscrit.
""" """
@ -197,6 +205,8 @@ def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = Non
----- -----
size:<string:size> size:<string:size>
PARAMS
------
etudid : l'etudid de l'étudiant etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant nip : le code nip de l'étudiant
ine : le code ine de l'étudiant ine : le code ine de l'étudiant
@ -269,9 +279,12 @@ def etudiant_set_photo_image(etudid: int = None):
@as_json @as_json
def etudiants(etudid: int = None, nip: str = None, ine: str = None): def etudiants(etudid: int = None, nip: str = None, ine: str = None):
""" """
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie Info sur le ou les étudiants correspondants.
toujours une liste.
Comme `/etudiant` mais renvoie toujours une liste.
Si non trouvé, liste vide, pas d'erreur. Si non trouvé, liste vide, pas d'erreur.
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.). été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
""" """
@ -304,8 +317,9 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def etudiants_by_name(start: str = "", min_len=3, limit=32): def etudiants_by_name(start: str = "", min_len=3, limit=32):
"""Liste des étudiants dont le nom débute par start. """Liste des étudiants dont le nom débute par `start`.
Si start fait moins de min_len=3 caractères, liste vide.
Si `start` fait moins de `min_len=3` caractères, liste vide.
La casse et les accents sont ignorés. La casse et les accents sont ignorés.
""" """
if len(start) < min_len: if len(start) < min_len:
@ -340,13 +354,13 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
@as_json @as_json
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None):
""" """
Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique. Liste des formsemestres qu'un étudiant a suivi, triés par ordre chronologique.
Accès par etudid, nip ou ine. Accès par etudid, nip ou ine.
Attention, si accès via NIP ou INE, les semestres peuvent être de départements Attention, si accès via NIP ou INE, les formsemestres peuvent être de départements
différents (si l'étudiant a changé de département). L'id du département est `dept_id`. différents (si l'étudiant a changé de département). L'id du département est `dept_id`.
Si accès par département, ne retourne que les formsemestre suivis dans le département. Si accès par département, ne retourne que les formsemestres suivis dans le département.
""" """
if etudid is not None: if etudid is not None:
q_etud = Identite.query.filter_by(id=etudid) q_etud = Identite.query.filter_by(id=etudid)
@ -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é Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
PARAMS
------
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant etudid : l'etudid d'un étudiant
Exemple de résultat : Exemple de résultat :
```json
[ [
{ {
"partition_id": 1, "partition_id": 1,
@ -503,6 +520,7 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
"group_name": "A" "group_name": "A"
} }
] ]
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -530,9 +548,12 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def etudiant_create(force=False): def etudiant_create(force=False):
"""Création d'un nouvel étudiant """Création d'un nouvel étudiant.
Si force, crée même si homonymie détectée. Si force, crée même si homonymie détectée.
L'étudiant créé n'est pas inscrit à un semestre. L'étudiant créé n'est pas inscrit à un semestre.
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme) Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
""" """
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
@ -602,7 +623,10 @@ def etudiant_edit(
): ):
"""Édition des données étudiant (identité, admission, adresses). """É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) ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
if not ok: if not ok:
@ -642,7 +666,23 @@ def etudiant_annotation(
code_type: str = "etudid", code_type: str = "etudid",
code: str = None, code: str = None,
): ):
"""Ajout d'une annotation sur un étudiant""" """Ajout d'une annotation sur un étudiant.
Renvoie l'annotation créée.
PARAMS
------
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
`code`: la valeur du code
DATA
----
```json
{
"comment" : string
}
```
"""
if not current_user.has_permission(Permission.ViewEtudData): if not current_user.has_permission(Permission.ViewEtudData):
return json_error(403, "non autorisé (manque ViewEtudData)") return json_error(403, "non autorisé (manque ViewEtudData)")
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
@ -679,7 +719,13 @@ def etudiant_annotation_delete(
code_type: str = "etudid", code: str = None, annotation_id: int = None code_type: str = "etudid", code: str = None, annotation_id: int = None
): ):
""" """
Suppression d'une annotation Suppression d'une annotation. On spécifie l'étudiant et l'id de l'annotation.
PARAMS
------
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
`code`: la valeur du code
`annotation_id` : id de l'annotation
""" """
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept) ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
if not ok: if not ok:

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux évaluations ScoDoc 9 API : accès aux évaluations
CATEGORY
--------
Évaluations
""" """
from flask import g, request from flask import g, request
from flask_json import as_json from flask_json import as_json
@ -32,24 +36,28 @@ import app.scodoc.sco_utils as scu
def get_evaluation(evaluation_id: int): def get_evaluation(evaluation_id: int):
"""Description d'une évaluation. """Description d'une évaluation.
DATA
----
```json
{ {
'coefficient': 1.0, 'coefficient': 1.0,
'date_debut': '2016-01-04T08:30:00', 'date_debut': '2016-01-04T08:30:00',
'date_fin': '2016-01-04T12:30:00', 'date_fin': '2016-01-04T12:30:00',
'description': 'TP NI9219 Température', 'description': 'TP Température',
'evaluation_type': 0, 'evaluation_type': 0,
'id': 15797, 'id': 15797,
'moduleimpl_id': 1234, 'moduleimpl_id': 1234,
'note_max': 20.0, 'note_max': 20.0,
'numero': 3, 'numero': 3,
'poids': { 'poids': {
'UE1.1': 1.0, 'UE1.1': 1.0,
'UE1.2': 1.0, 'UE1.2': 1.0,
'UE1.3': 1.0 'UE1.3': 1.0
}, },
'publish_incomplete': False, 'publish_incomplete': False,
'visibulletin': True 'visibulletin': True
} }
```
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -70,11 +78,13 @@ def get_evaluation(evaluation_id: int):
@as_json @as_json
def moduleimpl_evaluations(moduleimpl_id: int): def moduleimpl_evaluations(moduleimpl_id: int):
""" """
Retourne la liste des évaluations d'un moduleimpl Retourne la liste des évaluations d'un moduleimpl.
PARAMS
------
moduleimpl_id : l'id d'un moduleimpl moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat : voir /evaluation Exemple de résultat : voir `/evaluation`.
""" """
modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations] return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
@ -88,30 +98,36 @@ def moduleimpl_evaluations(moduleimpl_id: int):
@as_json @as_json
def evaluation_notes(evaluation_id: int): def evaluation_notes(evaluation_id: int):
""" """
Retourne la liste des notes de l'évaluation Retourne la liste des notes de l'évaluation.
PARAMS
------
evaluation_id : l'id de l'évaluation evaluation_id : l'id de l'évaluation
Exemple de résultat : Exemple de résultat :
{ ```json
"11": { {
"11": {
"etudid": 11, "etudid": 11,
"evaluation_id": 1, "evaluation_id": 1,
"value": 15.0, "value": 15.0,
"note_max" : 20.0,
"comment": "", "comment": "",
"date": "Wed, 20 Apr 2022 06:49:05 GMT", "date": "2024-07-19T19:08:44+02:00",
"uid": 2 "uid": 2
}, },
"12": { "12": {
"etudid": 12, "etudid": 12,
"evaluation_id": 1, "evaluation_id": 1,
"value": 12.0, "value": "ABS",
"note_max" : 20.0,
"comment": "", "comment": "",
"date": "Wed, 20 Apr 2022 06:49:06 GMT", "date": "2024-07-19T19:08:44+02:00",
"uid": 2 "uid": 2
}, },
... ...
} }
```
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -145,13 +161,18 @@ def evaluation_notes(evaluation_id: int):
@as_json @as_json
def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
"""Écriture de notes dans une évaluation. """Écriture de notes dans une évaluation.
The request content type should be "application/json",
and contains: DATA
----
```json
{ {
'notes' : [ [etudid, value], ... ], 'notes' : [ [etudid, value], ... ],
'comment' : optional string 'comment' : optional string
} }
Result: ```
Résultat:
- nb_changed: nombre de notes changées - nb_changed: nombre de notes changées
- nb_suppress: nombre de notes effacées - nb_suppress: nombre de notes effacées
- etudids_with_decision: liste des etudiants dont la note a changé - etudids_with_decision: liste des etudiants dont la note a changé
@ -186,8 +207,9 @@ def evaluation_set_notes(evaluation_id: int): # evaluation-notes-set
@as_json @as_json
def evaluation_create(moduleimpl_id: int): def evaluation_create(moduleimpl_id: int):
"""Création d'une évaluation. """Création d'une évaluation.
The request content type should be "application/json",
and contains: DATA
----
{ {
"description" : str, "description" : str,
"evaluation_type" : int, // {0,1,2} default 0 (normale) "evaluation_type" : int, // {0,1,2} default 0 (normale)
@ -200,7 +222,8 @@ def evaluation_create(moduleimpl_id: int):
"coefficient" : float, // si non spécifié, 1.0 "coefficient" : float, // si non spécifié, 1.0
"poids" : { ue_id : poids } // optionnel "poids" : { ue_id : poids } // optionnel
} }
Result: l'évaluation créée.
Résultat: l'évaluation créée.
""" """
moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id) moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
if not moduleimpl.can_edit_evaluation(current_user): if not moduleimpl.can_edit_evaluation(current_user):
@ -250,7 +273,7 @@ def evaluation_create(moduleimpl_id: int):
@as_json @as_json
def evaluation_delete(evaluation_id: int): def evaluation_delete(evaluation_id: int):
"""Suppression d'une évaluation. """Suppression d'une évaluation.
Efface aussi toutes ses notes Efface aussi toutes ses notes.
""" """
query = Evaluation.query.filter_by(id=evaluation_id) query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept: if g.scodoc_dept:

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux formations ScoDoc 9 API : accès aux formations
CATEGORY
--------
Formations
""" """
from flask import flash, g, request from flask import flash, g, request
@ -38,7 +42,8 @@ from app.scodoc.sco_permissions import Permission
@as_json @as_json
def formations(): def formations():
""" """
Retourne la liste de toutes les formations (tous départements) Retourne la liste de toutes les formations (tous départements,
sauf si route départementale).
""" """
query = Formation.query query = Formation.query
if g.scodoc_dept: if g.scodoc_dept:
@ -58,7 +63,7 @@ def formations_ids():
Retourne la liste de toutes les id de formations Retourne la liste de toutes les id de formations
(tous départements, ou du département indiqué dans la route) (tous départements, ou du département indiqué dans la route)
Exemple de résultat : [ 17, 99, 32 ] Exemple de résultat : `[ 17, 99, 32 ]`.
""" """
query = Formation.query query = Formation.query
if g.scodoc_dept: if g.scodoc_dept:
@ -74,24 +79,26 @@ def formations_ids():
@as_json @as_json
def formation_by_id(formation_id: int): 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 : Exemple de résultat :
{
"id": 1, ```json
"acronyme": "BUT R&amp;T", {
"titre_officiel": "Bachelor technologique réseaux et télécommunications", "id": 1,
"formation_code": "V1RET", "acronyme": "BUT R&amp;T",
"code_specialite": null, "titre_officiel": "Bachelor technologique réseaux et télécommunications",
"dept_id": 1, "formation_code": "V1RET",
"titre": "BUT R&amp;T", "code_specialite": null,
"version": 1, "dept_id": 1,
"type_parcours": 700, "titre": "BUT R&amp;T",
"referentiel_competence_id": null, "version": 1,
"formation_id": 1 "type_parcours": 700,
} "referentiel_competence_id": null,
"formation_id": 1
}
```
""" """
query = Formation.query.filter_by(id=formation_id) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -123,97 +130,102 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
""" """
Retourne la formation, avec UE, matières, modules Retourne la formation, avec UE, matières, modules
PARAMS
------
formation_id : l'id d'une formation formation_id : l'id d'une formation
export_ids : True ou False, si l'on veut ou non exporter les ids export_with_ids : si présent, exporte aussi les ids des objets ScoDoc de la formation.
Exemple de résultat : Exemple de résultat :
```json
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications",
"formation_code": "V1RET",
"code_specialite": null,
"dept_id": 1,
"titre": "BUT R&amp;T",
"version": 1,
"type_parcours": 700,
"referentiel_competence_id": null,
"formation_id": 1,
"ue": [
{ {
"id": 1, "acronyme": "RT1.1",
"acronyme": "BUT R&amp;T", "numero": 1,
"titre_officiel": "Bachelor technologique r\u00e9seaux et t\u00e9l\u00e9communications", "titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"formation_code": "V1RET", "type": 0,
"code_specialite": null, "ue_code": "UCOD11",
"dept_id": 1, "ects": 12.0,
"titre": "BUT R&amp;T", "is_external": false,
"version": 1, "code_apogee": "",
"type_parcours": 700, "coefficient": 0.0,
"referentiel_competence_id": null, "semestre_idx": 1,
"formation_id": 1, "color": "#B80004",
"ue": [ "reference": 1,
"matiere": [
{ {
"acronyme": "RT1.1", "titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"numero": 1, "numero": 1,
"titre": "Administrer les r\u00e9seaux et l\u2019Internet", "module": [
"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", "titre": "Initiation aux r\u00e9seaux informatiques",
"numero": 1, "abbrev": "Init aux r\u00e9seaux informatiques",
"module": [ "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", "ue_reference": "1",
"abbrev": "Init aux r\u00e9seaux informatiques", "coef": "12.0"
"code": "R101",
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"coefficient": 1.0,
"ects": "",
"semestre_id": 1,
"numero": 10,
"code_apogee": "",
"module_type": 2,
"coefficients": [
{
"ue_reference": "1",
"coef": "12.0"
},
{
"ue_reference": "2",
"coef": "4.0"
},
{
"ue_reference": "3",
"coef": "4.0"
}
]
}, },
{ {
"titre": "Se sensibiliser \u00e0 l&apos;hygi\u00e8ne informatique...", "ue_reference": "2",
"abbrev": "Hygi\u00e8ne informatique", "coef": "4.0"
"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": "3",
"coef": "4.0"
}
]
},
{
"titre": "Se sensibiliser \u00e0 l&apos;hygi\u00e8ne informatique...",
"abbrev": "Hygi\u00e8ne informatique",
"code": "SAE11",
"heures_cours": 0.0,
"heures_td": 0.0,
"heures_tp": 0.0,
"coefficient": 1.0,
"ects": "",
"semestre_id": 1,
"numero": 10,
"code_apogee": "",
"module_type": 3,
"coefficients": [
{
"ue_reference": "1",
"coef": "16.0"
}
]
}, },
... ...
] ]
}, },
] ...
} ]
},
]
}
```
""" """
query = Formation.query.filter_by(id=formation_id) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -236,11 +248,8 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
@as_json @as_json
def referentiel_competences(formation_id: int): def referentiel_competences(formation_id: int):
""" """
Retourne le référentiel de compétences Retourne le référentiel de compétences de la formation
ou null si pas de référentiel associé.
formation_id : l'id d'une formation
return null si pas de référentiel associé.
""" """
query = Formation.query.filter_by(id=formation_id) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -259,8 +268,14 @@ def referentiel_competences(formation_id: int):
@as_json @as_json
def ue_set_parcours(ue_id: int): def ue_set_parcours(ue_id: int):
"""Associe UE et parcours BUT. """Associe UE et parcours BUT.
La liste des ids de parcours est passée en argument JSON. La liste des ids de parcours est passée en argument JSON.
JSON arg: [parcour_id1, parcour_id2, ...]
DATA
----
```json
[ parcour_id1, parcour_id2, ... ]
```
""" """
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -293,7 +308,7 @@ def ue_set_parcours(ue_id: int):
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
@as_json @as_json
def ue_assoc_niveau(ue_id: int, niveau_id: int): def ue_assoc_niveau(ue_id: int, niveau_id: int):
"""Associe l'UE au niveau de compétence""" """Associe l'UE au niveau de compétence."""
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -323,7 +338,7 @@ def ue_assoc_niveau(ue_id: int, niveau_id: int):
@as_json @as_json
def ue_desassoc_niveau(ue_id: int): def ue_desassoc_niveau(ue_id: int):
"""Désassocie cette UE de son niveau de compétence """Désassocie cette UE de son niveau de compétence
(si elle n'est pas associée, ne fait rien) (si elle n'est pas associée, ne fait rien).
""" """
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -345,7 +360,7 @@ def ue_desassoc_niveau(ue_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def get_ue(ue_id: int): def get_ue(ue_id: int):
"""Renvoie l'UE""" """Renvoie l'UE."""
query = UniteEns.query.filter_by(id=ue_id) query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -359,7 +374,7 @@ def get_ue(ue_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def formation_module_get(module_id: int): def formation_module_get(module_id: int):
"""Renvoie le module""" """Renvoie le module."""
query = Module.query.filter_by(id=module_id) query = Module.query.filter_by(id=module_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
@ -390,15 +405,17 @@ def formation_module_get(module_id: int):
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""): def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
"""Change le code Apogée de l'UE. """Change le code Apogée de l'UE.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Si ue_id n'est pas spécifié, utilise l'argument oid du POST. Ce changement peut être fait sur formation verrouillée.
Si code_apogee n'est pas spécifié ou vide,
utilise l'argument value du POST
Le retour est une chaîne (le code enregistré), pas json. Si `ue_id` n'est pas spécifié, utilise l'argument oid du POST.
Si `code_apogee` n'est pas spécifié ou vide,
utilise l'argument value du POST.
Le retour est une chaîne (le code enregistré), pas du json.
""" """
if ue_id is None: if ue_id is None:
ue_id = request.form.get("oid") ue_id = request.form.get("oid")
@ -444,14 +461,16 @@ def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""): def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
"""Change le code Apogée du RCUE de l'UE. """Change le code Apogée du RCUE de l'UE.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Ce changement peut être fait sur formation verrouillée.
Si code_apogee n'est pas spécifié ou vide, Si code_apogee n'est pas spécifié ou vide,
utilise l'argument value du POST (utilisé par jinplace.js) utilise l'argument value du POST (utilisé par `jinplace.js`)
Le retour est une chaîne (le code enregistré), pas json. Le retour est une chaîne (le code enregistré), pas du json.
""" """
if not code_apogee: if not code_apogee:
code_apogee = request.form.get("value", "") code_apogee = request.form.get("value", "")
@ -497,15 +516,17 @@ def formation_module_set_code_apogee(
module_id: int | None = None, code_apogee: str = "" module_id: int | None = None, code_apogee: str = ""
): ):
"""Change le code Apogée du module. """Change le code Apogée du module.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Si module_id n'est pas spécifié, utilise l'argument oid du POST. Ce changement peut être fait sur formation verrouillée.
Si code_apogee n'est pas spécifié ou vide,
Si `module_id` n'est pas spécifié, utilise l'argument `oid` du POST.
Si `code_apogee` n'est pas spécifié ou vide,
utilise l'argument value du POST (utilisé par jinplace.js) utilise l'argument value du POST (utilisé par jinplace.js)
Le retour est une chaîne (le code enregistré), pas json. Le retour est une chaîne (le code enregistré), pas du json.
""" """
if module_id is None: if module_id is None:
module_id = request.form.get("oid") module_id = request.form.get("oid")

View File

@ -6,6 +6,12 @@
""" """
ScoDoc 9 API : accès aux formsemestres ScoDoc 9 API : accès aux formsemestres
CATEGORY
--------
FormSemestre
""" """
from operator import attrgetter, itemgetter from operator import attrgetter, itemgetter
@ -55,36 +61,37 @@ def formsemestre_infos(formsemestre_id: int):
formsemestre_id : l'id du formsemestre formsemestre_id : l'id du formsemestre
Exemple de résultat : Exemple de résultat :
{ ```json
"block_moyennes": false, {
"bul_bgcolor": "white", "block_moyennes": false,
"bul_hide_xml": false, "bul_bgcolor": "white",
"date_debut_iso": "2021-09-01", "bul_hide_xml": false,
"date_debut": "01/09/2021", "date_debut_iso": "2021-09-01",
"date_fin_iso": "2022-08-31", "date_debut": "01/09/2021",
"date_fin": "31/08/2022", "date_fin_iso": "2022-08-31",
"dept_id": 1, "date_fin": "31/08/2022",
"elt_annee_apo": null, "dept_id": 1,
"elt_passage_apo" : null, "elt_annee_apo": null,
"elt_sem_apo": null, "elt_passage_apo" : null,
"ens_can_edit_eval": false, "elt_sem_apo": null,
"etat": true, "ens_can_edit_eval": false,
"formation_id": 1, "etat": true,
"formsemestre_id": 1, "formation_id": 1,
"gestion_compensation": false, "formsemestre_id": 1,
"gestion_semestrielle": false, "gestion_compensation": false,
"id": 1, "gestion_semestrielle": false,
"modalite": "FI", "id": 1,
"resp_can_change_ens": true, "modalite": "FI",
"resp_can_edit": false, "resp_can_change_ens": true,
"responsables": [1, 99], // uids "resp_can_edit": false,
"scodoc7_id": null, "responsables": [1, 99], // uids
"semestre_id": 1, "scodoc7_id": null,
"titre_formation" : "BUT GEA", "semestre_id": 1,
"titre_num": "BUT GEA semestre 1", "titre_formation" : "BUT GEA",
"titre": "BUT GEA", "titre_num": "BUT GEA semestre 1",
} "titre": "BUT GEA",
}
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -101,8 +108,8 @@ def formsemestre_infos(formsemestre_id: int):
@as_json @as_json
def formsemestres_query(): def formsemestres_query():
""" """
Retourne les formsemestres filtrés par Retourne les formsemestres filtrés par étape Apogée ou année scolaire
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant ou département (acronyme ou id) ou état ou code étudiant.
PARAMS PARAMS
------ ------
@ -192,7 +199,36 @@ def formsemestres_query():
@permission_required(Permission.EditFormSemestre) @permission_required(Permission.EditFormSemestre)
@as_json @as_json
def formsemestre_edit(formsemestre_id: int): def formsemestre_edit(formsemestre_id: int):
"""Modifie les champs d'un formsemestre.""" """Modifie les champs d'un formsemestre.
On peut spécifier un ou plusieurs champs.
DATA
---
```json
{
"semestre_id" : string,
"titre" : string,
"date_debut" : date iso,
"date_fin" : date iso,
"edt_id" : string,
"etat" : string,
"modalite" : string,
"gestion_compensation" : bool,
"bul_hide_xml" : bool,
"block_moyennes" : bool,
"block_moyenne_generale" : bool,
"mode_calcul_moyennes" : string,
"gestion_semestrielle" : string,
"bul_bgcolor" : string,
"resp_can_edit" : bool,
"resp_can_change_ens" : bool,
"ens_can_edit_eval" : bool,
"elt_sem_apo" : string,
"elt_annee_apo : string,
}
```
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
editable_keys = { editable_keys = {
@ -230,13 +266,19 @@ def formsemestre_edit(formsemestre_id: int):
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_apo_etapes(): def formsemestre_set_apo_etapes():
"""Change les codes étapes du semestre indiqué. """Change les codes étapes du semestre indiqué.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé
oid=int, le formsemestre_id
value=chaine "V1RT, V1RT2", codes séparés par des virgules DATA
----
```json
{
oid : int, le formsemestre_id
value : string, eg "V1RT, V1RT2", codes séparés par des virgules
}
""" """
formsemestre_id = int(request.form.get("oid")) formsemestre_id = int(request.form.get("oid"))
etapes_apo_str = request.form.get("value") etapes_apo_str = request.form.get("value")
@ -267,13 +309,20 @@ def formsemestre_set_apo_etapes():
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_elt_sem_apo(): def formsemestre_set_elt_sem_apo():
"""Change les codes étapes du semestre indiqué. """Change les codes étapes du semestre indiqué.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules DATA
----
```json
{
oid : int, le formsemestre_id
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
}
```
""" """
oid = int(request.form.get("oid")) oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip() value = (request.form.get("value") or "").strip()
@ -295,13 +344,20 @@ def formsemestre_set_elt_sem_apo():
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_elt_annee_apo(): def formsemestre_set_elt_annee_apo():
"""Change les codes étapes du semestre indiqué (par le champ oid). """Change les codes étapes du semestre indiqué (par le champ oid).
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules DATA
----
```json
{
oid : int, le formsemestre_id
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
}
```
""" """
oid = int(request.form.get("oid")) oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip() value = (request.form.get("value") or "").strip()
@ -323,13 +379,20 @@ def formsemestre_set_elt_annee_apo():
@permission_required(Permission.EditApogee) @permission_required(Permission.EditApogee)
def formsemestre_set_elt_passage_apo(): def formsemestre_set_elt_passage_apo():
"""Change les codes apogée de passage du semestre indiqué (par le champ oid). """Change les codes apogée de passage du semestre indiqué (par le champ oid).
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules. par des virgules.
(Ce changement peut être fait sur un semestre verrouillé)
Args: Ce changement peut être fait sur un semestre verrouillé.
oid=int, le formsemestre_id
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules DATA
----
```json
{
oid : int, le formsemestre_id
value : string, eg "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
}
```
""" """
oid = int(request.form.get("oid")) oid = int(request.form.get("oid"))
value = (request.form.get("value") or "").strip() value = (request.form.get("value") or "").strip()
@ -355,9 +418,12 @@ def formsemestre_set_elt_passage_apo():
@as_json @as_json
def bulletins(formsemestre_id: int, version: str = "long"): def bulletins(formsemestre_id: int, version: str = "long"):
""" """
Retourne les bulletins d'un formsemestre donné Retourne les bulletins d'un formsemestre.
formsemestre_id : l'id d'un formesemestre PARAMS
------
formsemestre_id : int
version : string ("long", "short", "selectedevals")
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin 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 Retourne la liste des UEs, ressources et SAEs d'un semestre
formsemestre_id : l'id d'un formsemestre
Exemple de résultat : Exemple de résultat :
```json
{
"ues": [
{ {
"ues": [ "type": 0,
{ "formation_id": 1,
"type": 0, "ue_code": "UCOD11",
"formation_id": 1, "id": 1,
"ue_code": "UCOD11", "ects": 12.0,
"id": 1, "acronyme": "RT1.1",
"ects": 12.0, "is_external": false,
"acronyme": "RT1.1", "numero": 1,
"is_external": false, "code_apogee": "",
"numero": 1, "titre": "Administrer les r\u00e9seaux et l\u2019Internet",
"code_apogee": "", "coefficient": 0.0,
"titre": "Administrer les r\u00e9seaux et l\u2019Internet", "semestre_idx": 1,
"coefficient": 0.0, "color": "#B80004",
"semestre_idx": 1, "ue_id": 1
"color": "#B80004", },
"ue_id": 1 ...
}, ],
... "ressources": [
], {
"ressources": [ "ens": [ 10, 18 ],
{ "formsemestre_id": 1,
"ens": [ 10, 18 ], "id": 15,
"formsemestre_id": 1, "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, "id": 15,
"module": { "matiere_id": 3,
"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, "module_id": 15,
"moduleimpl_id": 15, "module_type": 3,
"responsable_id": 2 "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) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -567,9 +634,9 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
""" """
Informations sur l'état des évaluations d'un formsemestre. Informations sur l'état des évaluations d'un formsemestre.
formsemestre_id : l'id d'un semestre
Exemple de résultat : Exemple de résultat :
```json
[ [
{ {
"id": 1, // moduleimpl_id "id": 1, // moduleimpl_id
@ -597,6 +664,7 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
] ]
}, },
] ]
```
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
@ -671,7 +739,8 @@ def formsemestre_etat_evaluations(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_resultat(formsemestre_id: int): def formsemestre_resultat(formsemestre_id: int):
"""Tableau récapitulatif des résultats """Tableau récapitulatif des résultats.
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules. Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
Si `format=raw`, ne converti pas les valeurs. Si `format=raw`, ne converti pas les valeurs.
@ -726,7 +795,7 @@ def formsemestre_resultat(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def groups_get_auto_assignment(formsemestre_id: int): def groups_get_auto_assignment(formsemestre_id: int):
"""rend les données stockées par""" """Rend les données stockées par `groups_save_auto_assignment`."""
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
@ -747,12 +816,17 @@ def groups_get_auto_assignment(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def groups_save_auto_assignment(formsemestre_id: int): def groups_save_auto_assignment(formsemestre_id: int):
"""enregistre les données""" """Enregistre les données, associées à ce formsemestre.
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
"""
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
if not formsemestre.can_change_groups():
return json_error(403, "non autorisé (can_change_groups)")
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX: if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
return json_error(413, "data too large") return json_error(413, "data too large")
formsemestre.groups_auto_assignment_data = request.data formsemestre.groups_auto_assignment_data = request.data
@ -767,17 +841,16 @@ def groups_save_auto_assignment(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_edt(formsemestre_id: int): def formsemestre_edt(formsemestre_id: int):
"""l'emploi du temps du semestre. """L'emploi du temps du semestre.
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur. Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
group_ids permet de filtrer sur les groupes ScoDoc. Expérimental, ne pas utiliser hors ScoDoc.
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
QUERY QUERY
----- -----
group_ids:<string:group_ids> group_ids : string (optionnel) filtre sur les groupes ScoDoc.
show_modules_titles:<bool:show_modules_titles> show_modules_titles: show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:

View File

@ -5,7 +5,12 @@
############################################################################## ##############################################################################
""" """
ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions.
CATEGORY
--------
Jury
""" """
import datetime import datetime
@ -91,7 +96,7 @@ def _news_delete_jury_etud(etud: Identite, detail: str = ""):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def validation_ue_delete(etudid: int, validation_id: int): def validation_ue_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation d'UE."
return _validation_ue_delete(etudid, validation_id) return _validation_ue_delete(etudid, validation_id)
@ -108,7 +113,7 @@ def validation_ue_delete(etudid: int, validation_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def validation_formsemestre_delete(etudid: int, validation_id: int): def validation_formsemestre_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation de semestre."
# c'est la même chose (formations classiques) # c'est la même chose (formations classiques)
return _validation_ue_delete(etudid, validation_id) return _validation_ue_delete(etudid, validation_id)
@ -160,7 +165,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def autorisation_inscription_delete(etudid: int, validation_id: int): def autorisation_inscription_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette autorisation d'inscription."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404
@ -189,8 +194,12 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
@as_json @as_json
def validation_rcue_record(etudid: int): def validation_rcue_record(etudid: int):
"""Enregistre une validation de RCUE. """Enregistre une validation de RCUE.
Si une validation existe déjà pour ce RCUE, la remplace. Si une validation existe déjà pour ce RCUE, la remplace.
The request content type should be "application/json":
DATA
----
```json
{ {
"code" : str, "code" : str,
"ue1_id" : int, "ue1_id" : int,
@ -200,6 +209,7 @@ def validation_rcue_record(etudid: int):
"date" : date_iso, // si non spécifié, now() "date" : date_iso, // si non spécifié, now()
"parcours_id" :int, "parcours_id" :int,
} }
```
""" """
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
@ -314,7 +324,7 @@ def validation_rcue_record(etudid: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def validation_rcue_delete(etudid: int, validation_id: int): def validation_rcue_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation de RCUE."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404
@ -342,7 +352,7 @@ def validation_rcue_delete(etudid: int, validation_id: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def validation_annee_but_delete(etudid: int, validation_id: int): def validation_annee_but_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation d'année BUT."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404
@ -371,7 +381,7 @@ def validation_annee_but_delete(etudid: int, validation_id: int):
@permission_required(Permission.EtudInscrit) @permission_required(Permission.EtudInscrit)
@as_json @as_json
def validation_dut120_delete(etudid: int, validation_id: int): def validation_dut120_delete(etudid: int, validation_id: int):
"Efface cette validation" "Efface cette validation de DUT120."
etud = tools.get_etud(etudid) etud = tools.get_etud(etudid)
if etud is None: if etud is None:
return "étudiant inconnu", 404 return "étudiant inconnu", 404

View File

@ -50,7 +50,7 @@ from app.scodoc.sco_utils import json_error
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def logo_list_globals(): def logo_list_globals():
"""Liste tous les logos""" """Liste des noms des logos définis pour le site ScoDoc."""
logos = list_logos()[None] logos = list_logos()[None]
return list(logos.keys()) return list(logos.keys())
@ -59,6 +59,11 @@ def logo_list_globals():
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def logo_get_global(logoname): def logo_get_global(logoname):
"""Renvoie le logo global de nom donné.
L'image est au format png ou jpg; le format retourné dépend du format sous lequel
l'image a été initialement enregistrée.
"""
logo = find_logo(logoname=logoname) logo = find_logo(logoname=logoname)
if logo is None: if logo is None:
return json_error(404, message="logo not found") return json_error(404, message="logo not found")
@ -80,6 +85,9 @@ def _core_get_logos(dept_id) -> list:
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def logo_get_local_by_acronym(departement): 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 dept_id = Departement.from_acronym(departement).id
return _core_get_logos(dept_id) return _core_get_logos(dept_id)
@ -89,6 +97,9 @@ def logo_get_local_by_acronym(departement):
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
@as_json @as_json
def logo_get_local_by_id(dept_id): 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) return _core_get_logos(dept_id)
@ -108,6 +119,12 @@ def _core_get_logo(dept_id, logoname) -> Response:
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def logo_get_local_dept_by_acronym(departement, logoname): def logo_get_local_dept_by_acronym(departement, logoname):
"""Le logo: image (format png ou jpg).
**Exemple d'utilisation:**
* `/ScoDoc/api/departement/MMI/logo/header`
"""
dept_id = Departement.from_acronym(departement).id dept_id = Departement.from_acronym(departement).id
return _core_get_logo(dept_id, logoname) return _core_get_logo(dept_id, logoname)
@ -116,4 +133,10 @@ def logo_get_local_dept_by_acronym(departement, logoname):
@scodoc @scodoc
@permission_required(Permission.ScoSuperAdmin) @permission_required(Permission.ScoSuperAdmin)
def logo_get_local_dept_by_id(dept_id, logoname): def logo_get_local_dept_by_id(dept_id, logoname):
"""Le logo: image (format png ou jpg).
**Exemple d'utilisation:**
* `/ScoDoc/api/departement/id/3/logo/header`
"""
return _core_get_logo(dept_id, logoname) return _core_get_logo(dept_id, logoname)

View File

@ -6,6 +6,10 @@
""" """
ScoDoc 9 API : accès aux moduleimpl ScoDoc 9 API : accès aux moduleimpl
CATEGORY
--------
ModuleImpl
""" """
from flask_json import as_json from flask_json import as_json
@ -28,38 +32,43 @@ from app.scodoc.sco_permissions import Permission
@as_json @as_json
def moduleimpl(moduleimpl_id: int): def moduleimpl(moduleimpl_id: int):
""" """
Retourne un moduleimpl en fonction de son id Retourne le moduleimpl.
PARAMS
------
moduleimpl_id : l'id d'un moduleimpl moduleimpl_id : l'id d'un moduleimpl
Exemple de résultat : 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, "id": 1,
"formsemestre_id": 1, "ects": null,
"module_id": 1, "abbrev": "Init aux réseaux informatiques",
"responsable_id": 2, "ue_id": 1,
"moduleimpl_id": 1, "code": "R101",
"ens": [], "formation_id": 1,
"module": { "heures_cours": 0,
"heures_tp": 0, "matiere_id": 1,
"code_apogee": "", "heures_td": 0,
"titre": "Initiation aux réseaux informatiques", "semestre_id": 1,
"coefficient": 1, "numero": 10,
"module_type": 2, "module_id": 1
"id": 1,
"ects": null,
"abbrev": "Init aux réseaux informatiques",
"ue_id": 1,
"code": "R101",
"formation_id": 1,
"heures_cours": 0,
"matiere_id": 1,
"heures_td": 0,
"semestre_id": 1,
"numero": 10,
"module_id": 1
}
} }
}
```
""" """
modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return modimpl.to_dict(convert_objects=True) return modimpl.to_dict(convert_objects=True)
@ -72,16 +81,20 @@ def moduleimpl(moduleimpl_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def moduleimpl_inscriptions(moduleimpl_id: int): def moduleimpl_inscriptions(moduleimpl_id: int):
"""Liste des inscriptions à ce moduleimpl """Liste des inscriptions à ce moduleimpl.
Exemple de résultat : Exemple de résultat :
[
{ ```json
"id": 1, [
"etudid": 666, {
"moduleimpl_id": 1234, "id": 1,
}, "etudid": 666,
... "moduleimpl_id": 1234,
] },
...
]
```
""" """
modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
return [i.to_dict() for i in modimpl.inscriptions] return [i.to_dict() for i in modimpl.inscriptions]
@ -93,22 +106,26 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def moduleimpl_notes(moduleimpl_id: int): def moduleimpl_notes(moduleimpl_id: int):
"""Liste des notes dans ce moduleimpl """Liste des notes dans ce moduleimpl.
Exemple de résultat : Exemple de résultat :
[
{ ```json
"etudid": 17776, // code de l'étudiant [
"nom": "DUPONT", {
"prenom": "Luz", "etudid": 17776, // code de l'étudiant
"38411": 16.0, // Note dans l'évaluation d'id 38411 "nom": "DUPONT",
"38410": 15.0, "prenom": "Luz",
"moymod": 15.5, // Moyenne INDICATIVE module "38411": 16.0, // Note dans l'évaluation d'id 38411
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875 "38410": 15.0,
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876 "moymod": 15.5, // Moyenne INDICATIVE module
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877 "moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
}, "moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
... "moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
] },
...
]
```
""" """
modimpl = ModuleImpl.get_modimpl(moduleimpl_id) modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
app.set_sco_dept(modimpl.formsemestre.departement.acronym) app.set_sco_dept(modimpl.formsemestre.departement.acronym)

View File

@ -6,6 +6,11 @@
""" """
ScoDoc 9 API : partitions ScoDoc 9 API : partitions
CATEGORY
--------
Groupes et Partitions
""" """
from operator import attrgetter from operator import attrgetter
@ -41,7 +46,8 @@ def partition_info(partition_id: int):
"""Info sur une partition. """Info sur une partition.
Exemple de résultat : Exemple de résultat :
```
```json
{ {
'bul_show_rank': False, 'bul_show_rank': False,
'formsemestre_id': 39, 'formsemestre_id': 39,
@ -71,10 +77,11 @@ def partition_info(partition_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_partitions(formsemestre_id: int): def formsemestre_partitions(formsemestre_id: int):
"""Liste de toutes les partitions d'un formsemestre """Liste de toutes les partitions d'un formsemestre.
formsemestre_id : l'id d'un formsemestre Exemple de résultat :
```json
{ {
partition_id : { partition_id : {
"bul_show_rank": False, "bul_show_rank": False,
@ -88,7 +95,7 @@ def formsemestre_partitions(formsemestre_id: int):
}, },
... ...
} }
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -112,9 +119,14 @@ def group_etudiants(group_id: int):
""" """
Retourne la liste des étudiants dans un groupe Retourne la liste des étudiants dans un groupe
(inscrits au groupe et inscrits au semestre). (inscrits au groupe et inscrits au semestre).
PARAMS
------
group_id : l'id d'un groupe group_id : l'id d'un groupe
Exemple de résultat : Exemple de résultat :
```json
[ [
{ {
'civilite': 'M', 'civilite': 'M',
@ -127,6 +139,7 @@ def group_etudiants(group_id: int):
}, },
... ...
] ]
```
""" """
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -152,11 +165,11 @@ def group_etudiants(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_etudiants_query(group_id: int): def group_etudiants_query(group_id: int):
"""Étudiants du groupe, filtrés par état (aucun, I, D, DEF) """Étudiants du groupe, filtrés par état (aucun, `I`, `D`, `DEF`)
QUERY QUERY
----- -----
etat:<string:etat> etat : string
""" """
etat = request.args.get("etat") etat = request.args.get("etat")
@ -186,7 +199,7 @@ def group_etudiants_query(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_set_etudiant(group_id: int, etudid: int): def group_set_etudiant(group_id: int, etudid: int):
"""Affecte l'étudiant au groupe indiqué""" """Affecte l'étudiant au groupe indiqué."""
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -248,7 +261,8 @@ def group_remove_etud(group_id: int, etudid: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_remove_etud(partition_id: int, etudid: int): def partition_remove_etud(partition_id: int, etudid: int):
"""Enlève l'étudiant de tous les groupes de cette partition """Enlève l'étudiant de tous les groupes de cette partition.
(NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition) (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
""" """
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
@ -293,12 +307,15 @@ def partition_remove_etud(partition_id: int, etudid: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_create(partition_id: int): # partition-group-create def group_create(partition_id: int): # partition-group-create
"""Création d'un groupe dans une partition """Création d'un groupe dans une partition.
The request content type should be "application/json": DATA
----
```json
{ {
"group_name" : nom_du_groupe, "group_name" : nom_du_groupe,
} }
```
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -345,7 +362,7 @@ def group_create(partition_id: int): # partition-group-create
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_delete(group_id: int): def group_delete(group_id: int):
"""Suppression d'un groupe""" """Suppression d'un groupe."""
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
query = ( query = (
@ -374,7 +391,7 @@ def group_delete(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_edit(group_id: int): def group_edit(group_id: int):
"""Edit a group""" """Édition d'un groupe."""
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
query = ( query = (
@ -415,9 +432,10 @@ def group_edit(group_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def group_set_edt_id(group_id: int, edt_id: str): def group_set_edt_id(group_id: int, edt_id: str):
"""Set edt_id for this group. """Set edt_id du groupe.
Contrairement à /edit, peut-être changé pour toute partition
ou formsemestre non verrouillé. Contrairement à `/edit`, peut-être changé pour toute partition
d'un formsemestre non verrouillé.
""" """
query = GroupDescr.query.filter_by(id=group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -443,16 +461,19 @@ def group_set_edt_id(group_id: int, edt_id: str):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_create(formsemestre_id: int): def partition_create(formsemestre_id: int):
"""Création d'une partition dans un semestre """Création d'une partition dans un semestre.
The request content type should be "application/json": DATA
----
```json
{ {
"partition_name": str, "partition_name": str,
"numero":int, "numero": int,
"bul_show_rank":bool, "bul_show_rank": bool,
"show_in_lists":bool, "show_in_lists": bool,
"groups_editable":bool "groups_editable": bool
} }
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -508,8 +529,13 @@ def partition_create(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def formsemestre_set_partitions_order(formsemestre_id: int): def formsemestre_set_partitions_order(formsemestre_id: int):
"""Modifie l'ordre des partitions du formsemestre """Modifie l'ordre des partitions du formsemestre.
JSON args: [partition_id1, partition_id2, ...]
DATA
----
```json
[ partition_id1, partition_id2, ... ]
```
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -520,7 +546,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
if not formsemestre.can_change_groups(): if not formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(401, "opération non autorisée")
partition_ids = request.get_json(force=True) # may raise 400 Bad Request partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, int) and not all( if not isinstance(partition_ids, list) and not all(
isinstance(x, int) for x in partition_ids isinstance(x, int) for x in partition_ids
): ):
return json_error( return json_error(
@ -549,8 +575,13 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_order_groups(partition_id: int): def partition_order_groups(partition_id: int):
"""Modifie l'ordre des groupes de la partition """Modifie l'ordre des groupes de la partition.
JSON args: [group_id1, group_id2, ...]
DATA
----
```json
[ group_id1, group_id2, ... ]
```
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -561,7 +592,7 @@ def partition_order_groups(partition_id: int):
if not partition.formsemestre.can_change_groups(): if not partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(401, "opération non autorisée")
group_ids = request.get_json(force=True) # may raise 400 Bad Request group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, int) and not all( if not isinstance(group_ids, list) and not all(
isinstance(x, int) for x in group_ids isinstance(x, int) for x in group_ids
): ):
return json_error( return json_error(
@ -586,10 +617,13 @@ def partition_order_groups(partition_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def partition_edit(partition_id: int): def partition_edit(partition_id: int):
"""Modification d'une partition dans un semestre """Modification d'une partition dans un semestre.
The request content type should be "application/json" Tous les champs sont optionnels.
All fields are optional:
DATA
----
```json
{ {
"partition_name": str, "partition_name": str,
"numero":int, "numero":int,
@ -597,6 +631,7 @@ def partition_edit(partition_id: int):
"show_in_lists":bool, "show_in_lists":bool,
"groups_editable":bool "groups_editable":bool
} }
```
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept: if g.scodoc_dept:
@ -660,9 +695,9 @@ def partition_edit(partition_id: int):
def partition_delete(partition_id: int): def partition_delete(partition_id: int):
"""Suppression d'une partition (et de tous ses groupes). """Suppression d'une partition (et de tous ses groupes).
Note 1: La partition par défaut (tous les étudiants du sem.) ne peut * Note 1: La partition par défaut (tous les étudiants du sem.) ne peut
pas être supprimée. pas être supprimée.
Note 2: Si la partition de parcours est supprimée, les étudiants * Note 2: Si la partition de parcours est supprimée, les étudiants
sont désinscrits des parcours. sont désinscrits des parcours.
""" """
query = Partition.query.filter_by(id=partition_id) query = Partition.query.filter_by(id=partition_id)

View File

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

View File

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

View File

@ -52,6 +52,17 @@ class Departement(db.Model):
def __repr__(self): def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>" return f"<{self.__class__.__name__}(id={self.id}, acronym='{self.acronym}')>"
@classmethod
def get_departement(cls, dept_ident: str | int) -> "Departement":
"Le département, par id ou acronyme. Erreur 404 si pas trouvé."
try:
dept_id = int(dept_ident)
except ValueError:
dept_id = None
if dept_id is None:
return cls.query.filter_by(acronym=dept_ident).first_or_404()
return cls.query.get_or_404(dept_id)
def to_dict(self, with_dept_name=True, with_dept_preferences=False): def to_dict(self, with_dept_name=True, with_dept_preferences=False):
data = { data = {
"id": self.id, "id": self.id,

View File

@ -1,3 +1,4 @@
{# Template pour la doc mardown d'un point d'entrée de l'API #}
#### **`{{doc.nom}}`** #### **`{{doc.nom}}`**
{% if doc.routes %} {% if doc.routes %}
@ -6,7 +7,7 @@
{% else %} {% else %}
* **Routes:** * **Routes:**
{% for route in doc.routes %} {% for route in doc.routes %}
* `{{route|safe}}` * `{{route|safe}}`
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endif %} {% endif %}
@ -15,7 +16,7 @@
{% if doc.params %} {% if doc.params %}
* **Paramètres:** * **Paramètres:**
{% for param in doc.params %} {% for param in doc.params %}
* `{{param.nom|safe}}` : {{param.description|safe}} * `{{param.nom|safe}}` : {{param.description|safe}}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if doc.description %} {% if doc.description %}

View File

@ -547,7 +547,7 @@ def analyze_api_routes(app, endpoint_start: str) -> tuple:
# point d'entrée de la commande `flask gen-api-map` # point d'entrée de la commande `flask gen-api-map`
def gen_api_map(app, endpoint_start="api."): def gen_api_map(app, endpoint_start="api.") -> str:
""" """
Fonction permettant de générer une carte SVG de l'API de ScoDoc Fonction permettant de générer une carte SVG de l'API de ScoDoc
Elle récupère les routes de l'API et les transforme en un arbre de Token Elle récupère les routes de l'API et les transforme en un arbre de Token
@ -566,6 +566,7 @@ def gen_api_map(app, endpoint_start="api."):
# On génère le tableau à partir de doctable_lines # On génère le tableau à partir de doctable_lines
table = _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) _write_gen_table(table)
return table
def _get_bbox(element, x_offset=0, y_offset=0): def _get_bbox(element, x_offset=0, y_offset=0):
@ -905,7 +906,7 @@ def doc_route(doctable: dict) -> str:
"href": f"{jinja_obj['nom'].replace('_', '-')}.json.md", "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."): def gen_api_doc(app, endpoint_start="api."):
@ -922,7 +923,7 @@ def gen_api_doc(app, endpoint_start="api."):
categories[category].append(value) categories[category].append(value)
# sort categories by name # sort categories by name
categories: dict = dict(sorted(categories.items(), key=lambda x: x[0])) categories: dict = dict(sorted(categories.items(), key=lambda x: x[0].capitalize()))
category: str category: str
routes: list[dict] routes: list[dict]
@ -935,9 +936,12 @@ def gen_api_doc(app, endpoint_start="api."):
mddoc += doc_route(route) mddoc += doc_route(route)
mddoc += "\n\n" 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: with open(fname, "w", encoding="utf-8") as f:
f.write(mddoc) f.write(mdpage)
print( print(
"La documentation API a été générée avec succès. " "La documentation API a été générée avec succès. "
f"Vous pouvez la consulter à l'adresse suivante : {fname}" f"Vous pouvez la consulter à l'adresse suivante : {fname}"