############################################################################## # 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 flask_login import login_required import app.scodoc.sco_assiduites as scass import app.scodoc.sco_utils as scu from app import db from app.api import api_bp as bp from app.api import api_web_bp from app.decorators import permission_required, scodoc from app.models import Assiduite, FormSemestre, Identite, ModuleImpl from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @bp.route("/assiduite/") @api_web_bp.route("/assiduite/") @scodoc @permission_required(Permission.ScoView) def assiduite(assiduite_id: int = None): """Retourne un objet assiduité à partir de son id Exemple de résultat: { "assiduite_id": 1, "etudid": 2, "moduleimpl_id": 3, "date_debut": "2022-10-31T08:00+01:00", "date_fin": "2022-10-31T10:00+01:00", "etat": "retard", "desc": "une description", } """ query = Assiduite.query.filter_by(id=assiduite_id) # if g.scodoc_dept: # query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) assiduite_query = query.first_or_404() data = assiduite_query.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) filtered: dict[str, object] = {} metric: str = "all" if with_query: metric, filtered = count_manager(request) return jsonify( scass.get_assiduites_stats( assiduites=etud.assiduites, metric=metric, filtered=filtered ) ) @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= 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_query = etud.assiduites if with_query: assiduites_query = filter_manager(request, assiduites_query) data_set: List[dict] = [] for ass in assiduites_query.all(): data = ass.to_dict() data_set.append(change_etat(data)) return jsonify(data_set) @bp.route( "/assiduites/formsemestre/", defaults={"with_query": False} ) @api_web_bp.route( "/assiduites/formsemestre/", defaults={"with_query": False} ) @bp.route( "/assiduites/formsemestre//query", defaults={"with_query": True}, ) @api_web_bp.route( "/assiduites/formsemestre//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") assiduites_query = scass.filter_by_formsemestre(Assiduite.query, formsemestre) if with_query: assiduites_query = filter_manager(request, assiduites_query) data_set: List[dict] = [] for ass in assiduites_query.all(): data = ass.to_dict() data_set.append(change_etat(data)) return jsonify(data_set) @bp.route( "/assiduites/formsemestre//count", defaults={"with_query": False}, ) @api_web_bp.route( "/assiduites/formsemestre//count", defaults={"with_query": False}, ) @bp.route( "/assiduites/formsemestre//count/query", defaults={"with_query": True}, ) @api_web_bp.route( "/assiduites/formsemestre//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_query = Assiduite.query.filter(Assiduite.etudid.in_(etuds_id)) assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre) metric: str = "all" filtered: dict = {} if with_query: metric, filtered = count_manager(request) return jsonify(scass.get_assiduites_stats(assiduites_query, metric, filtered)) @bp.route("/assiduite//create", methods=["POST"]) @api_web_bp.route("/assiduite//create", methods=["POST"]) @scodoc @login_required @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) def create(etudid: int = None): """ 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, }, { "date_debut": str, "date_fin": str, "etat": str, "moduleimpl_id": int, "desc":str, } ... ] TODO: - vérifier si l'entrée est bien une liste """ etud: Identite = Identite.query.filter_by(id=etudid).first_or_404() create_list: list[object] = request.get_json(force=True) if not isinstance(create_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") errors: dict[int, str] = {} success: dict[int, object] = {} for i, data in enumerate(create_list): code, obj = _create_singular(data, etud) if code == 404: errors[i] = obj else: success[i] = obj return jsonify({"errors": errors, "success": success}) 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: 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("param 'date_fin': format invalide") # cas 4 : moduleimpl_id moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None if moduleimpl_id is not False: moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") # cas 5 : desc desc: str = data.get("desc", None) if errors: err: str = ", ".join(errors) return (404, err) # TOUT EST OK try: nouv_assiduite: Assiduite = Assiduite.create_assiduite( date_debut=deb, date_fin=fin, etat=etat, etud=etud, moduleimpl=moduleimpl, description=desc, ) db.session.add(nouv_assiduite) db.session.commit() return (200, {"assiduite_id": nouv_assiduite.assiduite_id}) except ScoValueError as excp: return ( 404, excp.args[0], ) @bp.route("/assiduite/delete", methods=["POST"]) @api_web_bp.route("/assiduite/delete", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) def delete(): """ Suppression d'une assiduité à partir de son id Forme des données envoyées : [ , ... ] """ assiduites_list: list[int] = request.get_json(force=True) if not isinstance(assiduites_list, list): return json_error(404, "Le contenu envoyé n'est pas une liste") output = {"errors": {}, "success": {}} for i, ass in enumerate(assiduites_list): 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) def _delete_singular(assiduite_id: int, database): assiduite_unique: Assiduite = Assiduite.query.filter_by(id=assiduite_id).first() if assiduite_unique is None: return (404, "Assiduite non existante") database.session.delete(assiduite_unique) return (200, "OK") @bp.route("/assiduite//edit", methods=["POST"]) @api_web_bp.route("/assiduite//edit", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoView) # @permission_required(Permission.ScoAssiduiteChange) def edit(assiduite_id: int): """ Edition d'une assiduité à partir de son id La requête doit avoir un content type "application/json": { "etat"?: str, "moduleimpl_id"?: int "desc"?: str } """ assiduite_unique: Assiduite = Assiduite.query.filter_by( id=assiduite_id ).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_unique.etat = data.get("etat") # Cas 2 : Moduleimpl_id moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl: ModuleImpl = None if moduleimpl_id is not False: if moduleimpl_id is not None: moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() if moduleimpl is None: errors.append("param 'moduleimpl_id': invalide") else: if not moduleimpl.est_inscrit( Identite.query.filter_by(id=assiduite_unique.etudid).first() ): errors.append("param 'moduleimpl_id': etud non inscrit") else: assiduite_unique.moduleimpl_id = moduleimpl_id else: assiduite_unique.moduleimpl_id = moduleimpl_id # Cas 3 : desc desc = data.get("desc", False) if desc is not False: assiduite_unique.desc = desc if errors: err: str = ", ".join(errors) return json_error(404, err) db.session.add(assiduite_unique) 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(requested) -> tuple[str, dict]: """ Retourne la/les métriques à utiliser ainsi que le filtre donnés en query de la requête """ filtered: dict = {} # cas 1 : etat assiduite etat = requested.args.get("etat") if etat is not None: filtered["etat"] = etat # cas 2 : date de début deb = requested.args.get("date_debut") deb: datetime = scu.is_iso_formated(deb, True) if deb is not None: filtered["date_debut"] = deb # cas 3 : date de fin fin = requested.args.get("date_fin") fin = scu.is_iso_formated(fin, True) if fin is not None: filtered["date_fin"] = fin # cas 4 : moduleimpl_id module = requested.args.get("moduleimpl_id", False) try: if module is False: raise ValueError if module != "": module = int(module) else: module = None except ValueError: module = False if module is not False: filtered["moduleimpl_id"] = module # cas 5 : formsemestre_id formsemestre_id = requested.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() filtered["formsemestre"] = formsemestre # cas 6 : type metric = requested.args.get("metric", "all") return (metric, filtered) def filter_manager(requested, assiduites_query): """ Retourne les assiduites entrées filtrées en fonction de la request """ # cas 1 : etat assiduite etat = requested.args.get("etat") if etat is not None: assiduites_query = scass.filter_by_etat(assiduites_query, etat) # cas 2 : date de début deb = requested.args.get("date_debut") deb: datetime = scu.is_iso_formated(deb, True) if deb is not None: assiduites_query = scass.filter_by_date(assiduites_query, deb, sup=True) # cas 3 : date de fin fin = requested.args.get("date_fin") fin = scu.is_iso_formated(fin, True) if fin is not None: assiduites_query = scass.filter_by_date(assiduites_query, fin, sup=False) # cas 4 : moduleimpl_id module = requested.args.get("moduleimpl_id", False) try: if module is False: raise ValueError if module != "": module = int(module) else: module = None except ValueError: module = False if module is not False: assiduites_query = scass.filter_by_module_impl(assiduites_query, module) # cas 5 : formsemestre_id formsemestre_id = requested.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_query = scass.filter_by_formsemestre(assiduites_query, formsemestre) return assiduites_query