forked from ScoDoc/ScoDoc
API Assiduites :
- Comptage - Filtrage par Formsemestre - Envoi en lot (create / delete)
This commit is contained in:
parent
0a1a847044
commit
3ff4abd19c
@ -6,25 +6,33 @@
|
|||||||
"""ScoDoc 9 API : Assiduités
|
"""ScoDoc 9 API : Assiduités
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pytz import UTC
|
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from flask import g, jsonify, request
|
from flask import g, jsonify, request
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
|
||||||
from app.api import api_bp as bp
|
from app.api import api_bp as bp, api_web_bp
|
||||||
from app.scodoc.sco_utils import json_error
|
from app.scodoc.sco_utils import json_error
|
||||||
from app.decorators import scodoc, permission_required
|
from app.decorators import scodoc, permission_required
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from flask_login import login_required
|
from flask_login import login_required
|
||||||
|
|
||||||
|
|
||||||
from app.models import Identite, Assiduite
|
from app.models import Identite, Assiduite, FormSemestreInscription, FormSemestre
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
|
import app.scodoc.sco_assiduites as scass
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
TODO:
|
||||||
|
- Faire une nouvelle route pour obtenir toutes les assiduites d'un formsemstre
|
||||||
|
- Ajouter des variantes "batch" pour chaque route POST pour envoyer des listes d'objets
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/assiduite/<int:assiduiteid>")
|
@bp.route("/assiduite/<int:assiduiteid>")
|
||||||
|
@api_web_bp.route("/assiduite/<int:assiduiteid>")
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def assiduite(assiduiteid: int = None):
|
def assiduite(assiduiteid: int = None):
|
||||||
@ -33,10 +41,10 @@ def assiduite(assiduiteid: int = None):
|
|||||||
Exemple de résultat:
|
Exemple de résultat:
|
||||||
{
|
{
|
||||||
"assiduiteid": 1,
|
"assiduiteid": 1,
|
||||||
"etuid": 2,
|
"etudid": 2,
|
||||||
"moduleimpl_id": 3,
|
"moduleimpl_id": 3,
|
||||||
"date_debut": "2022-10-31T08:00",
|
"date_debut": "2022-10-31T08:00+01:00",
|
||||||
"date_fin": "2022-10-31T10:00",
|
"date_fin": "2022-10-31T10:00+01:00",
|
||||||
"etat": "retard"
|
"etat": "retard"
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
@ -50,82 +58,250 @@ def assiduite(assiduiteid: int = None):
|
|||||||
return jsonify(change_etat(data))
|
return jsonify(change_etat(data))
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/assiduites/<int:etuid>", defaults={"with_query": False})
|
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
|
||||||
@bp.route("/assiduites/<int:etuid>/query", defaults={"with_query": True})
|
@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
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def assiduites(etuid: int = None, with_query: bool = False):
|
def count_assiduites(etudid: int = None, with_query: bool = False):
|
||||||
"""Retourne toutes les assiduités d'un étudiant"""
|
"""
|
||||||
query = Identite.query.filter_by(id=etuid)
|
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:
|
if g.scodoc_dept:
|
||||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
|
||||||
etud: Identite = query.first_or_404(etuid)
|
etud: Identite = query.first_or_404(etudid)
|
||||||
assiduites: List[Assiduite] = etud.assiduites.all()
|
filter: dict[str, object] = {}
|
||||||
|
metric: str = "all"
|
||||||
|
|
||||||
if with_query:
|
if with_query:
|
||||||
# cas 1 : etat assiduite
|
# cas 1 : etat assiduite
|
||||||
etat = request.args.get("etat")
|
etat = request.args.get("etat")
|
||||||
if etat is not None:
|
if etat is not None:
|
||||||
etat = list(etat.split(","))
|
filter["etat"] = etat
|
||||||
etat = [scu.ETATS_ASSIDUITE.get(e, "absent") for e in etat]
|
|
||||||
assiduites = [ass for ass in assiduites if ass.etat in etat]
|
|
||||||
|
|
||||||
# cas 2 : date de début
|
# cas 2 : date de début
|
||||||
deb = request.args.get("date_debut")
|
deb = request.args.get("date_debut")
|
||||||
deb: datetime = is_iso_formated(deb, True)
|
deb: datetime = scu.is_iso_formated(deb, True)
|
||||||
|
|
||||||
if deb is not None:
|
if deb is not None:
|
||||||
filtered_assiduites = []
|
filter["date_debut"] = deb
|
||||||
for ass in assiduites:
|
|
||||||
if deb.tzinfo is None:
|
|
||||||
deb: datetime = deb.replace(tzinfo=ass.date_debut.tzinfo)
|
|
||||||
|
|
||||||
if ass.date_debut >= deb:
|
|
||||||
filtered_assiduites.append(ass)
|
|
||||||
assiduites.clear()
|
|
||||||
assiduites.extend(filtered_assiduites)
|
|
||||||
|
|
||||||
# cas 3 : date de fin
|
# cas 3 : date de fin
|
||||||
fin = request.args.get("date_fin")
|
fin = request.args.get("date_fin")
|
||||||
fin = is_iso_formated(fin, True)
|
fin = scu.is_iso_formated(fin, True)
|
||||||
|
|
||||||
if fin is not None:
|
if fin is not None:
|
||||||
filtered_assiduites = []
|
filter["date_fin"] = fin
|
||||||
for ass in assiduites:
|
|
||||||
if fin.tzinfo is None:
|
|
||||||
fin: datetime = fin.replace(tzinfo=ass.date_fin.tzinfo)
|
|
||||||
|
|
||||||
if ass.date_fin <= fin:
|
|
||||||
filtered_assiduites.append(ass)
|
|
||||||
assiduites.clear()
|
|
||||||
assiduites.extend(filtered_assiduites)
|
|
||||||
|
|
||||||
# cas 4 : moduleimpl_id
|
# cas 4 : moduleimpl_id
|
||||||
module = request.args.get("moduleimpl_id")
|
module = request.args.get("moduleimpl_id", False)
|
||||||
try:
|
try:
|
||||||
module = int(module)
|
if module is False:
|
||||||
|
raise Exception
|
||||||
|
if module != "":
|
||||||
|
module = int(module)
|
||||||
|
else:
|
||||||
|
module = None
|
||||||
except Exception:
|
except Exception:
|
||||||
module = None
|
module = False
|
||||||
|
|
||||||
if module is not None:
|
if module is not False:
|
||||||
assiduites = [ass for ass in assiduites if ass.moduleimpl_id == module]
|
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", "count")
|
||||||
|
|
||||||
|
return jsonify(scass.get_assiduite_stats(etud=etud, 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=
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
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:
|
||||||
|
# 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)
|
||||||
|
|
||||||
data_set: List[dict] = []
|
data_set: List[dict] = []
|
||||||
for ass in assiduites:
|
for ass in assiduites.all():
|
||||||
data = ass.to_dict()
|
data = ass.to_dict()
|
||||||
data_set.append(change_etat(data))
|
data_set.append(change_etat(data))
|
||||||
|
|
||||||
return jsonify(data_set)
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/assiduite/<int:etuid>/create", methods=["POST"])
|
@bp.route("/assiduites/formsemestre/<int:formsemestre_id>")
|
||||||
|
@api_web_bp.route("/assiduites/formsemestre/<int:formsemestre_id>")
|
||||||
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
def create(etuid: int = None):
|
def assiduites_formsemestre(formsemestre_id: int = None):
|
||||||
|
if formsemestre_id is not None:
|
||||||
|
formsemestre: FormSemestre = None
|
||||||
|
formsemestre_id = int(formsemestre_id)
|
||||||
|
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
|
||||||
|
|
||||||
|
etuds = formsemestre.etuds.all()
|
||||||
|
assiduites = []
|
||||||
|
for etud in etuds:
|
||||||
|
assiduites.extend(
|
||||||
|
scass.filter_by_formsemstre(etud.assiduites, formsemestre).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
data_set: List[dict] = []
|
||||||
|
for ass in assiduites:
|
||||||
|
data = ass.to_dict()
|
||||||
|
data_set.append(change_etat(data))
|
||||||
|
|
||||||
|
return jsonify(data_set)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return json_error(404, "le paramètre 'formsemestre_id doit être renseigné'")
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def create(etudid: int = None, batch: bool = False):
|
||||||
"""
|
"""
|
||||||
Création d'une assiduité pour l'étudiant (etuid)
|
Création d'une assiduité pour l'étudiant (etudid)
|
||||||
La requête doit avoir un content type "application/json":
|
La requête doit avoir un content type "application/json":
|
||||||
{
|
{
|
||||||
"date_debut": str,
|
"date_debut": str,
|
||||||
@ -142,9 +318,33 @@ def create(etuid: int = None):
|
|||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
etud: Identite = Identite.query.filter_by(id=etuid).first_or_404()
|
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
|
||||||
|
|
||||||
data = request.get_json(force=True)
|
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] = []
|
errors: List[str] = []
|
||||||
|
|
||||||
# -- vérifications de l'objet json --
|
# -- vérifications de l'objet json --
|
||||||
@ -162,7 +362,7 @@ def create(etuid: int = None):
|
|||||||
date_debut = data.get("date_debut", None)
|
date_debut = data.get("date_debut", None)
|
||||||
if date_debut is None:
|
if date_debut is None:
|
||||||
errors.append("param 'date_debut': manquant")
|
errors.append("param 'date_debut': manquant")
|
||||||
deb = is_iso_formated(date_debut, True)
|
deb = scu.is_iso_formated(date_debut, convert=True)
|
||||||
if deb is None:
|
if deb is None:
|
||||||
errors.append("param 'date_debut': format invalide")
|
errors.append("param 'date_debut': format invalide")
|
||||||
|
|
||||||
@ -170,7 +370,7 @@ def create(etuid: int = None):
|
|||||||
date_fin = data.get("date_fin", None)
|
date_fin = data.get("date_fin", None)
|
||||||
if date_fin is None:
|
if date_fin is None:
|
||||||
errors.append("param 'date_fin': manquant")
|
errors.append("param 'date_fin': manquant")
|
||||||
fin = is_iso_formated(date_fin, True)
|
fin = scu.is_iso_formated(date_fin, convert=True)
|
||||||
if fin is None:
|
if fin is None:
|
||||||
errors.append(f"param 'date_fin': format invalide")
|
errors.append(f"param 'date_fin': format invalide")
|
||||||
|
|
||||||
@ -187,10 +387,10 @@ def create(etuid: int = None):
|
|||||||
|
|
||||||
if errors != []:
|
if errors != []:
|
||||||
err: str = ", ".join(errors)
|
err: str = ", ".join(errors)
|
||||||
return json_error(404, err)
|
return (404, err)
|
||||||
|
|
||||||
# TOUT EST OK
|
# TOUT EST OK
|
||||||
nouv_assiduite: Assiduite or str = Assiduite.create_assiduite(
|
nouv_assiduite: Assiduite or int = Assiduite.create_assiduite(
|
||||||
date_debut=deb,
|
date_debut=deb,
|
||||||
date_fin=fin,
|
date_fin=fin,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
@ -199,9 +399,11 @@ def create(etuid: int = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if type(nouv_assiduite) is Assiduite:
|
if type(nouv_assiduite) is Assiduite:
|
||||||
return jsonify({"assiduiteid": nouv_assiduite.assiduiteid})
|
db.session.add(nouv_assiduite)
|
||||||
|
db.session.commit()
|
||||||
|
return (200, {"assiduiteid": nouv_assiduite.assiduiteid})
|
||||||
|
|
||||||
return json_error(
|
return (
|
||||||
404,
|
404,
|
||||||
{
|
{
|
||||||
1: "La période sélectionnée est déjà couverte par une autre assiduite",
|
1: "La période sélectionnée est déjà couverte par une autre assiduite",
|
||||||
@ -210,25 +412,74 @@ def create(etuid: int = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/assiduite/<int:assiduiteid>/delete", methods=["POST"])
|
@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
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoAssiduiteChange)
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
def delete(assiduiteid: int):
|
def delete(batch: bool = False):
|
||||||
"""
|
"""
|
||||||
Suppression d'une assiduité à partir de son id
|
Suppression d'une assiduité à partir de son id
|
||||||
"""
|
"""
|
||||||
assiduite: Assiduite = Assiduite.query.filter_by(id=assiduiteid).first_or_404()
|
output = {}
|
||||||
db.session.delete(assiduite)
|
if batch:
|
||||||
|
assiduites: list[int] = request.get_json(force=True).get("batch", [])
|
||||||
|
output = {"errors": [], "success": []}
|
||||||
|
|
||||||
|
for ass in assiduites:
|
||||||
|
code, msg = delete_singular(ass, db)
|
||||||
|
if code == 404:
|
||||||
|
output["errors"].append({ass: msg})
|
||||||
|
else:
|
||||||
|
output["success"].append({ass: msg})
|
||||||
|
else:
|
||||||
|
code, msg = delete_singular(
|
||||||
|
request.get_json(force=True).get("assiduite_id", -1), db
|
||||||
|
)
|
||||||
|
output[code] = msg
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return jsonify({"OK": True})
|
return jsonify(output)
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/assiduite/<int:assiduiteid>/edit", methods=["POST"])
|
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"], defaults={"batch": False}
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduite/<int:assiduiteid>/edit", methods=["POST"], defaults={"batch": False}
|
||||||
|
)
|
||||||
|
@bp.route(
|
||||||
|
"/assiduite/<int:assiduiteid>/edit/batch",
|
||||||
|
methods=["POST"],
|
||||||
|
defaults={"batch": True},
|
||||||
|
)
|
||||||
|
@api_web_bp.route(
|
||||||
|
"/assiduite/<int:assiduiteid>/edit/batch",
|
||||||
|
methods=["POST"],
|
||||||
|
defaults={"batch": True},
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoAssiduiteChange)
|
@permission_required(Permission.ScoAssiduiteChange)
|
||||||
def edit(assiduiteid: int):
|
def edit(assiduiteid: int, batch: bool = False):
|
||||||
"""
|
"""
|
||||||
Edition d'une assiduité à partir de son id
|
Edition d'une assiduité à partir de son id
|
||||||
La requête doit avoir un content type "application/json":
|
La requête doit avoir un content type "application/json":
|
||||||
@ -285,25 +536,3 @@ def change_etat(data: dict, from_int: bool = True):
|
|||||||
else:
|
else:
|
||||||
data["etat"] = scu.ETATS_ASSIDUITE.get(data["etat"])
|
data["etat"] = scu.ETATS_ASSIDUITE.get(data["etat"])
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def is_iso_formated(date: str, convert=False) -> bool or datetime or None:
|
|
||||||
"""
|
|
||||||
Vérifie si une date est au format iso
|
|
||||||
|
|
||||||
Retourne un booléen Vrai (ou un objet Datetime si convert = True)
|
|
||||||
si l'objet est au format iso
|
|
||||||
|
|
||||||
Retourne Faux si l'objet n'est pas au format et convert = False
|
|
||||||
|
|
||||||
Retourne None sinon
|
|
||||||
"""
|
|
||||||
import dateutil.parser as dtparser
|
|
||||||
|
|
||||||
try:
|
|
||||||
date: datetime = dtparser.isoparse(date)
|
|
||||||
if date.tzinfo is None:
|
|
||||||
date = UTC.localize(date)
|
|
||||||
return date if convert else True
|
|
||||||
except Exception:
|
|
||||||
return None if convert else False
|
|
||||||
|
@ -2,13 +2,11 @@
|
|||||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||||
"""
|
"""
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import ModuleImpl
|
from app.models import ModuleImpl, ModuleImplInscription
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.formsemestre import FormSemestre
|
from app.scodoc.sco_utils import EtatAssiduite, localize_datetime, verif_interval
|
||||||
from app.scodoc.sco_utils import EtatAssiduite
|
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Tuple, List
|
|
||||||
|
|
||||||
|
|
||||||
class Assiduite(db.Model):
|
class Assiduite(db.Model):
|
||||||
@ -60,16 +58,22 @@ class Assiduite(db.Model):
|
|||||||
date_debut: datetime,
|
date_debut: datetime,
|
||||||
date_fin: datetime,
|
date_fin: datetime,
|
||||||
etat: EtatAssiduite,
|
etat: EtatAssiduite,
|
||||||
module: int or None = None or int,
|
module: int or None = None,
|
||||||
) -> object or int:
|
) -> object or int:
|
||||||
"Créer une nouvelle assiduité pour l'étudiant"
|
"""Créer une nouvelle assiduité pour l'étudiant
|
||||||
|
Documentation des codes d'erreurs renvoyés:
|
||||||
|
1: Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)
|
||||||
|
2: l'ID du module_impl n'existe pas.
|
||||||
|
"""
|
||||||
# Vérification de non duplication des périodes
|
# Vérification de non duplication des périodes
|
||||||
assiduites: List[Assiduite] = etud.assiduites.all()
|
assiduites: list[Assiduite] = etud.assiduites.all()
|
||||||
assiduites = [
|
assiduites = [
|
||||||
ass
|
ass
|
||||||
for ass in assiduites
|
for ass in assiduites
|
||||||
if verif_interval((date_debut, date_fin), (ass.date_debut, ass.date_fin))
|
if verif_interval(
|
||||||
|
(localize_datetime(date_debut), localize_datetime(date_fin)),
|
||||||
|
(ass.date_debut, ass.date_fin),
|
||||||
|
)
|
||||||
]
|
]
|
||||||
if len(assiduites) != 0:
|
if len(assiduites) != 0:
|
||||||
return 1
|
return 1
|
||||||
@ -78,8 +82,8 @@ class Assiduite(db.Model):
|
|||||||
# Vérification de l'existance du module pour l'étudiant
|
# Vérification de l'existance du module pour l'étudiant
|
||||||
if cls.verif_moduleimpl(module, etud):
|
if cls.verif_moduleimpl(module, etud):
|
||||||
nouv_assiduite = Assiduite(
|
nouv_assiduite = Assiduite(
|
||||||
date_debut=date_debut.isoformat(),
|
date_debut=date_debut,
|
||||||
date_fin=date_fin.isoformat(),
|
date_fin=date_fin,
|
||||||
etat=etat,
|
etat=etat,
|
||||||
etudiant=etud,
|
etudiant=etud,
|
||||||
moduleimpl_id=module,
|
moduleimpl_id=module,
|
||||||
@ -93,8 +97,7 @@ class Assiduite(db.Model):
|
|||||||
etat=etat,
|
etat=etat,
|
||||||
etudiant=etud,
|
etudiant=etud,
|
||||||
)
|
)
|
||||||
db.session.add(nouv_assiduite)
|
|
||||||
db.session.commit()
|
|
||||||
return nouv_assiduite
|
return nouv_assiduite
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -104,28 +107,22 @@ class Assiduite(db.Model):
|
|||||||
|
|
||||||
Retourne Vrai si c'est le cas, faux sinon
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
"""
|
"""
|
||||||
# -> get obj module impl -> get obj formsemestres -> query etuds avec etuid -> si vide = Error sinon good
|
output = True
|
||||||
|
|
||||||
module: ModuleImpl = ModuleImpl.query.filter_by(
|
module: ModuleImpl = ModuleImpl.query.filter_by(
|
||||||
moduleimpl_id=moduleimpl_id
|
moduleimpl_id=moduleimpl_id
|
||||||
).first()
|
).first()
|
||||||
if module is None:
|
if module is None:
|
||||||
retour = False
|
output = False
|
||||||
|
|
||||||
semestre: FormSemestre = FormSemestre.query.filter_by(
|
if output:
|
||||||
id=module.formsemestre_id
|
search_etudid: int = etud.id if type(etud) == Identite else etud
|
||||||
).first()
|
is_module: int = ModuleImplInscription.query.filter_by(
|
||||||
if semestre is None:
|
etudid=search_etudid, moduleimpl_id=moduleimpl_id
|
||||||
retour = False
|
).count()
|
||||||
|
|
||||||
etudiants: List[Identite] = semestre.etuds.all()
|
output = is_module > 0
|
||||||
|
|
||||||
if type(etud) is Identite:
|
return output
|
||||||
retour = etud in etudiants
|
|
||||||
else:
|
|
||||||
retour = etud in [e.id for e in etudiants]
|
|
||||||
|
|
||||||
return retour
|
|
||||||
|
|
||||||
|
|
||||||
class Justificatif(db.Model):
|
class Justificatif(db.Model):
|
||||||
@ -171,26 +168,3 @@ class Justificatif(db.Model):
|
|||||||
"fichier": self.fichier,
|
"fichier": self.fichier,
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def verif_interval(periode: Tuple[datetime], interval: Tuple[datetime]) -> bool:
|
|
||||||
"""
|
|
||||||
Vérifie si une période est comprise dans un interval, chevauche l'interval ou comprend l'interval
|
|
||||||
|
|
||||||
Retourne Vrai si c'est le cas, faux sinon
|
|
||||||
"""
|
|
||||||
p_deb, p_fin = periode
|
|
||||||
i_deb, i_fin = interval
|
|
||||||
|
|
||||||
from app.scodoc.intervals import intervalmap
|
|
||||||
|
|
||||||
i = intervalmap()
|
|
||||||
p = intervalmap()
|
|
||||||
i[:] = 0
|
|
||||||
p[:] = 0
|
|
||||||
i[i_deb:i_fin] = 1
|
|
||||||
p[p_deb:p_fin] = 1
|
|
||||||
|
|
||||||
res: int = sum((i[p_deb], i[p_fin], p[i_deb], p[i_fin]))
|
|
||||||
|
|
||||||
return res > 0
|
|
||||||
|
130
app/scodoc/sco_assiduites.py
Normal file
130
app/scodoc/sco_assiduites.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
from app.models.etudiants import Identite
|
||||||
|
from app.models.formsemestre import FormSemestre
|
||||||
|
from app.models.assiduites import Assiduite
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
from datetime import datetime, date, time, timedelta
|
||||||
|
|
||||||
|
# TOTALK: Réfléchir sur le fractionnement d'une assiduite prolongée
|
||||||
|
|
||||||
|
|
||||||
|
def get_assiduite_stats(
|
||||||
|
etud: Identite, metric: str = "all", filter: dict[str, object] = {}
|
||||||
|
) -> Assiduite:
|
||||||
|
assiduites: Assiduite = etud.assiduites
|
||||||
|
|
||||||
|
if filter != {}:
|
||||||
|
for key in filter:
|
||||||
|
if key == "etat":
|
||||||
|
assiduites = filter_by_etat(assiduites, filter[key])
|
||||||
|
elif key == "date_fin":
|
||||||
|
assiduites = filter_by_date(assiduites, filter[key], sup=False)
|
||||||
|
elif key == "date_debut":
|
||||||
|
assiduites = filter_by_date(assiduites, filter[key], sup=True)
|
||||||
|
elif key == "moduleimpl_id":
|
||||||
|
assiduites = filter_by_module_impl(assiduites, filter[key])
|
||||||
|
elif key == "formsemestre":
|
||||||
|
assiduites = filter_by_formsemstre(assiduites, filter[key])
|
||||||
|
|
||||||
|
count: dict = get_count(assiduites)
|
||||||
|
|
||||||
|
metrics: list[str] = metric.split(",")
|
||||||
|
|
||||||
|
output: dict = {}
|
||||||
|
|
||||||
|
for key in count:
|
||||||
|
if key in metrics:
|
||||||
|
output[key] = count[key]
|
||||||
|
return output if output != {} else count
|
||||||
|
|
||||||
|
|
||||||
|
def get_count(assiduites: Assiduite) -> dict[str, int or float]:
|
||||||
|
|
||||||
|
output: dict[str, int or float] = {}
|
||||||
|
output["compte"] = assiduites.count()
|
||||||
|
output["heure"] = 0.0
|
||||||
|
output["journee"] = 0
|
||||||
|
output["demi"] = 0
|
||||||
|
|
||||||
|
all_assiduites: list[Assiduite] = assiduites.order_by(Assiduite.date_debut).all()
|
||||||
|
|
||||||
|
current_day: date = None
|
||||||
|
current_time: str = None
|
||||||
|
|
||||||
|
MIDNIGHT: time = time(hour=0)
|
||||||
|
NOON: time = time(hour=12)
|
||||||
|
time_check = lambda d: (MIDNIGHT <= d.time() <= NOON)
|
||||||
|
|
||||||
|
for ass in all_assiduites:
|
||||||
|
delta: timedelta = ass.date_fin - ass.date_debut
|
||||||
|
output["heure"] += delta.total_seconds() / 3600
|
||||||
|
|
||||||
|
ass_time: str = time_check(ass.date_debut)
|
||||||
|
|
||||||
|
if current_day != ass.date_debut.date():
|
||||||
|
current_day = ass.date_debut.date()
|
||||||
|
current_time = ass_time
|
||||||
|
output["demi"] += 1
|
||||||
|
output["journee"] += 1
|
||||||
|
|
||||||
|
if current_time != ass_time:
|
||||||
|
current_time = ass_time
|
||||||
|
output["demi"] += 1
|
||||||
|
|
||||||
|
output["heure"] = round(output["heure"], 2)
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de leur état
|
||||||
|
"""
|
||||||
|
etats: list[str] = list(etat.split(","))
|
||||||
|
etats = [scu.ETATS_ASSIDUITE.get(e, "") for e in etats]
|
||||||
|
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_date(
|
||||||
|
assiduites: Assiduite, date: datetime, sup: bool = True
|
||||||
|
) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'une date
|
||||||
|
|
||||||
|
Sup == True -> les assiduites doivent débuter après 'date'\n
|
||||||
|
Sup == False -> les assiduites doivent finir avant 'date'
|
||||||
|
"""
|
||||||
|
|
||||||
|
if date.tzinfo is None:
|
||||||
|
first_assiduite: Assiduite = assiduites.first()
|
||||||
|
if first_assiduite is not None:
|
||||||
|
date: datetime = date.replace(tzinfo=first_assiduite.date_debut.tzinfo)
|
||||||
|
|
||||||
|
if sup:
|
||||||
|
return assiduites.filter(Assiduite.date_debut >= date)
|
||||||
|
else:
|
||||||
|
return assiduites.filter(Assiduite.date_fin <= date)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_module_impl(
|
||||||
|
assiduites: Assiduite, module_impl_id: int or None
|
||||||
|
) -> Assiduite:
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
|
||||||
|
"""
|
||||||
|
return assiduites.filter(Assiduite.moduleimpl_id == module_impl_id)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_by_formsemstre(assiduites: Assiduite, formsemestre: FormSemestre):
|
||||||
|
"""
|
||||||
|
Filtrage d'une collection d'assiduites en fonction d'un formsemestre
|
||||||
|
"""
|
||||||
|
|
||||||
|
if formsemestre is None:
|
||||||
|
return assiduites.filter(False)
|
||||||
|
|
||||||
|
assiduites = assiduites.filter(
|
||||||
|
Identite.query.filter_by(id=Assiduite.etudid).first()
|
||||||
|
in formsemestre.etuds.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
assiduites = assiduites.filter(Assiduite.date_debut >= formsemestre.date_debut)
|
||||||
|
return assiduites.filter(Assiduite.date_fin <= formsemestre.date_fin)
|
@ -60,6 +60,8 @@ from app import log
|
|||||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||||
from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL
|
from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL
|
||||||
from app.scodoc import sco_xml
|
from app.scodoc import sco_xml
|
||||||
|
from app.scodoc.intervals import intervalmap
|
||||||
|
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
||||||
@ -126,6 +128,63 @@ ETAT_JUSTIFICATIF_NAME = {
|
|||||||
EtatJustificatif.MODIFIE: "modifié",
|
EtatJustificatif.MODIFIE: "modifié",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
|
||||||
|
"""
|
||||||
|
Vérifie si une date est au format iso
|
||||||
|
|
||||||
|
Retourne un booléen Vrai (ou un objet Datetime si convert = True)
|
||||||
|
si l'objet est au format iso
|
||||||
|
|
||||||
|
Retourne Faux si l'objet n'est pas au format et convert = False
|
||||||
|
|
||||||
|
Retourne None sinon
|
||||||
|
"""
|
||||||
|
import dateutil.parser as dtparser
|
||||||
|
|
||||||
|
try:
|
||||||
|
date: datetime.datetime = dtparser.isoparse(date)
|
||||||
|
return date if convert else True
|
||||||
|
except Exception:
|
||||||
|
return None if convert else False
|
||||||
|
|
||||||
|
|
||||||
|
def localize_datetime(date: datetime.datetime) -> datetime.datetime:
|
||||||
|
|
||||||
|
new_date: datetime.datetime = date
|
||||||
|
if date.tzinfo == None:
|
||||||
|
from app.models.assiduites import Assiduite
|
||||||
|
|
||||||
|
first_assiduite = Assiduite.query.first()
|
||||||
|
if first_assiduite is not None:
|
||||||
|
new_date = date.replace(tzinfo=first_assiduite.date_debut.tzinfo)
|
||||||
|
|
||||||
|
return new_date
|
||||||
|
|
||||||
|
|
||||||
|
def verif_interval(
|
||||||
|
periode: tuple[datetime.datetime], interval: tuple[datetime.datetime]
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si une période est comprise dans un interval, chevauche l'interval ou comprend l'interval
|
||||||
|
|
||||||
|
Retourne Vrai si c'est le cas, faux sinon
|
||||||
|
"""
|
||||||
|
p_deb, p_fin = periode
|
||||||
|
i_deb, i_fin = interval
|
||||||
|
|
||||||
|
i = intervalmap()
|
||||||
|
p = intervalmap()
|
||||||
|
i[:] = 0
|
||||||
|
p[:] = 0
|
||||||
|
i[i_deb:i_fin] = 1
|
||||||
|
p[p_deb:p_fin] = 1
|
||||||
|
|
||||||
|
res: int = sum((i[p_deb], i[p_fin], p[i_deb], p[i_fin]))
|
||||||
|
|
||||||
|
return res > 0
|
||||||
|
|
||||||
|
|
||||||
# Types de modules
|
# Types de modules
|
||||||
class ModuleType(IntEnum):
|
class ModuleType(IntEnum):
|
||||||
"""Code des types de module."""
|
"""Code des types de module."""
|
||||||
|
Loading…
Reference in New Issue
Block a user