ScoDoc/app/api/assiduites.py

601 lines
18 KiB
Python

##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""ScoDoc 9 API : Assiduités
"""
from datetime import datetime
from typing import List
from flask import g, jsonify, request
from app import db
from app.api import api_bp as bp, api_web_bp
from app.scodoc.sco_utils import json_error
from app.decorators import scodoc, permission_required
from app.scodoc.sco_permissions import Permission
from flask_login import login_required
from app.models import Identite, Assiduite, FormSemestreInscription, FormSemestre
import app.scodoc.sco_utils as scu
import app.scodoc.sco_assiduites as scass
@bp.route("/assiduite/<int:assiduiteid>")
@api_web_bp.route("/assiduite/<int:assiduiteid>")
@scodoc
@permission_required(Permission.ScoView)
def assiduite(assiduiteid: int = None):
"""Retourne un objet assiduité à partir de son id
Exemple de résultat:
{
"assiduiteid": 1,
"etudid": 2,
"moduleimpl_id": 3,
"date_debut": "2022-10-31T08:00+01:00",
"date_fin": "2022-10-31T10:00+01:00",
"etat": "retard"
}
"""
assiduite = Assiduite.query.get(assiduiteid)
if assiduite is None:
return json_error(404, message="assiduité inexistante")
data = assiduite.to_dict()
return jsonify(change_etat(data))
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
@bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
@api_web_bp.route("/assiduites/<int:etudid>/count/query", defaults={"with_query": True})
@login_required
@scodoc
@permission_required(Permission.ScoView)
def count_assiduites(etudid: int = None, with_query: bool = False):
"""
Retourne le nombre d'assiduités d'un étudiant
chemin : /assiduites/<int:etudid>/count
Un filtrage peut être donné avec une query
chemin : /assiduites/<int:etudid>/count/query?
Les différents filtres :
Type (type de comptage -> journee, demi, heure, nombre d'assiduite):
query?type=(journee, demi, heure) -> une seule valeur parmis les trois
ex: .../query?type=heure
Comportement par défaut : compte le nombre d'assiduité enregistrée
Etat (etat de l'étudiant -> absent, present ou retard):
query?etat=[- liste des états séparé par une virgule -]
ex: .../query?etat=present,retard
Date debut
(date de début de l'assiduité, sont affichés les assiduités
dont la date de début est supérieur ou égale à la valeur donnée):
query?date_debut=[- date au format iso -]
ex: query?date_debut=2022-11-03T08:00+01:00
Date fin
(date de fin de l'assiduité, sont affichés les assiduités
dont la date de fin est inférieure ou égale à la valeur donnée):
query?date_fin=[- date au format iso -]
ex: query?date_fin=2022-11-03T10:00+01:00
Moduleimpl_id (l'id du module concerné par l'assiduité):
query?moduleimpl_id=[- int ou vide -]
ex: query?moduleimpl_id=1234
query?moduleimpl_od=
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
query?formsemstre_id=[int]
ex query?formsemestre_id=3
"""
query = Identite.query.filter_by(id=etudid)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
etud: Identite = query.first_or_404(etudid)
filter: dict[str, object] = {}
metric: str = "all"
if with_query:
metric, filter = count_manager(request)
return jsonify(
scass.get_assiduites_stats(
assiduites=etud.assiduites, metric=metric, filter=filter
)
)
@bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
@bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
@api_web_bp.route("/assiduites/<int:etudid>", defaults={"with_query": False})
@api_web_bp.route("/assiduites/<int:etudid>/query", defaults={"with_query": True})
@login_required
@scodoc
@permission_required(Permission.ScoView)
def assiduites(etudid: int = None, with_query: bool = False):
"""
Retourne toutes les assiduités d'un étudiant
chemin : /assiduites/<int:etudid>
Un filtrage peut être donné avec une query
chemin : /assiduites/<int:etudid>/query?
Les différents filtres :
Etat (etat de l'étudiant -> absent, present ou retard):
query?etat=[- liste des états séparé par une virgule -]
ex: .../query?etat=present,retard
Date debut
(date de début de l'assiduité, sont affichés les assiduités
dont la date de début est supérieur ou égale à la valeur donnée):
query?date_debut=[- date au format iso -]
ex: query?date_debut=2022-11-03T08:00+01:00
Date fin
(date de fin de l'assiduité, sont affichés les assiduités
dont la date de fin est inférieure ou égale à la valeur donnée):
query?date_fin=[- date au format iso -]
ex: query?date_fin=2022-11-03T10:00+01:00
Moduleimpl_id (l'id du module concerné par l'assiduité):
query?moduleimpl_id=[- int ou vide -]
ex: query?moduleimpl_id=1234
query?moduleimpl_od=
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
query?formsemstre_id=[int]
ex query?formsemestre_id=3
"""
query = Identite.query.filter_by(id=etudid)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
etud: Identite = query.first_or_404(etudid)
assiduites = etud.assiduites
if with_query:
assiduites = filter_manager(request, assiduites)
data_set: List[dict] = []
for ass in assiduites.all():
data = ass.to_dict()
data_set.append(change_etat(data))
return jsonify(data_set)
@bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
)
@api_web_bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
)
@bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>/query",
defaults={"with_query": True},
)
@api_web_bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>/query",
defaults={"with_query": True},
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
etuds = formsemestre.etuds.all()
etuds_id = [etud.id for etud in etuds]
assiduites = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
assiduites = scass.filter_by_formsemstre(assiduites, formsemestre)
if with_query:
assiduites = filter_manager(request, assiduites)
data_set: List[dict] = []
for ass in assiduites.all():
data = ass.to_dict()
data_set.append(change_etat(data))
return jsonify(data_set)
@bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>/count",
defaults={"with_query": False},
)
@api_web_bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>/count",
defaults={"with_query": False},
)
@bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
defaults={"with_query": True},
)
@api_web_bp.route(
"/assiduites/formsemestre/<int:formsemestre_id>/count/query",
defaults={"with_query": True},
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
def count_assiduites_formsemestre(
formsemestre_id: int = None, with_query: bool = False
):
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
etuds = formsemestre.etuds.all()
etuds_id = [etud.id for etud in etuds]
assiduites = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id))
assiduites = scass.filter_by_formsemstre(assiduites, formsemestre)
metric: str = "all"
filter: dict = {}
if with_query:
metric, filter = count_manager(request)
return jsonify(scass.get_assiduites_stats(assiduites, metric, filter))
@bp.route("/assiduite/<int:etudid>/create", methods=["POST"], defaults={"batch": False})
@api_web_bp.route(
"/assiduite/<int:etudid>/create", methods=["POST"], defaults={"batch": False}
)
@bp.route(
"/assiduite/<int:etudid>/create/batch", methods=["POST"], defaults={"batch": True}
)
@api_web_bp.route(
"/assiduite/<int:etudid>/create/batch", methods=["POST"], defaults={"batch": True}
)
@scodoc
@login_required
@permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange)
def create(etudid: int = None, batch: bool = False):
"""
Création d'une assiduité pour l'étudiant (etudid)
La requête doit avoir un content type "application/json":
{
"date_debut": str,
"date_fin": str,
"etat": str,
}
ou
{
"date_debut": str,
"date_fin": str,
"etat": str,
"moduleimpl_id": int,
}
"""
etud: Identite = Identite.query.filter_by(id=etudid).first_or_404()
if batch:
errors: dict[int, str] = {}
success: dict[
int,
] = {}
for i, data in enumerate(request.get_json(force=True).get("batch")):
code, obj = create_singular(data, etud)
if code == 404:
errors[i] = obj
else:
success[i] = obj
return jsonify({"errors": errors, "success": success})
else:
code, obj = create_singular(request.get_json(force=True), etud)
if code == 404:
return json_error(code, obj)
else:
return jsonify(obj)
def create_singular(
data: dict,
etud: Identite,
) -> tuple[int, object]:
errors: List[str] = []
# -- vérifications de l'objet json --
# cas 1 : ETAT
etat = data.get("etat", None)
if etat is None:
errors.append("param 'etat': manquant")
elif etat not in scu.ETATS_ASSIDUITE.keys():
errors.append("param 'etat': invalide")
data = change_etat(data, False)
etat = data.get("etat", None)
# cas 2 : date_debut
date_debut = data.get("date_debut", None)
if date_debut is None:
errors.append("param 'date_debut': manquant")
deb = scu.is_iso_formated(date_debut, convert=True)
if deb is None:
errors.append("param 'date_debut': format invalide")
# cas 3 : date_fin
date_fin = data.get("date_fin", None)
if date_fin is None:
errors.append("param 'date_fin': manquant")
fin = scu.is_iso_formated(date_fin, convert=True)
if fin is None:
errors.append(f"param 'date_fin': format invalide")
# cas 4 : moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", None)
if moduleimpl_id is not None:
try:
moduleimpl_id: int = int(moduleimpl_id)
if moduleimpl_id < 0:
raise Exception
except:
errors.append("param 'moduleimpl_id': invalide")
if errors != []:
err: str = ", ".join(errors)
return (404, err)
# TOUT EST OK
nouv_assiduite: Assiduite or int = Assiduite.create_assiduite(
date_debut=deb,
date_fin=fin,
etat=etat,
etud=etud,
module=moduleimpl_id,
)
if type(nouv_assiduite) is Assiduite:
db.session.add(nouv_assiduite)
db.session.commit()
return (200, {"assiduiteid": nouv_assiduite.assiduiteid})
return (
404,
{
1: "La période sélectionnée est déjà couverte par une autre assiduite",
2: "L'étudiant ne participe pas au moduleimpl sélectionné",
}.get(nouv_assiduite),
)
@bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False})
@api_web_bp.route("/assiduite/delete", methods=["POST"], defaults={"batch": False})
@bp.route(
"/assiduite/delete/batch",
methods=["POST"],
defaults={"batch": True},
)
@api_web_bp.route(
"/assiduite/delete/batch",
methods=["POST"],
defaults={"batch": True},
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange)
def delete(batch: bool = False):
"""
Suppression d'une assiduité à partir de son id
"""
if batch:
assiduites: list[int] = request.get_json(force=True).get("batch", [])
output = {"errors": {}, "success": {}}
for i, ass in enumerate(assiduites):
code, msg = delete_singular(ass, db)
if code == 404:
output["errors"][f"{i}"] = msg
else:
output["success"][f"{i}"] = {"OK": True}
db.session.commit()
return jsonify(output)
else:
code, msg = delete_singular(
request.get_json(force=True).get("assiduiteid", -1), db
)
if code == 404:
return json_error(code, msg)
if code == 200:
db.session.commit()
return jsonify({"OK": True})
def delete_singular(assiduite_id: int, db):
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first()
if assiduite is None:
return (404, "Assiduite non existante")
db.session.delete(assiduite)
return (200, "OK")
@bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"])
@api_web_bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange)
def edit(assiduiteid: int):
"""
Edition d'une assiduité à partir de son id
La requête doit avoir un content type "application/json":
{
"etat": str,
"moduleimpl_id": int
}
"""
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduiteid).first_or_404()
errors: List[str] = []
data = request.get_json(force=True)
# Vérifications de data
# Cas 1 : Etat
if data.get("etat") is not None:
data = change_etat(data, False)
if data.get("etat") is None:
errors.append("param 'etat': invalide")
else:
assiduite.etat = data.get("etat")
# Cas 2 : Moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", False)
if moduleimpl_id is not False:
try:
if moduleimpl_id is not None:
moduleimpl_id: int = int(moduleimpl_id)
if moduleimpl_id < 0 or not Assiduite.verif_moduleimpl(
moduleimpl_id, assiduite.etudid
):
errors.append("param 'moduleimpl_id': etud non inscrit")
assiduite.moduleimpl_id = moduleimpl_id
except:
errors.append("param 'moduleimpl_id': invalide")
if errors != []:
err: str = ", ".join(errors)
return json_error(404, err)
db.session.add(assiduite)
db.session.commit()
return jsonify({"OK": True})
# -- Utils --
def change_etat(data: dict, from_int: bool = True):
"""change dans un json la valeur du champs état"""
if from_int:
data["etat"] = scu.ETAT_ASSIDUITE_NAME.get(data["etat"])
else:
data["etat"] = scu.ETATS_ASSIDUITE.get(data["etat"])
return data
def count_manager(request) -> tuple[str, dict]:
"""
Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête
"""
filter: dict = {}
# cas 1 : etat assiduite
etat = request.args.get("etat")
if etat is not None:
filter["etat"] = etat
# cas 2 : date de début
deb = request.args.get("date_debut")
deb: datetime = scu.is_iso_formated(deb, True)
if deb is not None:
filter["date_debut"] = deb
# cas 3 : date de fin
fin = request.args.get("date_fin")
fin = scu.is_iso_formated(fin, True)
if fin is not None:
filter["date_fin"] = fin
# cas 4 : moduleimpl_id
module = request.args.get("moduleimpl_id", False)
try:
if module is False:
raise Exception
if module != "":
module = int(module)
else:
module = None
except Exception:
module = False
if module is not False:
filter["moduleimpl_id"] = module
# cas 5 : formsemestre_id
formsemestre_id = request.args.get("formsemestre_id")
if formsemestre_id is not None:
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
filter["formsemestre"] = formsemestre
# cas 6 : type
metric = request.args.get("metric", "all")
return (metric, filter)
def filter_manager(request, assiduites):
"""
Retourne les assiduites entrées filtrées en fonction de la request
"""
# cas 1 : etat assiduite
etat = request.args.get("etat")
if etat is not None:
assiduites = scass.filter_by_etat(assiduites, etat)
# cas 2 : date de début
deb = request.args.get("date_debut")
deb: datetime = scu.is_iso_formated(deb, True)
if deb is not None:
assiduites = scass.filter_by_date(assiduites, deb, sup=True)
# cas 3 : date de fin
fin = request.args.get("date_fin")
fin = scu.is_iso_formated(fin, True)
if fin is not None:
assiduites = scass.filter_by_date(assiduites, fin, sup=False)
# cas 4 : moduleimpl_id
module = request.args.get("moduleimpl_id", False)
try:
if module is False:
raise Exception
if module != "":
module = int(module)
else:
module = None
except Exception:
module = False
if module is not False:
assiduites = scass.filter_by_module_impl(assiduites, module)
# cas 5 : formsemestre_id
formsemestre_id = request.args.get("formsemestre_id")
if formsemestre_id is not None:
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
assiduites = scass.filter_by_formsemstre(assiduites, formsemestre)
return assiduites