1
0
forked from ScoDoc/ScoDoc
This commit is contained in:
Emmanuel Viennet 2024-08-06 22:40:33 +02:00
commit 450d503c39
17 changed files with 311 additions and 94 deletions

View File

@ -7,7 +7,7 @@ from flask_json import as_json
from flask import Blueprint from flask import Blueprint
from flask import current_app, g, request from flask import current_app, g, request
from flask_login import current_user from flask_login import current_user
from app import db from app import db, log
from app.decorators import permission_required from app.decorators import permission_required
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoException from app.scodoc.sco_exceptions import AccessDenied, ScoException
@ -47,6 +47,7 @@ def api_permission_required(permission):
@api_bp.errorhandler(404) @api_bp.errorhandler(404)
def api_error_handler(e): def api_error_handler(e):
"erreurs API => json" "erreurs API => json"
log(f"api_error_handler: {e}")
return scu.json_error(404, message=str(e)) return scu.json_error(404, message=str(e))

View File

@ -41,6 +41,10 @@ from app.models import (
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
from app.scodoc import sco_edt_cal from app.scodoc import sco_edt_cal
from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules,
do_formsemestre_desinscription,
)
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
@ -64,10 +68,7 @@ def formsemestre_get(formsemestre_id: int):
------- -------
/formsemestre/1 /formsemestre/1
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
return formsemestre.to_dict_api() return formsemestre.to_dict_api()
@ -400,12 +401,7 @@ def bulletins(formsemestre_id: int, version: str = "long"):
------- -------
/formsemestre/1/bulletins /formsemestre/1/bulletins
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first()
if formsemestre is None:
return json_error(404, "formsemestre non trouve")
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
data = [] data = []
@ -432,10 +428,7 @@ def formsemestre_programme(formsemestre_id: int):
------- -------
/formsemestre/1/programme /formsemestre/1/programme
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
ues = formsemestre.get_ues() ues = formsemestre.get_ues()
m_list = { m_list = {
ModuleType.RESSOURCE: [], ModuleType.RESSOURCE: [],
@ -508,10 +501,7 @@ def formsemestre_etudiants(
/formsemestre/1/etudiants/query; /formsemestre/1/etudiants/query;
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(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 with_query: if with_query:
etat = request.args.get("etat") etat = request.args.get("etat")
if etat is not None: if etat is not None:
@ -543,6 +533,63 @@ def formsemestre_etudiants(
return sorted(etuds, key=itemgetter("sort_key")) return sorted(etuds, key=itemgetter("sort_key"))
@bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/inscrit")
@api_web_bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/inscrit")
@login_required
@scodoc
@permission_required(Permission.EtudInscrit)
@as_json
def formsemestre_etud_inscrit(formsemestre_id: int, etudid: int):
"""Inscrit l'étudiant à ce formsemestre et TOUS ses modules STANDARDS
(donc sauf les modules bonus sport).
DATA
----
```json
{
"dept_id" : int, # le département
"etape" : string, # optionnel: l'étape Apogée d'inscription
"group_ids" : [int], # optionnel: liste des groupes où inscrire l'étudiant (doivent exister)
}
```
"""
data = request.get_json(force=True) if request.data else {}
dept_id = data.get("dept_id", g.scodoc_dept_id)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
app.set_sco_dept(formsemestre.departement.acronym)
etud = Identite.get_etud(etudid)
group_ids = data.get("group_ids", [])
etape = data.get("etape", None)
do_formsemestre_inscription_with_modules(
formsemestre.id, etud.id, dept_id=dept_id, etape=etape, group_ids=group_ids
)
app.log(f"formsemestre_etud_inscrit: {etud} inscrit à {formsemestre}")
return (
FormSemestreInscription.query.filter_by(
formsemestre_id=formsemestre.id, etudid=etud.id
)
.first()
.to_dict()
)
@bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/desinscrit")
@api_web_bp.post("/formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/desinscrit")
@login_required
@scodoc
@permission_required(Permission.EtudInscrit)
@as_json
def formsemestre_etud_desinscrit(formsemestre_id: int, etudid: int):
"""Désinscrit l'étudiant de ce formsemestre et TOUS ses modules"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
etud = Identite.get_etud(etudid)
do_formsemestre_desinscription(etud.id, formsemestre.id)
app.log(f"formsemestre_etud_desinscrit: {etud} désinscrit de {formsemestre}")
return {"status": "ok"}
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals") @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals") @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@login_required @login_required
@ -649,10 +696,7 @@ def formsemestre_resultat(formsemestre_id: int):
return json_error(API_CLIENT_ERROR, "invalid format specification") return json_error(API_CLIENT_ERROR, "invalid format specification")
convert_values = format_spec != "raw" convert_values = format_spec != "raw"
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# Ajoute le groupe de chaque partition, # Ajoute le groupe de chaque partition,
@ -690,10 +734,7 @@ def formsemestre_resultat(formsemestre_id: int):
@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 `groups_save_auto_assignment`.""" """Rend les données stockées par `groups_save_auto_assignment`."""
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
response = make_response(formsemestre.groups_auto_assignment_data or b"") response = make_response(formsemestre.groups_auto_assignment_data or b"")
response.headers["Content-Type"] = scu.JSON_MIMETYPE response.headers["Content-Type"] = scu.JSON_MIMETYPE
return response return response
@ -713,11 +754,7 @@ def groups_save_auto_assignment(formsemestre_id: int):
"""Enregistre les données, associées à ce formsemestre. """Enregistre les données, associées à ce formsemestre.
Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs. Usage réservé aux fonctions de gestion des groupes, ne pas utiliser ailleurs.
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) formsemestre = FormSemestre.get_formsemestre(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(): if not formsemestre.can_change_groups():
return json_error(403, "non autorisé (can_change_groups)") return json_error(403, "non autorisé (can_change_groups)")
@ -726,6 +763,7 @@ def groups_save_auto_assignment(formsemestre_id: int):
formsemestre.groups_auto_assignment_data = request.data formsemestre.groups_auto_assignment_data = request.data
db.session.add(formsemestre) db.session.add(formsemestre)
db.session.commit() db.session.commit()
return {"status": "ok"}
@bp.route("/formsemestre/<int:formsemestre_id>/edt") @bp.route("/formsemestre/<int:formsemestre_id>/edt")
@ -746,10 +784,7 @@ def formsemestre_edt(formsemestre_id: int):
group_ids : string (optionnel) filtre sur les groupes ScoDoc. 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. 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) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
group_ids = request.args.getlist("group_ids", int) group_ids = request.args.getlist("group_ids", int)
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False)) show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
return sco_edt_cal.formsemestre_edt_dict( return sco_edt_cal.formsemestre_edt_dict(

View File

@ -765,7 +765,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
current_user.has_permission(Permission.AbsJustifView) current_user.has_permission(Permission.AbsJustifView)
or justificatif_unique.user_id == current_user.id or justificatif_unique.user_id == current_user.id
): ):
return json_error(401, "non autorisé à voir ce fichier") return json_error(403, "non autorisé à voir ce fichier")
# On récupère l'archive concernée # On récupère l'archive concernée
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier

View File

@ -16,12 +16,15 @@ from flask_json import as_json
from flask_login import login_required from flask_login import login_required
import app import app
from app import db
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.api import api_permission_required as permission_required from app.api import api_permission_required as permission_required
from app.decorators import scodoc from app.decorators import scodoc
from app.models import ModuleImpl from app.models import Identite, ModuleImpl, ModuleImplInscription
from app.scodoc import sco_liste_notes from app.scodoc import sco_cache, sco_liste_notes
from app.scodoc.sco_moduleimpl import do_moduleimpl_inscrit_etuds
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
@bp.route("/moduleimpl/<int:moduleimpl_id>") @bp.route("/moduleimpl/<int:moduleimpl_id>")
@ -63,6 +66,60 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
return [i.to_dict() for i in modimpl.inscriptions] return [i.to_dict() for i in modimpl.inscriptions]
@bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/inscrit")
@api_web_bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/inscrit")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl_etud_inscrit(moduleimpl_id: int, etudid: int):
"""Inscrit l'étudiant à ce moduleimpl.
SAMPLES
-------
/moduleimpl/1/etudid/2/inscrit
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if not modimpl.can_change_inscriptions():
return json_error(403, "opération non autorisée")
etud = Identite.get_etud(etudid)
do_moduleimpl_inscrit_etuds(modimpl.id, modimpl.formsemestre_id, [etud.id])
app.log(f"moduleimpl_etud_inscrit: {etud} inscrit à {modimpl}")
return (
ModuleImplInscription.query.filter_by(moduleimpl_id=modimpl.id, etudid=etud.id)
.first()
.to_dict()
)
@bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/desinscrit")
@api_web_bp.post("/moduleimpl/<int:moduleimpl_id>/etudid/<int:etudid>/desinscrit")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl_etud_desinscrit(moduleimpl_id: int, etudid: int):
"""Désinscrit l'étudiant de ce moduleimpl.
SAMPLES
-------
/moduleimpl/1/etudid/2/desinscrit
"""
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
if not modimpl.can_change_inscriptions():
return json_error(403, "opération non autorisée")
etud = Identite.get_etud(etudid)
inscription = ModuleImplInscription.query.filter_by(
etudid=etud.id, moduleimpl_id=modimpl.id
).first()
if inscription:
db.session.delete(inscription)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=modimpl.formsemestre_id)
app.log(f"moduleimpl_etud_desinscrit: {etud} inscrit à {modimpl}")
return {"status": "ok"}
@bp.route("/moduleimpl/<int:moduleimpl_id>/notes") @bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes") @api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
@login_required @login_required

View File

@ -169,7 +169,7 @@ def group_set_etudiant(group_id: int, etudid: int):
if not group.partition.formsemestre.etat: if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
if not group.partition.formsemestre.can_change_groups(): if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "opération non autorisée")
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: if etud.id not in {e.id for e in group.partition.formsemestre.etuds}:
return json_error(404, "etud non inscrit au formsemestre du groupe") return json_error(404, "etud non inscrit au formsemestre du groupe")
@ -202,7 +202,7 @@ def group_remove_etud(group_id: int, etudid: int):
if not group.partition.formsemestre.etat: if not group.partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
if not group.partition.formsemestre.can_change_groups(): if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "opération non autorisée")
group.remove_etud(etud) group.remove_etud(etud)
@ -232,7 +232,7 @@ def partition_remove_etud(partition_id: int, etudid: int):
if not partition.formsemestre.etat: if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
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(403, "opération non autorisée")
db.session.execute( db.session.execute(
sa.text( sa.text(
"""DELETE FROM group_membership """DELETE FROM group_membership
@ -289,7 +289,7 @@ def group_create(partition_id: int): # partition-group-create
if not partition.groups_editable: if not partition.groups_editable:
return json_error(403, "partition non editable") return json_error(403, "partition non editable")
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(403, "opération non autorisée")
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
group_name = args.get("group_name") group_name = args.get("group_name")
@ -337,7 +337,7 @@ def group_delete(group_id: int):
if not group.partition.groups_editable: if not group.partition.groups_editable:
return json_error(403, "partition non editable") return json_error(403, "partition non editable")
if not group.partition.formsemestre.can_change_groups(): if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "opération non autorisée")
formsemestre_id = group.partition.formsemestre_id formsemestre_id = group.partition.formsemestre_id
log(f"deleting {group}") log(f"deleting {group}")
db.session.delete(group) db.session.delete(group)
@ -378,7 +378,7 @@ def group_edit(group_id: int):
if not group.partition.groups_editable: if not group.partition.groups_editable:
return json_error(403, "partition non editable") return json_error(403, "partition non editable")
if not group.partition.formsemestre.can_change_groups(): if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "opération non autorisée")
args = request.get_json(force=True) # may raise 400 Bad Request args = request.get_json(force=True) # may raise 400 Bad Request
if "group_name" in args: if "group_name" in args:
@ -423,7 +423,7 @@ def group_set_edt_id(group_id: int, edt_id: str):
) )
group: GroupDescr = query.first_or_404() group: GroupDescr = query.first_or_404()
if not group.partition.formsemestre.can_change_groups(): if not group.partition.formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "opération non autorisée")
log(f"group_set_edt_id( {group_id}, '{edt_id}' )") log(f"group_set_edt_id( {group_id}, '{edt_id}' )")
group.edt_id = edt_id group.edt_id = edt_id
db.session.add(group) db.session.add(group)
@ -461,7 +461,7 @@ def partition_create(formsemestre_id: int):
if not formsemestre.etat: if not formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
if not formsemestre.can_change_groups(): if not formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
if partition_name is None: if partition_name is None:
@ -523,7 +523,7 @@ def formsemestre_set_partitions_order(formsemestre_id: int):
if not formsemestre.etat: if not formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
if not formsemestre.can_change_groups(): if not formsemestre.can_change_groups():
return json_error(401, "opération non autorisée") return json_error(403, "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, list) 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
@ -569,7 +569,7 @@ def partition_order_groups(partition_id: int):
if not partition.formsemestre.etat: if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
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(403, "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, list) 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
@ -623,7 +623,7 @@ def partition_edit(partition_id: int):
if not partition.formsemestre.etat: if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
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(403, "opération non autorisée")
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
modified = False modified = False
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
@ -689,7 +689,7 @@ def partition_delete(partition_id: int):
if not partition.formsemestre.etat: if not partition.formsemestre.etat:
return json_error(403, "formsemestre verrouillé") return json_error(403, "formsemestre verrouillé")
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(403, "opération non autorisée")
if not partition.partition_name: if not partition.partition_name:
return json_error( return json_error(
API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut" API_CLIENT_ERROR, "ne peut pas supprimer la partition par défaut"

View File

@ -111,6 +111,12 @@ class ScoDocModel(db.Model):
db.session.add(self) db.session.add(self)
return modified return modified
def to_dict(self) -> dict:
"dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def edit_from_form(self, form) -> bool: def edit_from_form(self, form) -> bool:
"""Generic edit method for updating model instance. """Generic edit method for updating model instance.
True if modification. True if modification.

View File

@ -1318,7 +1318,7 @@ notes_formsemestre_responsables = db.Table(
) )
class FormSemestreEtape(db.Model): class FormSemestreEtape(models.ScoDocModel):
"""Étape Apogée associée au semestre""" """Étape Apogée associée au semestre"""
__tablename__ = "notes_formsemestre_etapes" __tablename__ = "notes_formsemestre_etapes"
@ -1349,7 +1349,7 @@ class FormSemestreEtape(db.Model):
return ApoEtapeVDI(self.etape_apo) return ApoEtapeVDI(self.etape_apo)
class FormationModalite(db.Model): class FormationModalite(models.ScoDocModel):
"""Modalités de formation, utilisées pour la présentation """Modalités de formation, utilisées pour la présentation
(grouper les semestres, générer des codes, etc.) (grouper les semestres, générer des codes, etc.)
""" """
@ -1400,7 +1400,7 @@ class FormationModalite(db.Model):
raise raise
class FormSemestreUECoef(db.Model): class FormSemestreUECoef(models.ScoDocModel):
"""Coef des UE capitalisees arrivant dans ce semestre""" """Coef des UE capitalisees arrivant dans ce semestre"""
__tablename__ = "notes_formsemestre_uecoef" __tablename__ = "notes_formsemestre_uecoef"
@ -1441,7 +1441,7 @@ class FormSemestreUEComputationExpr(db.Model):
computation_expr = db.Column(db.Text()) computation_expr = db.Column(db.Text())
class FormSemestreCustomMenu(db.Model): class FormSemestreCustomMenu(models.ScoDocModel):
"""Menu custom associe au semestre""" """Menu custom associe au semestre"""
__tablename__ = "notes_formsemestre_custommenu" __tablename__ = "notes_formsemestre_custommenu"
@ -1457,7 +1457,7 @@ class FormSemestreCustomMenu(db.Model):
idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu idx = db.Column(db.Integer, default=0, server_default="0") # rang dans le menu
class FormSemestreInscription(db.Model): class FormSemestreInscription(models.ScoDocModel):
"""Inscription à un semestre de formation""" """Inscription à un semestre de formation"""
__tablename__ = "notes_formsemestre_inscription" __tablename__ = "notes_formsemestre_inscription"
@ -1503,7 +1503,7 @@ class FormSemestreInscription(db.Model):
} {('etape="'+self.etape+'"') if self.etape else ''}>""" } {('etape="'+self.etape+'"') if self.etape else ''}>"""
class NotesSemSet(db.Model): class NotesSemSet(models.ScoDocModel):
"""semsets: ensemble de formsemestres pour exports Apogée""" """semsets: ensemble de formsemestres pour exports Apogée"""
__tablename__ = "notes_semset" __tablename__ = "notes_semset"

View File

@ -325,7 +325,7 @@ notes_modules_enseignants = db.Table(
# XXX il manque probablement une relation pour gérer cela # XXX il manque probablement une relation pour gérer cela
class ModuleImplInscription(db.Model): class ModuleImplInscription(ScoDocModel):
"""Inscription à un module (etudiants,moduleimpl)""" """Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription" __tablename__ = "notes_moduleimpl_inscription"

View File

@ -56,7 +56,7 @@ class BulAppreciations(models.ScoDocModel):
return safehtml.html_to_safe_html(self.comment or "") return safehtml.html_to_safe_html(self.comment or "")
class NotesNotes(db.Model): class NotesNotes(models.ScoDocModel):
"""Une note""" """Une note"""
__tablename__ = "notes_notes" __tablename__ = "notes_notes"
@ -75,12 +75,6 @@ class NotesNotes(db.Model):
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
uid = db.Column(db.Integer, db.ForeignKey("user.id")) uid = db.Column(db.Integer, db.ForeignKey("user.id"))
def to_dict(self) -> dict:
"dict"
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
return d
def __repr__(self): def __repr__(self):
"pour debug" "pour debug"
from app.models.evaluations import Evaluation from app.models.evaluations import Evaluation

View File

@ -287,6 +287,8 @@ def do_formsemestre_inscription_with_modules(
group_ids = group_ids or [] group_ids = group_ids or []
if isinstance(group_ids, int): if isinstance(group_ids, int):
group_ids = [group_ids] group_ids = [group_ids]
# Check that all groups exist before creating the inscription
groups = [GroupDescr.query.get_or_404(group_id) for group_id in group_ids]
formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id, dept_id=dept_id)
# inscription au semestre # inscription au semestre
args = {"formsemestre_id": formsemestre_id, "etudid": etudid} args = {"formsemestre_id": formsemestre_id, "etudid": etudid}
@ -303,14 +305,13 @@ def do_formsemestre_inscription_with_modules(
# 1- inscrit au groupe 'tous' # 1- inscrit au groupe 'tous'
group_id = sco_groups.get_default_group(formsemestre_id) group_id = sco_groups.get_default_group(formsemestre_id)
sco_groups.set_group(etudid, group_id) sco_groups.set_group(etudid, group_id)
gdone = {group_id: 1} # empeche doublons gdone = {group_id} # empeche doublons
# 2- inscrit aux groupes # 2- inscrit aux groupes
for group_id in group_ids: for group in groups:
if group_id and group_id not in gdone: if group.id not in gdone:
_ = GroupDescr.query.get_or_404(group_id)
sco_groups.set_group(etudid, group_id) sco_groups.set_group(etudid, group_id)
gdone[group_id] = 1 gdone.add(group_id)
# Inscription à tous les modules de ce semestre # Inscription à tous les modules de ce semestre
for modimpl in formsemestre.modimpls: for modimpl in formsemestre.modimpls:

View File

@ -137,7 +137,8 @@ def moduleimpl_inscriptions_edit(
} }
} else { } else {
for (var i =nb_inputs_to_skip; i < elems.length; i++) { for (var i =nb_inputs_to_skip; i < elems.length; i++) {
var cells = elems[i].parentNode.parentNode.getElementsByTagName("td")[partitionIdx].childNodes; let tds = elems[i].parentNode.parentNode.getElementsByTagName("td");
var cells = tds[partitionIdx].childNodes;
if (cells.length && cells[0].nodeValue == groupName) { if (cells.length && cells[0].nodeValue == groupName) {
elems[i].checked=check; elems[i].checked=check;
} }
@ -179,19 +180,19 @@ def moduleimpl_inscriptions_edit(
else: else:
checked = "" checked = ""
H.append( H.append(
f"""<tr><td class="etud"><input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>""" f"""<tr><td class="etud">
) <input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>
H.append( <a class="discretelink etudinfo" href="{
f"""<a class="discretelink etudinfo" href="{
url_for( url_for(
"scolar.fiche_etud", "scolar.fiche_etud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etudid=etud["etudid"], etudid=etud["etudid"],
) )
}" id="{etud['etudid']}">{etud['nomprenom']}</a>""" }" id="{etud['etudid']}">{etud['nomprenom']}</a>
</input>
</td>
"""
) )
H.append("""</input></td>""")
groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id) groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id)
for partition in partitions: for partition in partitions:
if partition["partition_name"]: if partition["partition_name"]:

View File

@ -270,6 +270,9 @@ Pour uniformiser les résultats des exemples, ceux sont soumis à quelques post-
Voir exemples d'utilisation de l'API en Python, dans `tests/api/`. Voir exemples d'utilisation de l'API en Python, dans `tests/api/`.
!!! info
Cette page a été générée par la commande `flask gen-api-doc`, et les exemples de résultats
sont créés par `tools/test_api.sh --make-samples`.
!!! note "Voir aussi" !!! note "Voir aussi"

View File

@ -56,6 +56,9 @@ class APIError(Exception):
self.message = message self.message = message
self.payload = payload or {} self.payload = payload or {}
def __str__(self):
return f"APIError: {self.message} payload={self.payload}"
def get_auth_headers(user, password) -> dict: def get_auth_headers(user, password) -> dict:
"Demande de jeton, dict à utiliser dans les en-têtes de requêtes http" "Demande de jeton, dict à utiliser dans les en-têtes de requêtes http"
@ -130,11 +133,17 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
def POST( def POST(
path: str, data: dict = {}, headers: dict = None, errmsg=None, dept=None, raw=False path: str,
data: dict = None,
headers: dict = None,
errmsg=None,
dept=None,
raw=False,
): ):
"""Post """Post
Decode réponse en json, sauf si raw. Decode réponse en json, sauf si raw.
""" """
data = data or {}
if dept: if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
else: else:
@ -147,7 +156,13 @@ def POST(
timeout=SCO_TEST_API_TIMEOUT, timeout=SCO_TEST_API_TIMEOUT,
) )
if r.status_code != 200: if r.status_code != 200:
raise APIError(errmsg or f"erreur status={r.status_code} !", r.json()) try:
payload = r.json()
except requests.exceptions.JSONDecodeError:
payload = r.text
raise APIError(
errmsg or f"erreur url={url} status={r.status_code} !", payload=payload
)
return r if raw else r.json() # decode la reponse JSON return r if raw else r.json() # decode la reponse JSON

View File

@ -0,0 +1,51 @@
"""Test API exceptions
"""
import json
import requests
import pytest
from tests.api.setup_test_api import (
API_URL,
CHECK_CERTIFICATE,
api_headers,
)
from app.scodoc import sco_utils as scu
def test_exceptions(api_headers):
"""
Vérifie que les exceptions de l'API sont toutes en JSON.
"""
# Une requete sur une url inexistante ne passe pas par les blueprints API
# et est donc en HTML
r = requests.get(
f"{API_URL}/mmm/non/existant/mmm",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 404
assert r.headers["Content-Type"] == "text/html; charset=utf-8"
# Une requete d'un objet non existant est en JSON
r = requests.get(
f"{API_URL}/formsemestre/999999",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 404
assert r.headers["Content-Type"] == "application/json"
assert r.json()
# Une requête API sans autorisation est en JSON
r = requests.post(
f"{API_URL}/formsemestre/1/etudid/1/inscrit",
headers=api_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 401
assert r.headers["Content-Type"] == "application/json"
assert r.json()

View File

@ -29,6 +29,7 @@ from tests.api.setup_test_api import (
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
api_headers, api_headers,
api_admin_headers,
) )
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
@ -585,6 +586,46 @@ def test_formsemestre_etudiants(api_headers):
assert r_error_defaillants.status_code == 404 assert r_error_defaillants.status_code == 404
def test_formsemestre_inscriptions(api_admin_headers):
"""
Route: /formsemestre/<int:formsemestre_id>/etudid/<int:etudid>/inscrit
"""
dept_id = 1
formsemestre_id = 1
etudid = 20 # pas déjà inscrit au semestre 1
# -- Inscription
r = requests.post(
f"{API_URL}/formsemestre/{formsemestre_id}/etudid/{etudid}/inscrit",
data=json.dumps({"dept_id": dept_id}),
headers=api_admin_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
inscription = r.json()
assert inscription["formsemestre_id"] == formsemestre_id
assert inscription["etudid"] == etudid
assert inscription["etat"] == "I"
# -- Désincription
r = requests.post(
f"{API_URL}/formsemestre/{formsemestre_id}/etudid/{etudid}/desinscrit",
headers=api_admin_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r.status_code == 200
### ERROR ###
etudid_inexistant = 165165165165165165165
r_error = requests.post(
f"{API_URL}/formsemestre/{formsemestre_id}/etudid/{etudid_inexistant}/inscrit",
headers=api_admin_headers,
verify=CHECK_CERTIFICATE,
timeout=scu.SCO_TEST_API_TIMEOUT,
)
assert r_error.status_code == 404
def test_formsemestre_programme(api_headers): def test_formsemestre_programme(api_headers):
""" """
Route: /formsemestre/1/programme Route: /formsemestre/1/programme

View File

@ -968,6 +968,8 @@ def gen_api_doc(app, endpoint_start="api."):
with open(fname, "w", encoding="utf-8") as f: with open(fname, "w", encoding="utf-8") as f:
f.write(mdpage) f.write(mdpage)
print( print(
"La documentation API a été générée avec succès. " f"""La documentation API a été générée avec succès.
f"Vous pouvez la consulter à l'adresse suivante : {fname}" Vous pouvez la consulter à l'adresse suivante : {fname}.
Vous pouvez maintenant générer les samples avec `tools/test_api.sh --make-samples`.
"""
) )

View File

@ -11,13 +11,17 @@
# #
# Toutes les autres options sont passées telles qu'elles à pytest # Toutes les autres options sont passées telles qu'elles à pytest
# #
# Utilisation pour générer des exemples de documentation:
# tools/test_api.sh --make-samples
#
# Exemples: # Exemples:
# - lancer tous les tests API: tools/test_api.sh # - lancer tous les tests API: tools/test_api.sh
# - lancer tous les tests, en mode debug (arrêt pdb sur le 1er): # - lancer tous les tests, en mode debug (arrêt pdb sur le 1er):
# tools/test_api.sh -x --pdb tests/api # tools/test_api.sh -x --pdb tests/api
# - lancer un module de test, en utilisant un server dev existant: # - lancer un module de test, en utilisant un server dev existant:
# tools/test_api.sh --dont-start-server -x --pdb tests/api/test_api_evaluations.py # tools/test_api.sh --dont-start-server -x --pdb tests/api/test_api_evaluations.py
# # - Générer les samples pour la doc:
# tools/test_api.sh --make-samples
# #
# E. Viennet, Fev 2023 # E. Viennet, Fev 2023
@ -71,8 +75,14 @@ then
echo "Starting pytest tests/api" echo "Starting pytest tests/api"
pytest tests/api pytest tests/api
else else
if [ "$1" = "--make-samples" ]
then
echo "Generating API documentation samples"
python tests/api/make_samples.py -i /tmp/samples.csv
else
echo "Starting pytest $@" echo "Starting pytest $@"
pytest "$@" pytest "$@"
fi
fi fi
# ------------------ # ------------------