1
0
forked from ScoDoc/ScoDoc

API Assiduites :

- Comptage
- Filtrage par Formsemestre
- Envoi en lot (create / delete)
This commit is contained in:
iziram 2022-12-14 17:41:57 +01:00
parent 0a1a847044
commit 3ff4abd19c
4 changed files with 526 additions and 134 deletions

View File

@ -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

View File

@ -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

View 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)

View File

@ -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."""