forked from ScoDoc/ScoDoc
681 lines
24 KiB
Python
Executable File
681 lines
24 KiB
Python
Executable File
##############################################################################
|
|
# ScoDoc
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
|
# See LICENSE
|
|
##############################################################################
|
|
|
|
"""
|
|
API : accès aux étudiants
|
|
"""
|
|
from datetime import datetime
|
|
from operator import attrgetter
|
|
|
|
from flask import g, request, Response
|
|
from flask_json import as_json
|
|
from flask_login import current_user
|
|
from flask_login import login_required
|
|
from sqlalchemy import desc, func, or_
|
|
from sqlalchemy.dialects.postgresql import VARCHAR
|
|
|
|
import app
|
|
from app import db, log
|
|
from app.api import api_bp as bp, api_web_bp
|
|
from app.api import tools
|
|
from app.but import bulletin_but_court
|
|
from app.decorators import scodoc, permission_required
|
|
from app.models import (
|
|
Admission,
|
|
Departement,
|
|
EtudAnnotation,
|
|
FormSemestreInscription,
|
|
FormSemestre,
|
|
Identite,
|
|
ScolarNews,
|
|
)
|
|
from app.scodoc import sco_bulletins
|
|
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.sco_utils import json_error, suppress_accents
|
|
|
|
import app.scodoc.sco_photos as sco_photos
|
|
import app.scodoc.sco_utils as scu
|
|
|
|
# Un exemple:
|
|
# @bp.route("/api_function/<int:arg>")
|
|
# @api_web_bp.route("/api_function/<int:arg>")
|
|
# @login_required
|
|
# @scodoc
|
|
# @permission_required(Permission.ScoView)
|
|
# @as_json
|
|
# def api_function(arg: int):
|
|
# """Une fonction quelconque de l'API"""
|
|
# return {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept}
|
|
#
|
|
|
|
|
|
def _get_etud_by_code(
|
|
code_type: str, code: str, dept: Departement
|
|
) -> tuple[bool, Response | Identite]:
|
|
"""Get etud, using etudid, NIP or INE
|
|
Returns True, etud if ok, or False, error response.
|
|
"""
|
|
if code_type == "nip":
|
|
query = Identite.query.filter_by(code_nip=code)
|
|
elif code_type == "etudid":
|
|
try:
|
|
etudid = int(code)
|
|
except ValueError:
|
|
return False, json_error(404, "invalid etudid type")
|
|
query = Identite.query.filter_by(id=etudid)
|
|
elif code_type == "ine":
|
|
query = Identite.query.filter_by(code_ine=code)
|
|
else:
|
|
return False, json_error(404, "invalid code_type")
|
|
if dept:
|
|
query = query.filter_by(dept_id=dept.id)
|
|
etud = query.first()
|
|
if etud is None:
|
|
return False, json_error(404, message="etudiant inexistant")
|
|
return True, etud
|
|
|
|
|
|
@bp.route("/etudiants/courants", defaults={"long": False})
|
|
@bp.route("/etudiants/courants/long", defaults={"long": True})
|
|
@api_web_bp.route("/etudiants/courants", defaults={"long": False})
|
|
@api_web_bp.route("/etudiants/courants/long", defaults={"long": True})
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@as_json
|
|
def etudiants_courants(long=False):
|
|
"""
|
|
La liste des étudiants des semestres "courants" (tous départements)
|
|
(date du jour comprise dans la période couverte par le sem.)
|
|
dans lesquels l'utilisateur a la permission ScoView
|
|
(donc tous si le dept du rôle est None).
|
|
|
|
Exemple de résultat :
|
|
[
|
|
{
|
|
"id": 1234,
|
|
"code_nip": "12345678",
|
|
"code_ine": null,
|
|
"nom": "JOHN",
|
|
"nom_usuel": None,
|
|
"prenom": "DEUF",
|
|
"civilite": "M",
|
|
}
|
|
...
|
|
]
|
|
|
|
En format "long": voir documentation.
|
|
|
|
"""
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
|
date_courante = request.args.get("date_courante")
|
|
if date_courante:
|
|
test_date = datetime.fromisoformat(date_courante)
|
|
else:
|
|
test_date = app.db.func.now()
|
|
etuds = Identite.query.filter(
|
|
Identite.id == FormSemestreInscription.etudid,
|
|
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
|
FormSemestre.date_debut <= test_date,
|
|
FormSemestre.date_fin >= test_date,
|
|
)
|
|
if not None in allowed_depts:
|
|
# restreint aux départements autorisés:
|
|
etuds = etuds.join(Departement).filter(
|
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
|
)
|
|
if long:
|
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
data = [
|
|
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in etuds
|
|
]
|
|
else:
|
|
data = [etud.to_dict_short() for etud in etuds]
|
|
return data
|
|
|
|
|
|
@bp.route("/etudiant/etudid/<int:etudid>")
|
|
@bp.route("/etudiant/nip/<string:nip>")
|
|
@bp.route("/etudiant/ine/<string:ine>")
|
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>")
|
|
@api_web_bp.route("/etudiant/nip/<string:nip>")
|
|
@api_web_bp.route("/etudiant/ine/<string:ine>")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@as_json
|
|
def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
|
"""
|
|
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
|
|
|
|
etudid : l'etudid de l'étudiant
|
|
nip : le code nip de l'étudiant
|
|
ine : le code ine de l'étudiant
|
|
|
|
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.
|
|
"""
|
|
etud = tools.get_etud(etudid, nip, ine)
|
|
|
|
if etud is None:
|
|
return json_error(
|
|
404,
|
|
message="étudiant inconnu",
|
|
)
|
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
return etud.to_dict_api(restrict=restrict, with_annotations=True)
|
|
|
|
|
|
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
|
@bp.route("/etudiant/nip/<string:nip>/photo")
|
|
@bp.route("/etudiant/ine/<string:ine>/photo")
|
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo")
|
|
@api_web_bp.route("/etudiant/nip/<string:nip>/photo")
|
|
@api_web_bp.route("/etudiant/ine/<string:ine>/photo")
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
def get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
|
|
"""
|
|
Retourne la photo de l'étudiant
|
|
correspondant ou un placeholder si non existant.
|
|
|
|
etudid : l'etudid de l'étudiant
|
|
nip : le code nip de l'étudiant
|
|
ine : le code ine de l'étudiant
|
|
"""
|
|
|
|
etud = tools.get_etud(etudid, nip, ine)
|
|
|
|
if etud is None:
|
|
return json_error(
|
|
404,
|
|
message="étudiant inconnu",
|
|
)
|
|
if not etudid:
|
|
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
|
|
|
size = request.args.get("size", "orig")
|
|
filename = sco_photos.photo_pathname(etud.photo_filename, size=size)
|
|
if not filename:
|
|
filename = sco_photos.UNKNOWN_IMAGE_PATH
|
|
res = sco_photos.build_image_response(filename)
|
|
return res
|
|
|
|
|
|
@bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
|
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>/photo", methods=["POST"])
|
|
@login_required
|
|
@scodoc
|
|
@permission_required(Permission.EtudChangeAdr)
|
|
@as_json
|
|
def set_photo_image(etudid: int = None):
|
|
"""Enregistre la photo de l'étudiant."""
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.EtudChangeAdr)
|
|
query = Identite.query.filter_by(id=etudid)
|
|
if not None in allowed_depts:
|
|
# restreint aux départements autorisés:
|
|
query = query.join(Departement).filter(
|
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
|
)
|
|
if g.scodoc_dept is not None:
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
etud: Identite = query.first()
|
|
if etud is None:
|
|
return json_error(404, message="etudiant inexistant")
|
|
# Récupère l'image
|
|
if len(request.files) == 0:
|
|
return json_error(404, "Il n'y a pas de fichier joint")
|
|
|
|
file = list(request.files.values())[0]
|
|
if not file.filename:
|
|
return json_error(404, "Il n'y a pas de fichier joint")
|
|
data = file.stream.read()
|
|
|
|
status, err_msg = sco_photos.store_photo(etud, data, file.filename)
|
|
if status:
|
|
return {"etudid": etud.id, "message": "recorded photo"}
|
|
return json_error(
|
|
404,
|
|
message=f"Erreur: {err_msg}",
|
|
)
|
|
|
|
|
|
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
|
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
|
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
|
@api_web_bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
|
|
@api_web_bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
|
|
@api_web_bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@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.
|
|
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.).
|
|
"""
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
|
if etudid is not None:
|
|
query = Identite.query.filter_by(id=etudid)
|
|
elif nip is not None:
|
|
query = Identite.query.filter_by(code_nip=nip)
|
|
elif ine is not None:
|
|
query = Identite.query.filter_by(code_ine=ine)
|
|
else:
|
|
return json_error(
|
|
404,
|
|
message="parametre manquant",
|
|
)
|
|
if not None in allowed_depts:
|
|
# restreint aux départements autorisés:
|
|
query = query.join(Departement).filter(
|
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
|
)
|
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
return [
|
|
etud.to_dict_api(restrict=restrict, with_annotations=True) for etud in query
|
|
]
|
|
|
|
|
|
@bp.route("/etudiants/name/<string:start>")
|
|
@api_web_bp.route("/etudiants/name/<string:start>")
|
|
@scodoc
|
|
@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.
|
|
La casse et les accents sont ignorés.
|
|
"""
|
|
if len(start) < min_len:
|
|
return []
|
|
start = suppress_accents(start).lower()
|
|
query = Identite.query.filter(
|
|
func.lower(func.unaccent(Identite.nom, type_=VARCHAR)).ilike(start + "%")
|
|
)
|
|
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
|
if not None in allowed_depts:
|
|
# restreint aux départements autorisés:
|
|
query = query.join(Departement).filter(
|
|
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
|
)
|
|
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
|
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
return [
|
|
etud.to_dict_api(restrict=restrict)
|
|
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
|
]
|
|
|
|
|
|
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
|
@bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
|
@bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
|
@api_web_bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
|
@api_web_bp.route("/etudiant/nip/<string:nip>/formsemestres")
|
|
@api_web_bp.route("/etudiant/ine/<string:ine>/formsemestres")
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@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.
|
|
Accès par etudid, nip ou ine.
|
|
|
|
Attention, si accès via NIP ou INE, les semestres 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.
|
|
"""
|
|
if etudid is not None:
|
|
q_etud = Identite.query.filter_by(id=etudid)
|
|
elif nip is not None:
|
|
q_etud = Identite.query.filter_by(code_nip=nip)
|
|
elif ine is not None:
|
|
q_etud = Identite.query.filter_by(code_ine=ine)
|
|
else:
|
|
return json_error(404, message="parametre manquant")
|
|
if g.scodoc_dept is not None:
|
|
q_etud = q_etud.filter_by(dept_id=g.scodoc_dept_id)
|
|
etud = q_etud.join(Admission).order_by(desc(Admission.annee)).first()
|
|
if etud is None:
|
|
return json_error(404, message="etudiant inexistant")
|
|
query = FormSemestre.query.filter(
|
|
FormSemestreInscription.etudid == etud.id,
|
|
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
|
)
|
|
if g.scodoc_dept is not None:
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestres = query.order_by(FormSemestre.date_debut)
|
|
|
|
return [formsemestre.to_dict_api() for formsemestre in formsemestres]
|
|
|
|
|
|
@bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
|
)
|
|
@bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
|
)
|
|
@bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
|
defaults={"pdf": True},
|
|
)
|
|
@bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
|
|
defaults={"pdf": True, "with_img_signatures_pdf": False},
|
|
)
|
|
@api_web_bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin",
|
|
)
|
|
@api_web_bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>",
|
|
)
|
|
@api_web_bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf",
|
|
defaults={"pdf": True},
|
|
)
|
|
@api_web_bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/formsemestre/<int:formsemestre_id>/bulletin/<string:version>/pdf/nosig",
|
|
defaults={"pdf": True, "with_img_signatures_pdf": False},
|
|
)
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
def bulletin(
|
|
code_type: str = "etudid",
|
|
code: str = None,
|
|
formsemestre_id: int = None,
|
|
version: str = "selectedevals",
|
|
pdf: bool = False,
|
|
with_img_signatures_pdf: bool = True,
|
|
):
|
|
"""
|
|
Retourne le bulletin d'un étudiant dans un formsemestre.
|
|
|
|
formsemestre_id : l'id d'un formsemestre
|
|
code_type : "etudid", "nip" ou "ine"
|
|
code : valeur du code INE, NIP ou etudid, selon code_type.
|
|
version : type de bulletin (par défaut, "selectedevals"): short, long, selectedevals, butcourt
|
|
pdf : si spécifié, bulletin au format PDF (et non JSON).
|
|
|
|
Exemple de résultat : voir https://scodoc.org/ScoDoc9API/#bulletin
|
|
"""
|
|
if version == "pdf":
|
|
version = "long"
|
|
pdf = True
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
if version not in (
|
|
scu.BULLETINS_VERSIONS_BUT
|
|
if formsemestre.formation.is_apc()
|
|
else scu.BULLETINS_VERSIONS
|
|
):
|
|
return json_error(404, "version invalide")
|
|
if formsemestre.bul_hide_xml and pdf:
|
|
return json_error(403, "bulletin non disponible")
|
|
# note: la version json est réduite si bul_hide_xml
|
|
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
|
|
if g.scodoc_dept and dept.acronym != g.scodoc_dept:
|
|
return json_error(404, "formsemestre inexistant")
|
|
app.set_sco_dept(dept.acronym)
|
|
|
|
ok, etud = _get_etud_by_code(code_type, code, dept)
|
|
if not ok:
|
|
return etud # json error
|
|
|
|
if version == "butcourt":
|
|
if pdf:
|
|
return bulletin_but_court.bulletin_but(formsemestre_id, etud.id, fmt="pdf")
|
|
else:
|
|
return json_error(404, message="butcourt available only in pdf")
|
|
if pdf:
|
|
pdf_response, _ = do_formsemestre_bulletinetud(
|
|
formsemestre,
|
|
etud,
|
|
version=version,
|
|
fmt="pdf",
|
|
with_img_signatures_pdf=with_img_signatures_pdf,
|
|
)
|
|
return pdf_response
|
|
return sco_bulletins.get_formsemestre_bulletin_etud_json(
|
|
formsemestre, etud, version=version
|
|
)
|
|
|
|
|
|
@bp.route("/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups")
|
|
@api_web_bp.route(
|
|
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups"
|
|
)
|
|
@scodoc
|
|
@permission_required(Permission.ScoView)
|
|
@as_json
|
|
def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
|
"""
|
|
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
|
|
|
formsemestre_id : l'id d'un formsemestre
|
|
etudid : l'etudid d'un étudiant
|
|
|
|
Exemple de résultat :
|
|
[
|
|
{
|
|
"partition_id": 1,
|
|
"id": 1,
|
|
"formsemestre_id": 1,
|
|
"partition_name": null,
|
|
"numero": 0,
|
|
"bul_show_rank": false,
|
|
"show_in_lists": true,
|
|
"group_id": 1,
|
|
"group_name": null
|
|
},
|
|
{
|
|
"partition_id": 2,
|
|
"id": 2,
|
|
"formsemestre_id": 1,
|
|
"partition_name": "TD",
|
|
"numero": 1,
|
|
"bul_show_rank": false,
|
|
"show_in_lists": true,
|
|
"group_id": 2,
|
|
"group_name": "A"
|
|
}
|
|
]
|
|
"""
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
if g.scodoc_dept:
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
formsemestre = query.first()
|
|
if formsemestre is None:
|
|
return json_error(
|
|
404,
|
|
message="formsemestre inconnu",
|
|
)
|
|
dept = formsemestre.departement
|
|
etud = Identite.query.filter_by(id=etudid, dept_id=dept.id).first_or_404(etudid)
|
|
|
|
app.set_sco_dept(dept.acronym)
|
|
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
|
|
|
return data
|
|
|
|
|
|
@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
|
@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
|
@api_web_bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
|
@api_web_bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
|
@scodoc
|
|
@permission_required(Permission.EtudInscrit)
|
|
@as_json
|
|
def etudiant_create(force=False):
|
|
"""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
|
|
dept = args.get("dept", None)
|
|
if not dept:
|
|
return scu.json_error(400, "dept requis")
|
|
dept_o = Departement.query.filter_by(acronym=dept).first()
|
|
if not dept_o:
|
|
return scu.json_error(400, "dept invalide")
|
|
if g.scodoc_dept and g.scodoc_dept_id != dept_o.id:
|
|
return scu.json_error(400, "dept invalide (route departementale)")
|
|
else:
|
|
app.set_sco_dept(dept)
|
|
args["dept_id"] = dept_o.id
|
|
# vérifie que le département de création est bien autorisé
|
|
if not current_user.has_permission(Permission.EtudInscrit, dept):
|
|
return json_error(403, "departement non autorisé")
|
|
nom = args.get("nom", None)
|
|
prenom = args.get("prenom", None)
|
|
ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
|
|
if not ok:
|
|
return scu.json_error(400, "nom ou prénom invalide")
|
|
if len(homonyms) > 0 and not force:
|
|
return scu.json_error(
|
|
400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
|
|
)
|
|
etud = Identite.create_etud(**args)
|
|
db.session.flush()
|
|
# --- Données admission
|
|
admission_args = args.get("admission", None)
|
|
if admission_args:
|
|
etud.admission.from_dict(admission_args)
|
|
# --- Adresse
|
|
adresses = args.get("adresses", [])
|
|
if adresses:
|
|
# ne prend en compte que la première adresse
|
|
# car si la base est concue pour avoir plusieurs adresses par étudiant,
|
|
# l'application n'en gère plus qu'une seule.
|
|
adresse = etud.adresses.first()
|
|
adresse.from_dict(adresses[0])
|
|
|
|
# Poste une nouvelle dans le département concerné:
|
|
ScolarNews.add(
|
|
typ=ScolarNews.NEWS_INSCR,
|
|
text=f"Nouvel étudiant {etud.html_link_fiche()}",
|
|
url=etud.url_fiche(),
|
|
max_frequency=0,
|
|
dept_id=dept_o.id,
|
|
)
|
|
db.session.commit()
|
|
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
|
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
|
db.session.refresh(etud)
|
|
|
|
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
|
|
return r
|
|
|
|
|
|
@bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
|
@api_web_bp.route("/etudiant/<string:code_type>/<string:code>/edit", methods=["POST"])
|
|
@scodoc
|
|
@permission_required(Permission.EtudInscrit)
|
|
@as_json
|
|
def etudiant_edit(
|
|
code_type: str = "etudid",
|
|
code: str = None,
|
|
):
|
|
"""Edition des données étudiant (identité, admission, adresses)"""
|
|
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
if not ok:
|
|
return etud # json error
|
|
#
|
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
etud.from_dict(args)
|
|
admission_args = args.get("admission", None)
|
|
if admission_args:
|
|
etud.admission.from_dict(admission_args)
|
|
# --- Adresse
|
|
adresses = args.get("adresses", [])
|
|
if adresses:
|
|
# ne prend en compte que la première adresse
|
|
# car si la base est concue pour avoir plusieurs adresses par étudiant,
|
|
# l'application n'en gère plus qu'une seule.
|
|
adresse = etud.adresses.first()
|
|
adresse.from_dict(adresses[0])
|
|
|
|
db.session.commit()
|
|
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
|
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
|
db.session.refresh(etud)
|
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
r = etud.to_dict_api(restrict=restrict)
|
|
return r
|
|
|
|
|
|
@bp.route("/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"])
|
|
@api_web_bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/annotation", methods=["POST"]
|
|
)
|
|
@scodoc
|
|
@permission_required(Permission.EtudInscrit) # il faut en plus ViewEtudData
|
|
@as_json
|
|
def etudiant_annotation(
|
|
code_type: str = "etudid",
|
|
code: str = None,
|
|
):
|
|
"""Ajout d'une annotation sur un étudiant"""
|
|
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)
|
|
if not ok:
|
|
return etud # json error
|
|
#
|
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
comment = args.get("comment", None)
|
|
if not isinstance(comment, str):
|
|
return json_error(404, "invalid comment (expected string)")
|
|
if len(comment) > scu.MAX_TEXT_LEN:
|
|
return json_error(404, "invalid comment (too large)")
|
|
annotation = EtudAnnotation(comment=comment, author=current_user.user_name)
|
|
etud.annotations.append(annotation)
|
|
db.session.add(etud)
|
|
db.session.commit()
|
|
log(f"etudiant_annotation/{etud.id}/{annotation.id}")
|
|
return annotation.to_dict()
|
|
|
|
|
|
@bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
|
methods=["POST"],
|
|
)
|
|
@api_web_bp.route(
|
|
"/etudiant/<string:code_type>/<string:code>/annotation/<int:annotation_id>/delete",
|
|
methods=["POST"],
|
|
)
|
|
@login_required
|
|
@scodoc
|
|
@as_json
|
|
@permission_required(Permission.EtudInscrit)
|
|
def etudiant_annotation_delete(
|
|
code_type: str = "etudid", code: str = None, annotation_id: int = None
|
|
):
|
|
"""
|
|
Suppression d'une annotation
|
|
"""
|
|
ok, etud = _get_etud_by_code(code_type, code, g.scodoc_dept)
|
|
if not ok:
|
|
return etud # json error
|
|
annotation = EtudAnnotation.query.filter_by(
|
|
etudid=etud.id, id=annotation_id
|
|
).first()
|
|
if annotation is None:
|
|
return json_error(404, "annotation not found")
|
|
log(f"etudiant_annotation_delete/{etud.id}/{annotation.id}")
|
|
db.session.delete(annotation)
|
|
db.session.commit()
|
|
return "ok"
|