############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ ScoDoc 9 API : accès aux formsemestres """ from flask import g, jsonify, request from flask_login import login_required import app from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR from app.decorators import scodoc, permission_required from app.scodoc.sco_utils import json_error from app.comp import res_sem from app.comp.moy_mod import ModuleImplResults from app.comp.res_compat import NotesTableCompat from app.models import ( Departement, Evaluation, FormSemestre, FormSemestreEtape, FormSemestreInscription, Identite, ModuleImpl, NotesNotes, ) from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import ModuleType import app.scodoc.sco_utils as scu from app.tables.recap import TableRecap @bp.route("/formsemestre/") @api_web_bp.route("/formsemestre/") @login_required @scodoc @permission_required(Permission.ScoView) def formsemestre_infos(formsemestre_id: int): """ Information sur le formsemestre indiqué. formsemestre_id : l'id du formsemestre Exemple de résultat : { "block_moyennes": false, "bul_bgcolor": "white", "bul_hide_xml": false, "date_debut_iso": "2021-09-01", "date_debut": "01/09/2021", "date_fin_iso": "2022-08-31", "date_fin": "31/08/2022", "dept_id": 1, "elt_annee_apo": null, "elt_sem_apo": null, "ens_can_edit_eval": false, "etat": true, "formation_id": 1, "formsemestre_id": 1, "gestion_compensation": false, "gestion_semestrielle": false, "id": 1, "modalite": "FI", "resp_can_change_ens": true, "resp_can_edit": false, "responsables": [1, 99], // uids "scodoc7_id": null, "semestre_id": 1, "titre_formation" : "BUT GEA", "titre_num": "BUT GEA semestre 1", "titre": "BUT GEA", } """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) return jsonify(formsemestre.to_dict_api()) @bp.route("/formsemestres/query") @api_web_bp.route("/formsemestres/query") @login_required @scodoc @permission_required(Permission.ScoView) def formsemestres_query(): """ Retourne les formsemestres filtrés par étape Apogée ou année scolaire ou département (acronyme ou id) etape_apo : un code étape apogée annee_scolaire : année de début de l'année scolaire dept_acronym : acronyme du département (eg "RT") dept_id : id du département ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit. """ etape_apo = request.args.get("etape_apo") annee_scolaire = request.args.get("annee_scolaire") dept_acronym = request.args.get("dept_acronym") dept_id = request.args.get("dept_id") nip = request.args.get("nip") ine = request.args.get("ine") formsemestres = FormSemestre.query if g.scodoc_dept: formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id) if annee_scolaire is not None: try: annee_scolaire_int = int(annee_scolaire) except ValueError: return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int") debut_annee = scu.date_debut_anne_scolaire(annee_scolaire_int) fin_annee = scu.date_fin_anne_scolaire(annee_scolaire_int) formsemestres = formsemestres.filter( FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee ) if dept_acronym is not None: formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym) if dept_id is not None: try: dept_id = int(dept_id) except ValueError: return json_error(404, "invalid dept_id: integer expected") formsemestres = formsemestres.filter_by(dept_id=dept_id) if etape_apo is not None: formsemestres = formsemestres.join(FormSemestreEtape).filter( FormSemestreEtape.etape_apo == etape_apo ) inscr_joined = False if nip is not None: formsemestres = ( formsemestres.join(FormSemestreInscription) .join(Identite) .filter_by(code_nip=nip) ) inscr_joined = True if ine is not None: if not inscr_joined: formsemestres = formsemestres.join(FormSemestreInscription).join(Identite) formsemestres = formsemestres.filter_by(code_ine=ine) return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) @bp.route("/formsemestre//bulletins") @bp.route("/formsemestre//bulletins/") @api_web_bp.route("/formsemestre//bulletins") @api_web_bp.route("/formsemestre//bulletins/") @login_required @scodoc @permission_required(Permission.ScoView) def bulletins(formsemestre_id: int, version: str = "long"): """ Retourne les bulletins d'un formsemestre donné formsemestre_id : l'id d'un formesemestre Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first() if formsemestre is None: return json_error(404, "formsemestre non trouve") app.set_sco_dept(formsemestre.departement.acronym) data = [] for etu in formsemestre.etuds: bul_etu = get_formsemestre_bulletin_etud_json( formsemestre, etu, version=version ) data.append(bul_etu.json) return jsonify(data) @bp.route("/formsemestre//programme") @api_web_bp.route("/formsemestre//programme") @login_required @scodoc @permission_required(Permission.ScoView) def formsemestre_programme(formsemestre_id: int): """ Retourne la liste des Ues, ressources et SAE d'un semestre formsemestre_id : l'id d'un formsemestre Exemple de résultat : { "ues": [ { "type": 0, "formation_id": 1, "ue_code": "UCOD11", "id": 1, "ects": 12.0, "acronyme": "RT1.1", "is_external": false, "numero": 1, "code_apogee": "", "titre": "Administrer les r\u00e9seaux et l\u2019Internet", "coefficient": 0.0, "semestre_idx": 1, "color": "#B80004", "ue_id": 1 }, ... ], "ressources": [ { "ens": [ 10, 18 ], "formsemestre_id": 1, "id": 15, "module": { "abbrev": "Programmer", "code": "SAE15", "code_apogee": "V7GOP", "coefficient": 1.0, "formation_id": 1, "heures_cours": 0.0, "heures_td": 0.0, "heures_tp": 0.0, "id": 15, "matiere_id": 3, "module_id": 15, "module_type": 3, "numero": 50, "semestre_id": 1, "titre": "Programmer en Python", "ue_id": 3 }, "module_id": 15, "moduleimpl_id": 15, "responsable_id": 2 }, ... ], "saes": [ { ... }, ... ], "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] } """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) ues = formsemestre.query_ues() m_list = { ModuleType.RESSOURCE: [], ModuleType.SAE: [], ModuleType.STANDARD: [], ModuleType.MALUS: [], } for modimpl in formsemestre.modimpls_sorted: d = modimpl.to_dict(convert_objects=True) m_list[modimpl.module.module_type].append(d) return jsonify( { "ues": [ue.to_dict(convert_objects=True) for ue in ues], "ressources": m_list[ModuleType.RESSOURCE], "saes": m_list[ModuleType.SAE], "modules": m_list[ModuleType.STANDARD], "malus": m_list[ModuleType.MALUS], } ) @bp.route( "/formsemestre//etudiants", defaults={"with_query": False, "long": False}, ) @bp.route( "/formsemestre//etudiants/long", defaults={"with_query": False, "long": True}, ) @bp.route( "/formsemestre//etudiants/query", defaults={"with_query": True, "long": False}, ) @bp.route( "/formsemestre//etudiants/long/query", defaults={"with_query": True, "long": True}, ) @api_web_bp.route( "/formsemestre//etudiants", defaults={"with_query": False, "long": False}, ) @api_web_bp.route( "/formsemestre//etudiants/long", defaults={"with_query": False, "long": True}, ) @api_web_bp.route( "/formsemestre//etudiants/query", defaults={"with_query": True, "long": False}, ) @api_web_bp.route( "/formsemestre//etudiants/long/query", defaults={"with_query": True, "long": True}, ) @login_required @scodoc @permission_required(Permission.ScoView) def formsemestre_etudiants( formsemestre_id: int, with_query: bool = False, long: bool = False ): """Étudiants d'un formsemestre.""" query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) if with_query: etat = request.args.get("etat") if etat is not None: etat = { "actifs": scu.INSCRIT, "demissionnaires": scu.DEMISSION, "defaillants": scu.DEF, }.get(etat, etat) inscriptions = [ ins for ins in formsemestre.inscriptions if ins.etat == etat ] else: inscriptions = formsemestre.inscriptions else: inscriptions = formsemestre.inscriptions if long: etuds = [ins.etud.to_dict_api() for ins in inscriptions] else: etuds = [ins.etud.to_dict_short() for ins in inscriptions] # Ajout des groupes de chaque étudiants # XXX A REVOIR: trop inefficace ! for etud in etuds: etud["groups"] = sco_groups.get_etud_groups( etud["id"], formsemestre_id, exclude_default=True ) return jsonify(sorted(etuds, key=lambda e: e["sort_key"])) @bp.route("/formsemestre//etat_evals") @api_web_bp.route("/formsemestre//etat_evals") @login_required @scodoc @permission_required(Permission.ScoView) def etat_evals(formsemestre_id: int): """ Informations sur l'état des évaluations d'un formsemestre. formsemestre_id : l'id d'un semestre Exemple de résultat : [ { "id": 1, // moduleimpl_id "titre": "Initiation aux réseaux informatiques", "evaluations": [ { "id": 1, "description": null, "datetime_epreuve": null, "heure_fin": "09:00:00", "coefficient": "02.00" "is_complete": true, "nb_inscrits": 16, "nb_manquantes": 0, "ABS": 0, "ATT": 0, "EXC": 0, "saisie_notes": { "datetime_debut": "2021-09-11T00:00:00+02:00", "datetime_fin": "2022-08-25T00:00:00+02:00", "datetime_mediane": "2022-03-19T00:00:00+01:00" } }, ... ] }, ] """ query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) result = [] for modimpl_id in nt.modimpls_results: modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id] modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id) modimpl_dict = modimpl.to_dict(convert_objects=True) list_eval = [] for evaluation_id in modimpl_results.evaluations_etat: eval_etat = modimpl_results.evaluations_etat[evaluation_id] evaluation = Evaluation.query.get_or_404(evaluation_id) eval_dict = evaluation.to_dict_api() eval_dict["etat"] = eval_etat.to_dict() eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module eval_dict["nb_notes_manquantes"] = len( modimpl_results.evals_etudids_sans_note[evaluation.id] ) eval_dict["nb_notes_abs"] = sum( modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE ) eval_dict["nb_notes_att"] = eval_etat.nb_attente eval_dict["nb_notes_exc"] = sum( modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE ) # Récupération de toutes les notes de l'évaluation # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id) notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all() date_debut = None date_fin = None date_mediane = None # Si il y a plus d'une note saisie pour l'évaluation if len(notes) >= 1: # Tri des notes en fonction de leurs dates notes_sorted = sorted(notes, key=lambda note: note.date) date_debut = notes_sorted[0].date date_fin = notes_sorted[-1].date # Note médiane date_mediane = notes_sorted[len(notes_sorted) // 2].date eval_dict["saisie_notes"] = { "datetime_debut": date_debut.isoformat() if date_debut is not None else None, "datetime_fin": date_fin.isoformat() if date_fin is not None else None, "datetime_mediane": date_mediane.isoformat() if date_mediane is not None else None, } list_eval.append(eval_dict) modimpl_dict["evaluations"] = list_eval result.append(modimpl_dict) return jsonify(result) @bp.route("/formsemestre//resultats") @api_web_bp.route("/formsemestre//resultats") @login_required @scodoc @permission_required(Permission.ScoView) def formsemestre_resultat(formsemestre_id: int): """Tableau récapitulatif des résultats Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules. """ format_spec = request.args.get("format", None) if format_spec is not None and format_spec != "raw": return json_error(API_CLIENT_ERROR, "invalid format specification") convert_values = format_spec != "raw" query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) app.set_sco_dept(formsemestre.departement.acronym) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) table = res.get_table_recap( convert_values=convert_values, include_evaluations=False, mode_jury=False, allow_html=False, ) # Supprime les champs inutiles (mise en forme) rows = table.to_list() # Ajoute le groupe de chaque partition: etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id) for row in rows: row["partitions"] = etud_groups.get(row["etudid"], {}) return jsonify(rows)