ScoDoc/app/api/etudiants.py

728 lines
25 KiB
Python
Raw Permalink Normal View History

2022-05-11 00:59:51 +02:00
##############################################################################
# ScoDoc
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2022-05-11 00:59:51 +02:00
# See LICENSE
##############################################################################
"""
API : accès aux étudiants
2024-07-24 17:34:30 +02:00
CATEGORY
--------
Étudiants
2022-05-11 00:59:51 +02:00
"""
from datetime import datetime
from operator import attrgetter
2024-02-11 12:05:43 +01:00
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
2022-04-26 13:46:09 +02:00
import app
2024-02-11 12:05:43 +01:00
from app import db, log
from app.api import api_bp as bp, api_web_bp
from app.api import tools
from app.api import api_permission_required as permission_required
from app.but import bulletin_but_court
from app.decorators import scodoc
from app.models import (
Admission,
Departement,
2024-02-11 12:05:43 +01:00
EtudAnnotation,
FormSemestreInscription,
FormSemestre,
Identite,
ScolarNews,
)
2022-04-26 13:46:09 +02:00
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
2022-03-04 17:16:08 +01:00
from app.scodoc.sco_permissions import Permission
2024-07-24 17:34:30 +02:00
from app.scodoc import sco_photos
from app.scodoc.sco_utils import json_error, suppress_accents
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}
#
2022-07-26 09:00:48 +02:00
2024-02-11 12:05:43 +01:00
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: bool = False):
"""
La liste des étudiants des semestres "courants".
Considère tous les départements dans lesquels l'utilisateur a la
permission `ScoView` (donc tous si le dépt. du rôle est `None`),
et les formsemestres contenant la date courante,
ou à défaut celle indiquée en argument (au format ISO).
En format "long": voir l'exemple.
QUERY
-----
date_courante:<string:date_courante>
SAMPLES
-------
/etudiants/courants?date_courante=2022-05-01;
/etudiants/courants/long?date_courante=2022-05-01;
2022-07-31 21:44:39 +02:00
"""
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()
2022-04-26 13:46:09 +02:00
etuds = Identite.query.filter(
Identite.id == FormSemestreInscription.etudid,
FormSemestreInscription.formsemestre_id == FormSemestre.id,
FormSemestre.date_debut <= test_date,
FormSemestre.date_fin >= test_date,
2022-04-26 13:46:09 +02:00
)
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:
2024-01-20 17:37:24 +01:00
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
2022-05-11 00:59:51 +02:00
def etudiant(etudid: int = None, nip: str = None, ine: str = None):
"""
2022-05-11 00:59:51 +02:00
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
2024-07-24 17:34:30 +02:00
PARAMS
------
2022-05-11 00:59:51 +02:00
etudid : l'etudid de l'étudiant
nip : le code nip de l'étudiant
ine : le code ine de l'étudiant
2024-07-24 17:34:30 +02:00
`etudid` est unique dans la base (tous départements).
2022-05-11 00:59:51 +02:00
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)
2022-05-11 00:59:51 +02:00
if etud is None:
return json_error(
404,
2022-05-11 00:59:51 +02:00
message="étudiant inconnu",
)
2024-01-20 17:37:24 +01:00
restrict = not current_user.has_permission(Permission.ViewEtudData)
return etud.to_dict_api(restrict=restrict, with_annotations=True)
2022-05-11 00:59:51 +02:00
@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)
2024-07-17 14:58:49 +02:00
def etudiant_get_photo_image(etudid: int = None, nip: str = None, ine: str = None):
"""
Retourne la photo de l'étudiant ou un placeholder si non existant.
Le paramètre `size` peut prendre la valeur `small` (taille réduite, hauteur
environ 90 pixels) ou `orig` (défaut, image de la taille originale).
QUERY
-----
size:<string:size>
2024-07-24 17:34:30 +02:00
PARAMS
------
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
2024-07-17 14:58:49 +02:00
def etudiant_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}",
)
2022-05-11 00:59:51 +02:00
@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
2022-05-11 00:59:51 +02:00
def etudiants(etudid: int = None, nip: str = None, ine: str = None):
"""
2024-07-24 17:34:30 +02:00
Info sur le ou les étudiants correspondants.
Comme `/etudiant` mais renvoie toujours une liste.
2022-05-11 00:59:51 +02:00
Si non trouvé, liste vide, pas d'erreur.
2024-07-24 17:34:30 +02:00
2022-05-11 00:59:51 +02:00
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)
2022-05-11 00:59:51 +02:00
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(
2022-05-11 00:59:51 +02:00
404,
message="parametre manquant",
)
2022-07-26 09:00:48 +02:00
if not None in allowed_depts:
# restreint aux départements autorisés:
query = query.join(Departement).filter(
2022-07-26 09:00:48 +02:00
or_(Departement.acronym == acronym for acronym in allowed_depts)
)
2024-01-20 17:37:24 +01:00
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):
2024-07-24 17:34:30 +02:00
"""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:
2024-01-20 17:37:24 +01:00
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):
"""
2024-07-24 17:34:30 +02:00
Liste des formsemestres qu'un étudiant a suivi, triés par ordre chronologique.
Accès par etudid, nip ou ine.
2024-07-24 17:34:30 +02:00
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`.
2024-07-24 17:34:30 +02:00
Si accès par département, ne retourne que les formsemestres suivis dans le département.
"""
2022-05-11 00:59:51 +02:00
if etudid is not None:
q_etud = Identite.query.filter_by(id=etudid)
2022-05-11 00:59:51 +02:00
elif nip is not None:
q_etud = Identite.query.filter_by(code_nip=nip)
2022-05-11 00:59:51 +02:00
elif ine is not None:
q_etud = Identite.query.filter_by(code_ine=ine)
2022-05-11 00:59:51 +02:00
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)
2022-05-11 00:59:51 +02:00
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},
)
2023-02-15 16:15:53 +01:00
@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},
)
2023-02-15 16:15:53 +01:00
@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",
2022-05-17 16:07:46 +02:00
pdf: bool = False,
2023-02-15 16:15:53 +01:00
with_img_signatures_pdf: bool = True,
):
"""
Retourne le bulletin d'un étudiant dans un formsemestre.
2024-07-23 16:49:11 +02:00
PARAMS
------
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).
SAMPLES
-------
/etudiant/etudid/1/formsemestre/1/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
2022-05-11 00:59:51 +02:00
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)
2024-02-11 12:05:43 +01:00
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(
2023-02-15 16:15:53 +01:00
formsemestre,
etud,
2023-02-15 16:15:53 +01:00
version=version,
fmt="pdf",
2023-02-15 16:15:53 +01:00
with_img_signatures_pdf=with_img_signatures_pdf,
)
return pdf_response
return sco_bulletins.get_formsemestre_bulletin_etud_json(
2022-07-05 20:37:38 +02:00
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é
2024-07-24 17:34:30 +02:00
PARAMS
------
formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant
SAMPLES
-------
/etudiant/etudid/1/formsemestre/1/groups
"""
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()
2022-05-11 00:59:51 +02:00
if formsemestre is None:
return json_error(
2022-05-11 00:59:51 +02:00
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)
2022-05-11 00:59:51 +02:00
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):
2024-07-24 17:34:30 +02:00
"""Création d'un nouvel étudiant.
Si force, crée même si homonymie détectée.
2024-07-24 17:34:30 +02:00
L'étudiant créé n'est pas inscrit à un semestre.
2024-07-24 17:34:30 +02:00
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)
2024-01-20 17:37:24 +01:00
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)
2024-02-11 12:05:43 +01:00
@as_json
def etudiant_edit(
code_type: str = "etudid",
code: str = None,
):
2024-07-23 16:49:11 +02:00
"""Édition des données étudiant (identité, admission, adresses).
2024-07-24 17:34:30 +02:00
PARAMS
------
`code_type`: le type du code, `etudid`, `ine` ou `nip`.
`code`: la valeur du code
SAMPLES
-------
/etudiant/ine/INE1/edit;{""prenom"":""Nouveau Prénom"", ""adresses"":[{""email"":""nouvelle@adresse.fr""}]}
2024-07-23 16:49:11 +02:00
"""
dept: Departement = (
db.session.get(Departement, g.scodoc_dept_id) if g.scodoc_dept else None
)
ok, etud = _get_etud_by_code(code_type, code, dept)
2024-02-11 12:05:43 +01:00
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)
2024-01-20 17:37:24 +01:00
restrict = not current_user.has_permission(Permission.ViewEtudData)
r = etud.to_dict_api(restrict=restrict)
return r
2024-02-11 12:05:43 +01:00
@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,
):
2024-07-24 17:34:30 +02:00
"""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
}
```
SAMPLES
-------
/etudiant/etudid/1/annotation;{""comment"":""une annotation sur l'étudiant""}
2024-07-24 17:34:30 +02:00
"""
2024-02-11 12:05:43 +01:00
if not current_user.has_permission(Permission.ViewEtudData):
return json_error(403, "non autorisé (manque ViewEtudData)")
dept: Departement = (
db.session.get(Departement, g.scodoc_dept_id) if g.scodoc_dept else None
)
ok, etud = _get_etud_by_code(code_type, code, dept)
2024-02-11 12:05:43 +01:00
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
):
"""
2024-07-24 17:34:30 +02:00
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
2024-02-11 12:05:43 +01:00
"""
dept: Departement = (
db.session.get(Departement, g.scodoc_dept_id) if g.scodoc_dept else None
)
ok, etud = _get_etud_by_code(code_type, code, dept)
2024-02-11 12:05:43 +01:00
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"