############################################################################## # 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 """ 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/") @api_web_bp.route("/assiduite/") @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//count", defaults={"with_query": False}) @bp.route("/assiduites//count/query", defaults={"with_query": True}) @api_web_bp.route("/assiduites//count", defaults={"with_query": False}) @api_web_bp.route("/assiduites//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//count Un filtrage peut être donné avec une query chemin : /assiduites//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: # 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", "count") return jsonify(scass.get_assiduite_stats(etud=etud, metric=metric, filter=filter)) @bp.route("/assiduites/", defaults={"with_query": False}) @bp.route("/assiduites//query", defaults={"with_query": True}) @api_web_bp.route("/assiduites/", defaults={"with_query": False}) @api_web_bp.route("/assiduites//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/ Un filtrage peut être donné avec une query chemin : /assiduites//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] = [] for ass in assiduites.all(): data = ass.to_dict() data_set.append(change_etat(data)) return jsonify(data_set) @bp.route("/assiduites/formsemestre/") @api_web_bp.route("/assiduites/formsemestre/") @login_required @scodoc @permission_required(Permission.ScoView) 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//create", methods=["POST"], defaults={"batch": False}) @api_web_bp.route( "/assiduite//create", methods=["POST"], defaults={"batch": False} ) @bp.route( "/assiduite//create/batch", methods=["POST"], defaults={"batch": True} ) @api_web_bp.route( "/assiduite//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 (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.ScoAssiduiteChange) def delete(batch: bool = False): """ Suppression d'une assiduité à partir de son id """ output = {} 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() return jsonify(output) 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//edit", methods=["POST"], defaults={"batch": False} ) @api_web_bp.route( "/assiduite//edit", methods=["POST"], defaults={"batch": False} ) @bp.route( "/assiduite//edit/batch", methods=["POST"], defaults={"batch": True}, ) @api_web_bp.route( "/assiduite//edit/batch", methods=["POST"], defaults={"batch": True}, ) @login_required @scodoc @permission_required(Permission.ScoAssiduiteChange) def edit(assiduiteid: int, batch: bool = False): """ 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 ): raise Exception 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