Compare commits

...

3 Commits

15 changed files with 748 additions and 425 deletions

View File

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

View File

@ -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/<dept>/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).

View File

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

View File

@ -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,30 +98,36 @@ 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 :
{
"11": {
```json
{
"11": {
"etudid": 11,
"evaluation_id": 1,
"value": 15.0,
"note_max" : 20.0,
"comment": "",
"date": "Wed, 20 Apr 2022 06:49:05 GMT",
"date": "2024-07-19T19:08:44+02:00",
"uid": 2
},
"12": {
},
"12": {
"etudid": 12,
"evaluation_id": 1,
"value": 12.0,
"value": "ABS",
"note_max" : 20.0,
"comment": "",
"date": "Wed, 20 Apr 2022 06:49:06 GMT",
"date": "2024-07-19T19:08:44+02:00",
"uid": 2
},
...
}
},
...
}
```
"""
query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
@ -145,13 +161,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 +207,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 +222,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 +273,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:

View File

@ -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&amp;T",
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
"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
}
```json
{
"id": 1,
"acronyme": "BUT R&amp;T",
"titre_officiel": "Bachelor technologique réseaux et télécommunications",
"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
}
```
"""
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&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": "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": [
"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&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"
}
]
"ue_reference": "2",
"coef": "4.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)
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,15 +405,17 @@ 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)
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
Ce changement peut être fait sur formation verrouillée.
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:
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)
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)
utilise l'argument value du POST (utilisé par `jinplace.js`)
Le retour est une chaîne (le code enregistré), pas json.
Le retour est une chaîne (le code enregistré), pas du json.
"""
if not code_apogee:
code_apogee = request.form.get("value", "")
@ -497,15 +516,17 @@ 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)
Si module_id n'est pas spécifié, utilise l'argument oid du POST.
Si code_apogee n'est pas spécifié ou vide,
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,
utilise l'argument value du POST (utilisé par jinplace.js)
Le retour est une chaîne (le code enregistré), pas json.
Le retour est une chaîne (le code enregistré), pas du json.
"""
if module_id is None:
module_id = request.form.get("oid")

View File

@ -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:<string:group_ids>
show_modules_titles:<bool: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:

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
@ -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,12 @@ 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 +209,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 +324,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 +352,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 +381,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

View File

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

View File

@ -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]
@ -93,22 +106,26 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
@scodoc
@permission_required(Permission.ScoView)
def moduleimpl_notes(moduleimpl_id: int):
"""Liste des notes dans ce moduleimpl
"""Liste des notes dans ce moduleimpl.
Exemple de résultat :
[
{
"etudid": 17776, // code de l'étudiant
"nom": "DUPONT",
"prenom": "Luz",
"38411": 16.0, // Note dans l'évaluation d'id 38411
"38410": 15.0,
"moymod": 15.5, // Moyenne INDICATIVE module
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
},
...
]
```json
[
{
"etudid": 17776, // code de l'étudiant
"nom": "DUPONT",
"prenom": "Luz",
"38411": 16.0, // Note dans l'évaluation d'id 38411
"38410": 15.0,
"moymod": 15.5, // Moyenne INDICATIVE module
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
},
...
]
```
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
app.set_sco_dept(modimpl.formsemestre.departement.acronym)

View File

@ -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:<string: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)

View File

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

View File

@ -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=<string:nom>
```
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:<bool:active>
departement:<string:departement>
starts_with:<string: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()

View File

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

View File

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

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