From 6e86f7a9c4fbc97d06e9f112afe5d6b00084817c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 6 Apr 2023 16:10:32 +0200 Subject: [PATCH] =?UTF-8?q?G=C3=A9n=C3=A8re=20JSON=20avec=20Flask-JSON.=20?= =?UTF-8?q?Abandonne=20jsonify.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 11 +- app/api/absences.py | 12 +- app/api/billets_absences.py | 12 +- app/api/departements.py | 41 +- app/api/etudiants.py | 26 +- app/api/evaluations.py | 20 +- app/api/formations.py | 27 +- app/api/formsemestres.py | 38 +- app/api/jury.py | 5 +- app/api/logos.py | 26 +- app/api/partitions.py | 68 ++- app/api/tokens.py | 5 +- app/api/users.py | 48 +- app/entreprises/routes.py | 17 +- app/models/but_refcomp.py | 1 + app/scodoc/codes_cursus.py | 3 +- app/scodoc/sco_bulletins.py | 11 +- app/scodoc/sco_utils.py | 17 +- app/scodoc/sco_vdi.py | 6 +- app/templates/but/refcomp_show.j2 | 2 + app/views/pn_modules.py | 11 +- app/views/refcomp.py | 6 +- app/views/scolar.py | 10 +- config.py | 3 + requirements-3.9.txt | 1 + tests/api/setup_test_api.py | 6 +- tests/api/test_api_permissions.py | 4 +- .../scodoc_formation_INFO_BUT_INFO_v1.xml | 526 ------------------ .../fakedatabase/create_test_api_database.py | 2 +- 29 files changed, 272 insertions(+), 693 deletions(-) delete mode 100644 tests/ressources/formations/scodoc_formation_INFO_BUT_INFO_v1.xml diff --git a/app/__init__.py b/app/__init__.py index 0f64a7fd..d11347a8 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -17,7 +17,7 @@ import warnings from flask import current_app, g, request from flask import Flask -from flask import abort, flash, has_request_context, jsonify +from flask import abort, flash, has_request_context from flask import render_template # from flask.json import JSONEncoder @@ -25,6 +25,7 @@ from flask.logging import default_handler from flask_bootstrap import Bootstrap from flask_caching import Cache +from flask_json import FlaskJSON, json_response from flask_login import LoginManager, current_user from flask_mail import Mail from flask_migrate import Migrate @@ -139,12 +140,14 @@ def _async_dump(app, request_url: str): def handle_invalid_usage(error): - response = jsonify(error.to_dict()) + response = json_response(data_=error.to_dict()) response.status_code = error.status_code return response # JSON ENCODING +# used by some internal finctions +# the API is now using flask_son, NOT THIS ENCODER class ScoDocJSONEncoder(json.JSONEncoder): def default(self, o): # pylint: disable=E0202 if isinstance(o, (datetime.date, datetime.datetime)): @@ -251,13 +254,13 @@ class ReverseProxied(object): def create_app(config_class=DevConfig): app = Flask(__name__, static_url_path="/ScoDoc/static", static_folder="static") + app.config.from_object(config_class) from app.auth import cas CAS(app, url_prefix="/cas", configuration_function=cas.set_cas_configuration) app.wsgi_app = ReverseProxied(app.wsgi_app) - app.json_provider_class = ScoDocJSONEncoder + FlaskJSON(app) - app.config.from_object(config_class) # Pour conserver l'ordre des objets dans les JSON: # e.g. l'ordre des UE dans les bulletins app.json.sort_keys = False diff --git a/app/api/absences.py b/app/api/absences.py index 808fce3c..b2d33267 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -6,7 +6,7 @@ """ScoDoc 9 API : Absences """ -from flask import jsonify +from flask_json import as_json from app.api import api_bp as bp, API_CLIENT_ERROR from app.scodoc.sco_utils import json_error @@ -19,10 +19,12 @@ from app.scodoc import sco_abs from app.scodoc.sco_groups import get_group_members from app.scodoc.sco_permissions import Permission + # TODO XXX revoir routes web API et calcul des droits @bp.route("/absences/etudid/", methods=["GET"]) @scodoc @permission_required(Permission.ScoView) +@as_json def absences(etudid: int = None): """ Liste des absences de cet étudiant @@ -57,12 +59,13 @@ def absences(etudid: int = None): abs_list = sco_abs.list_abs_date(etud.id) for absence in abs_list: absence["jour"] = absence["jour"].isoformat() - return jsonify(abs_list) + return abs_list @bp.route("/absences/etudid//just", methods=["GET"]) @scodoc @permission_required(Permission.ScoView) +@as_json def absences_just(etudid: int = None): """ Retourne la liste des absences justifiées d'un étudiant donné @@ -103,7 +106,7 @@ def absences_just(etudid: int = None): ] for absence in abs_just: absence["jour"] = absence["jour"].isoformat() - return jsonify(abs_just) + return abs_just @bp.route( @@ -116,6 +119,7 @@ def absences_just(etudid: int = None): ) @scodoc @permission_required(Permission.ScoView) +@as_json def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): """ Liste des absences d'un groupe (possibilité de choisir entre deux dates) @@ -167,7 +171,7 @@ def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): } data.append(absence) - return jsonify(data) + return data # XXX TODO EV: A REVOIR (data json dans le POST + modifier les routes) diff --git a/app/api/billets_absences.py b/app/api/billets_absences.py index 44e6b3a3..7ada60f6 100644 --- a/app/api/billets_absences.py +++ b/app/api/billets_absences.py @@ -8,7 +8,8 @@ API : billets d'absences """ -from flask import g, jsonify, request +from flask import g, request +from flask_json import as_json from flask_login import login_required from app import db @@ -26,10 +27,11 @@ from app.scodoc.sco_permissions import Permission @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def billets_absence_etudiant(etudid: int): """Liste des billets d'absence pour cet étudiant""" billets = sco_abs_billets.query_billets_etud(etudid) - return jsonify([billet.to_dict() for billet in billets]) + return [billet.to_dict() for billet in billets] @bp.route("/billets_absence/create", methods=["POST"]) @@ -37,6 +39,7 @@ def billets_absence_etudiant(etudid: int): @login_required @scodoc @permission_required(Permission.ScoAbsAddBillet) +@as_json def billets_absence_create(): """Ajout d'un billet d'absence""" data = request.get_json(force=True) # may raise 400 Bad Request @@ -60,7 +63,7 @@ def billets_absence_create(): ) db.session.add(billet) db.session.commit() - return jsonify(billet.to_dict()) + return billet.to_dict() @bp.route("/billets_absence//delete", methods=["POST"]) @@ -68,6 +71,7 @@ def billets_absence_create(): @login_required @scodoc @permission_required(Permission.ScoAbsAddBillet) +@as_json def billets_absence_delete(billet_id: int): """Suppression d'un billet d'absence""" query = BilletAbsence.query.filter_by(id=billet_id) @@ -77,4 +81,4 @@ def billets_absence_delete(billet_id: int): billet = query.first_or_404() db.session.delete(billet) db.session.commit() - return jsonify({"OK": True}) + return {"OK": True} diff --git a/app/api/departements.py b/app/api/departements.py index d7ce28ef..a5d87bb5 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -12,7 +12,8 @@ """ from datetime import datetime -from flask import jsonify, request +from flask import request +from flask_json import as_json from flask_login import login_required import app @@ -41,24 +42,27 @@ def get_departement(dept_ident: str) -> Departement: @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def departements_list(): """Liste les départements""" - return jsonify([dept.to_dict(with_dept_name=True) for dept in Departement.query]) + return [dept.to_dict(with_dept_name=True) for dept in Departement.query] @bp.route("/departements_ids") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def departements_ids(): """Liste des ids de départements""" - return jsonify([dept.id for dept in Departement.query]) + return [dept.id for dept in Departement.query] @bp.route("/departement/") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def departement(acronym: str): """ Info sur un département. Accès par acronyme. @@ -74,25 +78,27 @@ def departement(acronym: str): } """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() - return jsonify(dept.to_dict(with_dept_name=True)) + return dept.to_dict(with_dept_name=True) @bp.route("/departement/id/") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def departement_by_id(dept_id: int): """ Info sur un département. Accès par id. """ dept = Departement.query.get_or_404(dept_id) - return jsonify(dept.to_dict()) + return dept.to_dict() @bp.route("/departement/create", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def departement_create(): """ Création d'un département. @@ -111,13 +117,14 @@ def departement_create(): dept = departements.create_dept(acronym, visible=visible) except ScoValueError as exc: return json_error(500, exc.args[0] if exc.args else "") - return jsonify(dept.to_dict()) + return dept.to_dict() @bp.route("/departement//edit", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def departement_edit(acronym): """ Edition d'un département: seul visible peut être modifié @@ -135,7 +142,7 @@ def departement_edit(acronym): dept.visible = visible db.session.add(dept) db.session.commit() - return jsonify(dept.to_dict()) + return dept.to_dict() @bp.route("/departement//delete", methods=["POST"]) @@ -149,13 +156,14 @@ def departement_delete(acronym): dept = Departement.query.filter_by(acronym=acronym).first_or_404() db.session.delete(dept) db.session.commit() - return jsonify({"OK": True}) + return {"OK": True} @bp.route("/departement//etudiants", methods=["GET"]) @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def dept_etudiants(acronym: str): """ Retourne la liste des étudiants d'un département @@ -179,45 +187,49 @@ def dept_etudiants(acronym: str): ] """ dept = Departement.query.filter_by(acronym=acronym).first_or_404() - return jsonify([etud.to_dict_short() for etud in dept.etudiants]) + return [etud.to_dict_short() for etud in dept.etudiants] @bp.route("/departement/id//etudiants") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def dept_etudiants_by_id(dept_id: int): """ Retourne la liste des étudiants d'un département d'id donné. """ dept = Departement.query.get_or_404(dept_id) - return jsonify([etud.to_dict_short() for etud in dept.etudiants]) + return [etud.to_dict_short() for etud in dept.etudiants] @bp.route("/departement//formsemestres_ids") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def dept_formsemestres_ids(acronym: str): """liste des ids formsemestre du département""" dept = Departement.query.filter_by(acronym=acronym).first_or_404() - return jsonify([formsemestre.id for formsemestre in dept.formsemestres]) + return [formsemestre.id for formsemestre in dept.formsemestres] @bp.route("/departement/id//formsemestres_ids") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def dept_formsemestres_ids_by_id(dept_id: int): """liste des ids formsemestre du département""" dept = Departement.query.get_or_404(dept_id) - return jsonify([formsemestre.id for formsemestre in dept.formsemestres]) + return [formsemestre.id for formsemestre in dept.formsemestres] @bp.route("/departement//formsemestres_courants") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def dept_formsemestres_courants(acronym: str): """ Liste des semestres actifs d'un département d'acronyme donné @@ -269,13 +281,14 @@ def dept_formsemestres_courants(acronym: str): FormSemestre.date_debut <= test_date, FormSemestre.date_fin >= test_date, ) - return jsonify([d.to_dict_api() for d in formsemestres]) + return [d.to_dict_api() for d in formsemestres] @bp.route("/departement/id//formsemestres_courants") @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def dept_formsemestres_courants_by_id(dept_id: int): """ Liste des semestres actifs d'un département d'id donné @@ -294,4 +307,4 @@ def dept_formsemestres_courants_by_id(dept_id: int): FormSemestre.date_fin >= test_date, ) - return jsonify([d.to_dict_api() for d in formsemestres]) + return [d.to_dict_api() for d in formsemestres] diff --git a/app/api/etudiants.py b/app/api/etudiants.py index af0aa32f..07df3d59 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -9,7 +9,8 @@ """ from datetime import datetime -from flask import abort, g, jsonify, request +from flask import g, request +from flask_json import as_json from flask_login import current_user from flask_login import login_required from sqlalchemy import desc, or_ @@ -37,11 +38,11 @@ from app.scodoc.sco_permissions import Permission # @login_required # @scodoc # @permission_required(Permission.ScoView) +# @as_json # def api_function(arg: int): # """Une fonction quelconque de l'API""" -# return jsonify( -# {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept} -# ) +# return {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept} +# @bp.route("/etudiants/courants", defaults={"long": False}) @@ -51,6 +52,7 @@ from app.scodoc.sco_permissions import Permission @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def etudiants_courants(long=False): """ La liste des étudiants des semestres "courants" (tous départements) @@ -96,7 +98,7 @@ def etudiants_courants(long=False): data = [etud.to_dict_api() for etud in etuds] else: data = [etud.to_dict_short() for etud in etuds] - return jsonify(data) + return data @bp.route("/etudiant/etudid/") @@ -108,6 +110,7 @@ def etudiants_courants(long=False): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def etudiant(etudid: int = None, nip: str = None, ine: str = None): """ Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé. @@ -127,7 +130,7 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None): message="étudiant inconnu", ) - return jsonify(etud.to_dict_api()) + return etud.to_dict_api() @bp.route("/etudiants/etudid/", methods=["GET"]) @@ -138,6 +141,7 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None): @api_web_bp.route("/etudiants/ine/", methods=["GET"]) @scodoc @permission_required(Permission.ScoView) +@as_json def etudiants(etudid: int = None, nip: str = None, ine: str = None): """ Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie @@ -163,7 +167,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None): etuds = etuds.join(Departement).filter( or_(Departement.acronym == acronym for acronym in allowed_depts) ) - return jsonify([etud.to_dict_api() for etud in query]) + return [etud.to_dict_api() for etud in query] @bp.route("/etudiant/etudid//formsemestres") @@ -174,6 +178,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None): @api_web_bp.route("/etudiant/ine//formsemestres") @scodoc @permission_required(Permission.ScoView) +@as_json def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): """ Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique. @@ -206,7 +211,7 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) formsemestres = query.order_by(FormSemestre.date_debut) - return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) + return [formsemestre.to_dict_api() for formsemestre in formsemestres] @bp.route( @@ -265,7 +270,7 @@ def bulletin( formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() if g.scodoc_dept and dept.acronym != g.scodoc_dept: - return json_error(404, "formsemestre inexistant") + return json_error(404, "formsemestre inexistant", as_response=True) app.set_sco_dept(dept.acronym) if code_type == "nip": @@ -303,6 +308,7 @@ def bulletin( ) @scodoc @permission_required(Permission.ScoView) +@as_json def etudiant_groups(formsemestre_id: int, etudid: int = None): """ Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué @@ -352,4 +358,4 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): app.set_sco_dept(dept.acronym) data = sco_groups.get_etud_groups(etud.id, formsemestre.id) - return jsonify(data) + return data diff --git a/app/api/evaluations.py b/app/api/evaluations.py index 5dd0f057..d5a7a3cf 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -8,7 +8,8 @@ ScoDoc 9 API : accès aux évaluations """ -from flask import g, jsonify +from flask import g +from flask_json import as_json from flask_login import login_required import app @@ -26,7 +27,8 @@ import app.scodoc.sco_utils as scu @login_required @scodoc @permission_required(Permission.ScoView) -def evaluation(evaluation_id: int): +@as_json +def the_eval(evaluation_id: int): """Description d'une évaluation. { @@ -56,7 +58,7 @@ def evaluation(evaluation_id: int): .filter_by(dept_id=g.scodoc_dept_id) ) e = query.first_or_404() - return jsonify(e.to_dict_api()) + return e.to_dict_api() @bp.route("/moduleimpl//evaluations") @@ -64,6 +66,7 @@ def evaluation(evaluation_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def evaluations(moduleimpl_id: int): """ Retourne la liste des évaluations d'un moduleimpl @@ -79,7 +82,7 @@ def evaluations(moduleimpl_id: int): .join(FormSemestre) .filter_by(dept_id=g.scodoc_dept_id) ) - return jsonify([e.to_dict_api() for e in query]) + return [e.to_dict_api() for e in query] @bp.route("/evaluation//notes") @@ -87,6 +90,7 @@ def evaluations(moduleimpl_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def evaluation_notes(evaluation_id: int): """ Retourne la liste des notes à partir de l'id d'une évaluation donnée @@ -124,8 +128,8 @@ def evaluation_notes(evaluation_id: int): .filter_by(dept_id=g.scodoc_dept_id) ) - evaluation = query.first_or_404() - dept = evaluation.moduleimpl.formsemestre.departement + the_eval = query.first_or_404() + dept = the_eval.moduleimpl.formsemestre.departement app.set_sco_dept(dept.acronym) notes = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) @@ -133,7 +137,7 @@ def evaluation_notes(evaluation_id: int): # "ABS", "EXC", etc mais laisse les notes sur le barème de l'éval. note = notes[etudid] note["value"] = scu.fmt_note(note["value"], keep_numeric=True) - note["note_max"] = evaluation.note_max + note["note_max"] = the_eval.note_max del note["id"] - return jsonify(notes) + return notes diff --git a/app/api/formations.py b/app/api/formations.py index 4eec8669..80c68022 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -8,7 +8,8 @@ ScoDoc 9 API : accès aux formations """ -from flask import g, jsonify, request +from flask import g, request +from flask_json import as_json from flask_login import login_required import app @@ -18,7 +19,6 @@ from app.scodoc.sco_utils import json_error from app.decorators import scodoc, permission_required from app.models import ApcParcours, Formation, FormSemestre, ModuleImpl, UniteEns from app.scodoc import sco_formations -from app.scodoc.sco_exceptions import ScoFormationConflict from app.scodoc.sco_permissions import Permission @@ -27,6 +27,7 @@ from app.scodoc.sco_permissions import Permission @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formations(): """ Retourne la liste de toutes les formations (tous départements) @@ -35,7 +36,7 @@ def formations(): if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) - return jsonify([d.to_dict() for d in query]) + return [d.to_dict() for d in query] @bp.route("/formations_ids") @@ -43,6 +44,7 @@ def formations(): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formations_ids(): """ Retourne la liste de toutes les id de formations (tous départements) @@ -52,7 +54,7 @@ def formations_ids(): query = Formation.query if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) - return jsonify([d.id for d in query]) + return [d.id for d in query] @bp.route("/formation/") @@ -60,6 +62,7 @@ def formations_ids(): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formation_by_id(formation_id: int): """ La formation d'id donné @@ -84,7 +87,7 @@ def formation_by_id(formation_id: int): query = Formation.query.filter_by(id=formation_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) - return jsonify(query.first_or_404().to_dict()) + return query.first_or_404().to_dict() @bp.route( @@ -106,6 +109,7 @@ def formation_by_id(formation_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formation_export_by_formation_id(formation_id: int, export_ids=False): """ Retourne la formation, avec UE, matières, modules @@ -212,7 +216,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): except ValueError: return json_error(500, message="Erreur inconnue") - return jsonify(data) + return data @bp.route("/formation//referentiel_competences") @@ -220,6 +224,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def referentiel_competences(formation_id: int): """ Retourne le référentiel de compétences @@ -233,8 +238,8 @@ def referentiel_competences(formation_id: int): query = query.filter_by(dept_id=g.scodoc_dept_id) formation = query.first_or_404(formation_id) if formation.referentiel_competence is None: - return jsonify(None) - return jsonify(formation.referentiel_competence.to_dict()) + return None + return formation.referentiel_competence.to_dict() @bp.route("/moduleimpl/") @@ -242,6 +247,7 @@ def referentiel_competences(formation_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def moduleimpl(moduleimpl_id: int): """ Retourne un moduleimpl en fonction de son id @@ -281,7 +287,7 @@ def moduleimpl(moduleimpl_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) modimpl: ModuleImpl = query.first_or_404() - return jsonify(modimpl.to_dict(convert_objects=True)) + return modimpl.to_dict(convert_objects=True) @bp.route("/set_ue_parcours/", methods=["POST"]) @@ -289,6 +295,7 @@ def moduleimpl(moduleimpl_id: int): @login_required @scodoc @permission_required(Permission.ScoChangeFormation) +@as_json def set_ue_parcours(ue_id: int): """Associe UE et parcours BUT. La liste des ids de parcours est passée en argument JSON. @@ -307,4 +314,4 @@ def set_ue_parcours(ue_id: int): ] log(f"set_ue_parcours: ue_id={ue.id} parcours_ids={parcours_ids}") ok, error_message = ue.set_parcours(parcours) - return jsonify({"status": ok, "message": error_message}) + return {"status": ok, "message": error_message} diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index 6f0ff414..e7ca242f 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -9,7 +9,8 @@ """ from operator import attrgetter, itemgetter -from flask import g, jsonify, request +from flask import g, request +from flask_json import as_json from flask_login import login_required import app @@ -42,6 +43,7 @@ from app.tables.recap import TableRecap @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formsemestre_infos(formsemestre_id: int): """ Information sur le formsemestre indiqué. @@ -83,7 +85,7 @@ def formsemestre_infos(formsemestre_id: int): 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()) + return formsemestre.to_dict_api() @bp.route("/formsemestres/query") @@ -91,6 +93,7 @@ def formsemestre_infos(formsemestre_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formsemestres_query(): """ Retourne les formsemestres filtrés par @@ -146,7 +149,7 @@ def formsemestres_query(): formsemestres = formsemestres.join(FormSemestreInscription).join(Identite) formsemestres = formsemestres.filter_by(code_ine=ine) - return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) + return [formsemestre.to_dict_api() for formsemestre in formsemestres] @bp.route("/formsemestre//bulletins") @@ -156,6 +159,7 @@ def formsemestres_query(): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def bulletins(formsemestre_id: int, version: str = "long"): """ Retourne les bulletins d'un formsemestre donné @@ -179,7 +183,7 @@ def bulletins(formsemestre_id: int, version: str = "long"): ) data.append(bul_etu.json) - return jsonify(data) + return data @bp.route("/formsemestre//programme") @@ -187,6 +191,7 @@ def bulletins(formsemestre_id: int, version: str = "long"): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formsemestre_programme(formsemestre_id: int): """ Retourne la liste des Ues, ressources et SAE d'un semestre @@ -266,15 +271,13 @@ def formsemestre_programme(formsemestre_id: int): 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], - } - ) + return { + "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( @@ -312,6 +315,7 @@ def formsemestre_programme(formsemestre_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formsemestre_etudiants( formsemestre_id: int, with_query: bool = False, long: bool = False ): @@ -347,7 +351,7 @@ def formsemestre_etudiants( etud["id"], formsemestre_id, exclude_default=True ) - return jsonify(sorted(etuds, key=itemgetter("sort_key"))) + return sorted(etuds, key=itemgetter("sort_key")) @bp.route("/formsemestre//etat_evals") @@ -355,6 +359,7 @@ def formsemestre_etudiants( @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def etat_evals(formsemestre_id: int): """ Informations sur l'état des évaluations d'un formsemestre. @@ -456,7 +461,7 @@ def etat_evals(formsemestre_id: int): modimpl_dict["evaluations"] = list_eval result.append(modimpl_dict) - return jsonify(result) + return result @bp.route("/formsemestre//resultats") @@ -464,6 +469,7 @@ def etat_evals(formsemestre_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json 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. @@ -489,4 +495,4 @@ def formsemestre_resultat(formsemestre_id: int): for row in rows: row["partitions"] = etud_groups.get(row["etudid"], {}) - return jsonify(rows) + return rows diff --git a/app/api/jury.py b/app/api/jury.py index 24ff2c27..28c0ae8b 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -8,7 +8,7 @@ ScoDoc 9 API : jury WIP """ -from flask import jsonify +from flask_json import as_json from flask_login import login_required import app @@ -25,6 +25,7 @@ from app.scodoc.sco_permissions import Permission @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def decisions_jury(formsemestre_id: int): """Décisions du jury des étudiants du formsemestre.""" # APC, pair: @@ -32,6 +33,6 @@ def decisions_jury(formsemestre_id: int): if formsemestre.formation.is_apc(): app.set_sco_dept(formsemestre.departement.acronym) rows = jury_but_results.get_jury_but_results(formsemestre) - return jsonify(rows) + return rows else: raise ScoException("non implemente") diff --git a/app/api/logos.py b/app/api/logos.py index 677c2b7b..db3a28cd 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -30,11 +30,10 @@ Contrib @jmp """ from datetime import datetime -from flask import jsonify, g, send_file -from flask_login import login_required +from flask import Response, send_file +from flask_json import as_json -from app.api import api_bp as bp, api_web_bp -from app.api import requested_format +from app.api import api_bp as bp from app.scodoc.sco_utils import json_error from app.models import Departement from app.scodoc.sco_logos import list_logos, find_logo @@ -47,10 +46,11 @@ from app.scodoc.sco_permissions import Permission @bp.route("/logos") @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def api_get_glob_logos(): """Liste tous les logos""" logos = list_logos()[None] - return jsonify(list(logos.keys())) + return list(logos.keys()) @bp.route("/logo/") @@ -68,27 +68,29 @@ def api_get_glob_logo(logoname): ) -def core_get_logos(dept_id): +def _core_get_logos(dept_id) -> list: logos = list_logos().get(dept_id, dict()) - return jsonify(list(logos.keys())) + return list(logos.keys()) @bp.route("/departement//logos") @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def api_get_local_logos_by_acronym(departement): dept_id = Departement.from_acronym(departement).id - return core_get_logos(dept_id) + return _core_get_logos(dept_id) @bp.route("/departement/id//logos") @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def api_get_local_logos_by_id(dept_id): - return core_get_logos(dept_id) + return _core_get_logos(dept_id) -def core_get_logo(dept_id, logoname): +def _core_get_logo(dept_id, logoname) -> Response: logo = find_logo(logoname=logoname, dept_id=dept_id) if logo is None: return json_error(404, message="logo not found") @@ -105,11 +107,11 @@ def core_get_logo(dept_id, logoname): @permission_required(Permission.ScoSuperAdmin) def api_get_local_logo_dept_by_acronym(departement, logoname): dept_id = Departement.from_acronym(departement).id - return core_get_logo(dept_id, logoname) + return _core_get_logo(dept_id, logoname) @bp.route("/departement/id//logo/") @scodoc @permission_required(Permission.ScoSuperAdmin) def api_get_local_logo_dept_by_id(dept_id, logoname): - return core_get_logo(dept_id, logoname) + return _core_get_logo(dept_id, logoname) diff --git a/app/api/partitions.py b/app/api/partitions.py index 6ca3e455..b31ff80d 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -9,7 +9,8 @@ """ from operator import attrgetter -from flask import g, jsonify, request +from flask import g, request +from flask_json import as_json from flask_login import login_required import app @@ -31,6 +32,7 @@ from app.scodoc import sco_utils as scu @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def partition_info(partition_id: int): """Info sur une partition. @@ -55,7 +57,7 @@ def partition_info(partition_id: int): if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition = query.first_or_404() - return jsonify(partition.to_dict(with_groups=True)) + return partition.to_dict(with_groups=True) @bp.route("/formsemestre//partitions") @@ -63,6 +65,7 @@ def partition_info(partition_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def formsemestre_partitions(formsemestre_id: int): """Liste de toutes les partitions d'un formsemestre @@ -88,13 +91,11 @@ def formsemestre_partitions(formsemestre_id: int): query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id) partitions = sorted(formsemestre.partitions, key=attrgetter("numero")) - return jsonify( - { - partition.id: partition.to_dict(with_groups=True) - for partition in partitions - if partition.partition_name is not None - } - ) + return { + partition.id: partition.to_dict(with_groups=True) + for partition in partitions + if partition.partition_name is not None + } @bp.route("/group//etudiants") @@ -102,6 +103,7 @@ def formsemestre_partitions(formsemestre_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def etud_in_group(group_id: int): """ Retourne la liste des étudiants dans un groupe @@ -128,7 +130,7 @@ def etud_in_group(group_id: int): query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() - return jsonify([etud.to_dict_short() for etud in group.etuds]) + return [etud.to_dict_short() for etud in group.etuds] @bp.route("/group//etudiants/query") @@ -136,6 +138,7 @@ def etud_in_group(group_id: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def etud_in_group_query(group_id: int): """Étudiants du groupe, filtrés par état""" etat = request.args.get("etat") @@ -156,7 +159,7 @@ def etud_in_group_query(group_id: int): query = query.join(group_membership).filter_by(group_id=group_id) - return jsonify([etud.to_dict_short() for etud in query]) + return [etud.to_dict_short() for etud in query] @bp.route("/group//set_etudiant/", methods=["POST"]) @@ -164,6 +167,7 @@ def etud_in_group_query(group_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def set_etud_group(etudid: int, group_id: int): """Affecte l'étudiant au groupe indiqué""" etud = Identite.query.get_or_404(etudid) @@ -182,7 +186,7 @@ def set_etud_group(etudid: int, group_id: int): etudid, group_id, group.partition.to_dict() ) - return jsonify({"group_id": group_id, "etudid": etudid}) + return {"group_id": group_id, "etudid": etudid} @bp.route("/group//remove_etudiant/", methods=["POST"]) @@ -192,6 +196,7 @@ def set_etud_group(etudid: int, group_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def group_remove_etud(group_id: int, etudid: int): """Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien.""" etud = Identite.query.get_or_404(etudid) @@ -215,7 +220,7 @@ def group_remove_etud(group_id: int, etudid: int): # Update parcours group.partition.formsemestre.update_inscriptions_parcours_from_groups() sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) - return jsonify({"group_id": group_id, "etudid": etudid}) + return {"group_id": group_id, "etudid": etudid} @bp.route( @@ -227,6 +232,7 @@ def group_remove_etud(group_id: int, etudid: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def partition_remove_etud(partition_id: int, etudid: int): """Enlève l'étudiant de tous les groupes de cette partition (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition) @@ -256,7 +262,7 @@ def partition_remove_etud(partition_id: int, etudid: int): partition.formsemestre.update_inscriptions_parcours_from_groups() app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) - return jsonify({"partition_id": partition_id, "etudid": etudid}) + return {"partition_id": partition_id, "etudid": etudid} @bp.route("/partition//group/create", methods=["POST"]) @@ -264,6 +270,7 @@ def partition_remove_etud(partition_id: int, etudid: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def group_create(partition_id: int): """Création d'un groupe dans une partition @@ -294,7 +301,7 @@ def group_create(partition_id: int): log(f"created group {group}") app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) - return jsonify(group.to_dict(with_partition=True)) + return group.to_dict(with_partition=True) @bp.route("/group//delete", methods=["POST"]) @@ -302,6 +309,7 @@ def group_create(partition_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def group_delete(group_id: int): """Suppression d'un groupe""" query = GroupDescr.query.filter_by(id=group_id) @@ -320,7 +328,7 @@ def group_delete(group_id: int): db.session.commit() app.set_sco_dept(group.partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) - return jsonify({"OK": True}) + return {"OK": True} @bp.route("/group//edit", methods=["POST"]) @@ -328,6 +336,7 @@ def group_delete(group_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def group_edit(group_id: int): """Edit a group""" query = GroupDescr.query.filter_by(id=group_id) @@ -352,7 +361,7 @@ def group_edit(group_id: int): log(f"modified {group}") app.set_sco_dept(group.partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) - return jsonify(group.to_dict(with_partition=True)) + return group.to_dict(with_partition=True) @bp.route("/formsemestre//partition/create", methods=["POST"]) @@ -362,6 +371,7 @@ def group_edit(group_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def partition_create(formsemestre_id: int): """Création d'une partition dans un semestre @@ -414,7 +424,7 @@ def partition_create(formsemestre_id: int): log(f"created partition {partition}") app.set_sco_dept(formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) - return jsonify(partition.to_dict(with_groups=True)) + return partition.to_dict(with_groups=True) @bp.route("/formsemestre//partitions/order", methods=["POST"]) @@ -424,6 +434,7 @@ def partition_create(formsemestre_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def formsemestre_order_partitions(formsemestre_id: int): """Modifie l'ordre des partitions du formsemestre JSON args: [partition_id1, partition_id2, ...] @@ -449,13 +460,11 @@ def formsemestre_order_partitions(formsemestre_id: int): db.session.commit() app.set_sco_dept(formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) - return jsonify( - [ - partition.to_dict() - for partition in formsemestre.partitions.order_by(Partition.numero) - if partition.partition_name is not None - ] - ) + return [ + partition.to_dict() + for partition in formsemestre.partitions.order_by(Partition.numero) + if partition.partition_name is not None + ] @bp.route("/partition//groups/order", methods=["POST"]) @@ -463,6 +472,7 @@ def formsemestre_order_partitions(formsemestre_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def partition_order_groups(partition_id: int): """Modifie l'ordre des groupes de la partition JSON args: [group_id1, group_id2, ...] @@ -489,7 +499,7 @@ def partition_order_groups(partition_id: int): app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) log(f"partition_order_groups: {partition} : {group_ids}") - return jsonify(partition.to_dict(with_groups=True)) + return partition.to_dict(with_groups=True) @bp.route("/partition//edit", methods=["POST"]) @@ -497,6 +507,7 @@ def partition_order_groups(partition_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def partition_edit(partition_id: int): """Modification d'une partition dans un semestre @@ -558,7 +569,7 @@ def partition_edit(partition_id: int): app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) - return jsonify(partition.to_dict(with_groups=True)) + return partition.to_dict(with_groups=True) @bp.route("/partition//delete", methods=["POST"]) @@ -566,6 +577,7 @@ def partition_edit(partition_id: int): @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) +@as_json def partition_delete(partition_id: int): """Suppression d'une partition (et de tous ses groupes). @@ -593,4 +605,4 @@ def partition_delete(partition_id: int): sco_cache.invalidate_formsemestre(formsemestre.id) if is_parcours: formsemestre.update_inscriptions_parcours_from_groups() - return jsonify({"OK": True}) + return {"OK": True} diff --git a/app/api/tokens.py b/app/api/tokens.py index 9bc5f5a2..e9ffd796 100644 --- a/app/api/tokens.py +++ b/app/api/tokens.py @@ -1,4 +1,4 @@ -from flask import jsonify +from flask_json import as_json from app import db, log from app.api import api_bp as bp from app.auth.logic import basic_auth, token_auth @@ -6,12 +6,13 @@ from app.auth.logic import basic_auth, token_auth @bp.route("/tokens", methods=["POST"]) @basic_auth.login_required +@as_json def get_token(): "renvoie un jeton jwt pour l'utilisateur courant" token = basic_auth.current_user().get_token() log(f"API: giving token to {basic_auth.current_user()}") db.session.commit() - return jsonify({"token": token}) + return {"token": token} @bp.route("/tokens", methods=["DELETE"]) diff --git a/app/api/users.py b/app/api/users.py index ab00f0a7..5cb3af44 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -9,7 +9,8 @@ """ -from flask import g, jsonify, request +from flask import g, request +from flask_json import as_json from flask_login import current_user, login_required from app import db @@ -29,6 +30,7 @@ from app.scodoc import sco_utils as scu @login_required @scodoc @permission_required(Permission.ScoUsersView) +@as_json def user_info(uid: int): """ Info sur un compte utilisateur scodoc @@ -41,7 +43,7 @@ def user_info(uid: int): if (None not in allowed_depts) and (user.dept not in allowed_depts): return json_error(404, "user not found") - return jsonify(user.to_dict()) + return user.to_dict() @bp.route("/users/query") @@ -49,6 +51,7 @@ def user_info(uid: int): @login_required @scodoc @permission_required(Permission.ScoView) +@as_json def users_info_query(): """Utilisateurs, filtrés par dept, active ou début nom /users/query?departement=dept_acronym&active=1&starts_with= @@ -79,7 +82,7 @@ def users_info_query(): ) query = query.order_by(User.user_name) - return jsonify([user.to_dict() for user in query]) + return [user.to_dict() for user in query] @bp.route("/user/create", methods=["POST"]) @@ -87,6 +90,7 @@ def users_info_query(): @login_required @scodoc @permission_required(Permission.ScoUsersAdmin) +@as_json def user_create(): """Création d'un utilisateur The request content type should be "application/json": @@ -121,7 +125,7 @@ def user_create(): user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom) db.session.add(user) db.session.commit() - return jsonify(user.to_dict()) + return user.to_dict() @bp.route("/user//edit", methods=["POST"]) @@ -129,6 +133,7 @@ def user_create(): @login_required @scodoc @permission_required(Permission.ScoUsersAdmin) +@as_json def user_edit(uid: int): """Modification d'un utilisateur Champs modifiables: @@ -165,7 +170,7 @@ def user_edit(uid: int): db.session.add(user) db.session.commit() - return jsonify(user.to_dict()) + return user.to_dict() @bp.route("/user//password", methods=["POST"]) @@ -173,6 +178,7 @@ def user_edit(uid: int): @login_required @scodoc @permission_required(Permission.ScoUsersAdmin) +@as_json def user_password(uid: int): """Modification du mot de passe d'un utilisateur Champs modifiables: @@ -194,7 +200,7 @@ def user_password(uid: int): user.set_password(password) db.session.add(user) db.session.commit() - return jsonify(user.to_dict()) + return user.to_dict() @bp.route("/user//role//add", methods=["POST"]) @@ -210,6 +216,7 @@ def user_password(uid: int): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def user_role_add(uid: int, role_name: str, dept: str = None): """Add a role to the user""" user: User = User.query.get_or_404(uid) @@ -222,7 +229,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None): user.add_role(role, dept) db.session.add(user) db.session.commit() - return jsonify(user.to_dict()) + return user.to_dict() @bp.route("/user//role//remove", methods=["POST"]) @@ -238,6 +245,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def user_role_remove(uid: int, role_name: str, dept: str = None): """Remove the role from the user""" user: User = User.query.get_or_404(uid) @@ -256,7 +264,7 @@ def user_role_remove(uid: int, role_name: str, dept: str = None): db.session.delete(user_role) db.session.add(user) db.session.commit() - return jsonify(user.to_dict()) + return user.to_dict() @bp.route("/permissions") @@ -264,9 +272,10 @@ def user_role_remove(uid: int, role_name: str, dept: str = None): @login_required @scodoc @permission_required(Permission.ScoUsersView) +@as_json def list_permissions(): """Liste des noms de permissions définies""" - return jsonify(list(Permission.permission_by_name.keys())) + return list(Permission.permission_by_name.keys()) @bp.route("/role/") @@ -274,9 +283,10 @@ def list_permissions(): @login_required @scodoc @permission_required(Permission.ScoUsersView) +@as_json def list_role(role_name: str): """Un rôle""" - return jsonify(Role.query.filter_by(name=role_name).first_or_404().to_dict()) + return Role.query.filter_by(name=role_name).first_or_404().to_dict() @bp.route("/roles") @@ -284,9 +294,10 @@ def list_role(role_name: str): @login_required @scodoc @permission_required(Permission.ScoUsersView) +@as_json def list_roles(): """Tous les rôles définis""" - return jsonify([role.to_dict() for role in Role.query]) + return [role.to_dict() for role in Role.query] @bp.route( @@ -300,6 +311,7 @@ def list_roles(): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def role_permission_add(role_name: str, perm_name: str): """Add permission to role""" role: Role = Role.query.filter_by(name=role_name).first_or_404() @@ -309,7 +321,7 @@ def role_permission_add(role_name: str, perm_name: str): role.add_permission(permission) db.session.add(role) db.session.commit() - return jsonify(role.to_dict()) + return role.to_dict() @bp.route( @@ -323,6 +335,7 @@ def role_permission_add(role_name: str, perm_name: str): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def role_permission_remove(role_name: str, perm_name: str): """Remove permission from role""" role: Role = Role.query.filter_by(name=role_name).first_or_404() @@ -332,7 +345,7 @@ def role_permission_remove(role_name: str, perm_name: str): role.remove_permission(permission) db.session.add(role) db.session.commit() - return jsonify(role.to_dict()) + return role.to_dict() @bp.route("/role/create/", methods=["POST"]) @@ -340,6 +353,7 @@ def role_permission_remove(role_name: str, perm_name: str): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def role_create(role_name: str): """Create a new role with permissions. { @@ -359,7 +373,7 @@ def role_create(role_name: str): return json_error(404, "role_create: invalid permissions") db.session.add(role) db.session.commit() - return jsonify(role.to_dict()) + return role.to_dict() @bp.route("/role//edit", methods=["POST"]) @@ -367,6 +381,7 @@ def role_create(role_name: str): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def role_edit(role_name: str): """Edit a role. On peut spécifier un nom et/ou des permissions. { @@ -390,7 +405,7 @@ def role_edit(role_name: str): role.name = role_name db.session.add(role) db.session.commit() - return jsonify(role.to_dict()) + return role.to_dict() @bp.route("/role//delete", methods=["POST"]) @@ -398,9 +413,10 @@ def role_edit(role_name: str): @login_required @scodoc @permission_required(Permission.ScoSuperAdmin) +@as_json def role_delete(role_name: str): """Delete a role""" role: Role = Role.query.filter_by(name=role_name).first_or_404() db.session.delete(role) db.session.commit() - return jsonify({"OK": True}) + return {"OK": True} diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index e50ebf71..7e977370 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -1,12 +1,13 @@ import os -from config import Config -from datetime import datetime, date +from datetime import datetime import glob import shutil from flask import render_template, redirect, url_for, request, flash, send_file, abort -from flask.json import jsonify +from flask_json import as_json from flask_login import current_user +from sqlalchemy import text, sql +from werkzeug.utils import secure_filename from app.decorators import permission_required @@ -58,8 +59,7 @@ from app.scodoc import sco_etud, sco_excel import app.scodoc.sco_utils as scu from app import db -from sqlalchemy import text, sql -from werkzeug.utils import secure_filename +from config import Config @bp.route("/", methods=["GET", "POST"]) @@ -1698,6 +1698,7 @@ def envoyer_offre(entreprise_id, offre_id): @bp.route("/etudiants") @permission_required(Permission.RelationsEntreprisesChange) +@as_json def json_etudiants(): """ Permet de récuperer un JSON avec tous les étudiants @@ -1723,7 +1724,7 @@ def json_etudiants(): "info": f"Département {are.get_dept_acronym_by_id(etudiant.dept_id)}", } list.append(content) - return jsonify(results=list) + return list @bp.route("/responsables") @@ -1749,7 +1750,7 @@ def json_responsables(): value = f"{responsable.get_nomplogin()}" content = {"id": f"{responsable.id}", "value": value} list.append(content) - return jsonify(results=list) + return list @bp.route("/export_donnees") @@ -1843,7 +1844,7 @@ def import_donnees(): db.session.add(correspondant) correspondants.append(correspondant) db.session.commit() - flash(f"Importation réussie") + flash("Importation réussie") return render_template( "entreprises/import_donnees.j2", title="Importation données", diff --git a/app/models/but_refcomp.py b/app/models/but_refcomp.py index 3a17cfc0..420f96ae 100644 --- a/app/models/but_refcomp.py +++ b/app/models/but_refcomp.py @@ -85,6 +85,7 @@ class ApcReferentielCompetences(db.Model, XMLModel): backref="referentiel", lazy="dynamic", cascade="all, delete-orphan", + order_by="ApcParcours.numero, ApcParcours.code", ) formations = db.relationship( "Formation", diff --git a/app/scodoc/codes_cursus.py b/app/scodoc/codes_cursus.py index e7af230d..f2285ac1 100644 --- a/app/scodoc/codes_cursus.py +++ b/app/scodoc/codes_cursus.py @@ -275,6 +275,7 @@ DEVENIRS_NEXT2 = {NEXT_OR_NEXT2: 1, NEXT2: 1} NO_SEMESTRE_ID = -1 # code semestre si pas de semestres + # Règles gestion cursus class DUTRule(object): def __init__(self, rule_id, premise, conclusion): @@ -298,7 +299,7 @@ class DUTRule(object): # Types de cursus -DEFAULT_TYPE_CURSUS = 100 # pour le menu de creation nouvelle formation +DEFAULT_TYPE_CURSUS = 700 # (BUT) pour le menu de creation nouvelle formation class TypeCursus: diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 82191269..fce11e1c 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -33,8 +33,9 @@ import email import time import numpy as np -from flask import g, request -from flask import flash, jsonify, render_template, url_for +from flask import g, request, Response +from flask import flash, render_template, url_for +from flask_json import json_response from flask_login import current_user from app import email @@ -79,14 +80,14 @@ def get_formsemestre_bulletin_etud_json( etud: Identite, force_publishing=False, version="long", -) -> str: +) -> Response: """Le JSON du bulletin d'un étudiant, quel que soit le type de formation.""" if formsemestre.formation.is_apc(): bulletins_sem = bulletin_but.BulletinBUT(formsemestre) if not etud.id in bulletins_sem.res.identdict: return json_error(404, "get_formsemestre_bulletin_etud_json: invalid etud") - return jsonify( - bulletins_sem.bulletin_etud( + return json_response( + data_=bulletins_sem.bulletin_etud( etud, formsemestre, force_publishing=force_publishing, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 639225e4..3a3ddd15 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -51,8 +51,9 @@ import pydot import requests import flask -from flask import g, request -from flask import flash, url_for, make_response, jsonify +from flask import g, request, Response +from flask import flash, url_for, make_response +from flask_json import json_response from werkzeug.http import HTTP_STATUS_CODES from config import Config @@ -804,24 +805,26 @@ def get_request_args(): return vals -def json_error(status_code, message=None): - """Simple JSON response, for errors""" +def json_error(status_code, message=None) -> Response: + """Simple JSON for errors. + If as-response, returns Flask's Response. Otherwise returns a dict. + """ payload = { "error": HTTP_STATUS_CODES.get(status_code, "Unknown error"), "status": status_code, } if message: payload["message"] = message - response = jsonify(payload) + response = json_response(status_=status_code, data_=payload) response.status_code = status_code log(f"Error: {response}") return response -def json_ok_response(status_code=200, payload=None): +def json_ok_response(status_code=200, payload=None) -> Response: """Simple JSON respons for "success" """ payload = payload or {"OK": True} - response = jsonify(payload) + response = json_response(status_=status_code, data_=payload) response.status_code = status_code return response diff --git a/app/scodoc/sco_vdi.py b/app/scodoc/sco_vdi.py index bb0a48da..47d77c21 100644 --- a/app/scodoc/sco_vdi.py +++ b/app/scodoc/sco_vdi.py @@ -33,7 +33,7 @@ from app.scodoc.sco_exceptions import ScoValueError class ApoEtapeVDI(object): _ETAPE_VDI_SEP = "!" - def __init__(self, etape_vdi=None, etape="", vdi=""): + def __init__(self, etape_vdi: str = None, etape: str = "", vdi: str = ""): """Build from string representation, e.g. 'V1RT!111'""" if etape_vdi: self.etape_vdi = etape_vdi @@ -52,6 +52,10 @@ class ApoEtapeVDI(object): def __str__(self): return self.etape_vdi + def __json__(self) -> str: + "json repr for flask_json" + return str(self) + def _cmp(self, other): """Test égalité de deux codes étapes. Si le VDI des deux est spécifié, on l'utilise. Sinon, seul le code étape est pris en compte. diff --git a/app/templates/but/refcomp_show.j2 b/app/templates/but/refcomp_show.j2 index 4b6521e5..5bd66dff 100644 --- a/app/templates/but/refcomp_show.j2 +++ b/app/templates/but/refcomp_show.j2 @@ -28,6 +28,8 @@
  • {{ formation.get_titre_version() }}
  • + {% else %} +
  • aucune
  • {% endfor %} diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py index 57932450..c0e1152c 100644 --- a/app/views/pn_modules.py +++ b/app/views/pn_modules.py @@ -32,10 +32,10 @@ Emmanuel Viennet, 2021 """ from flask import url_for -from flask import jsonify from flask import g, request -from flask.templating import render_template +from flask_json import as_json from flask_login import current_user +from flask.templating import render_template from app.scodoc.codes_cursus import UE_SPORT @@ -58,6 +58,7 @@ from app.scodoc.sco_permissions import Permission ) @scodoc @permission_required(Permission.ScoView) +@as_json def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = None): """Description JSON de la table des coefs modules/UE dans une formation @@ -111,10 +112,10 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N ] # Les champs de saisie cells = [] - for (row, mod) in enumerate(modules, start=2): + for row, mod in enumerate(modules, start=2): style = "champs champs_" + scu.ModuleType(mod.module_type).name mod_parcours_ids = {p.id for p in mod.parcours} - for (col, ue) in enumerate(ues, start=2): + for col, ue in enumerate(ues, start=2): # met en gris les coefs qui devraient être nuls # car le module n'est pas dans le parcours de l'UE: if ( @@ -136,7 +137,7 @@ def table_modules_ue_coefs(formation_id, semestre_idx=None, parcours_id: int = N "ue_id": ue.id, } ) - return jsonify(col_titres_mods + row_titres_ue + cells) + return col_titres_mods + row_titres_ue + cells @bp.route("/set_module_ue_coef", methods=["POST"]) diff --git a/app/views/refcomp.py b/app/views/refcomp.py index 2c1fa678..ad047dba 100644 --- a/app/views/refcomp.py +++ b/app/views/refcomp.py @@ -6,10 +6,11 @@ Emmanuel Viennet, 2021 from pathlib import Path import re -from flask import jsonify, flash, url_for +from flask import flash, url_for from flask import Markup from flask import current_app, g, request from flask.templating import render_template +from flask_json import as_json from flask_login import current_user from werkzeug.utils import redirect from werkzeug.utils import secure_filename @@ -33,12 +34,13 @@ from app.views import ScoData @bp.route("/referentiel/comp/get/") @scodoc @permission_required(Permission.ScoView) +@as_json def refcomp(refcomp_id): """Le référentiel de compétences, en JSON.""" ref: ApcReferentielCompetences = ApcReferentielCompetences.query.get_or_404( refcomp_id ) - return jsonify(ref.to_dict()) + return ref.to_dict() @bp.route("/referentiel/comp/show/") diff --git a/app/views/scolar.py b/app/views/scolar.py index 8f46df0e..9cb3cb94 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -35,8 +35,9 @@ import requests import time import flask -from flask import jsonify, url_for, flash, render_template, make_response +from flask import url_for, flash, render_template, make_response from flask import g, request +from flask_json import as_json from flask_login import current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileAllowed @@ -501,10 +502,11 @@ sco_publish( @bp.route("/Notes/search_etud_by_name") # for JS apis @scodoc @permission_required(Permission.ScoView) +@as_json def search_etud_by_name(): term = request.args["term"] data = sco_find_etud.search_etud_by_name(term) - return jsonify(data) + return data # XMLgetEtudInfos était le nom dans l'ancienne API ScoDoc 6 @@ -895,6 +897,7 @@ sco_publish( methods=["GET", "POST"], ) + # Nouvel éditeur de partitions et groupe, @SebL Jul 2022 @bp.route("/partition_editor", methods=["GET", "POST"]) @scodoc @@ -2266,7 +2269,7 @@ def formsemestre_import_etud_admission(formsemestre_id, import_email=True): ) if changed_mails: H.append("

    Adresses mails modifiées:

    ") - for (info, new_mail) in changed_mails: + for info, new_mail in changed_mails: H.append( "%s: %s devient %s
    " % (info["nom"], info["email"], new_mail) @@ -2286,6 +2289,7 @@ sco_publish( Permission.ScoEtudChangeAdr, ) + # --- Statistiques @bp.route("/stat_bac") @scodoc diff --git a/config.py b/config.py index ca8097d4..8ad045d8 100755 --- a/config.py +++ b/config.py @@ -38,6 +38,9 @@ class Config: SCODOC_ERR_FILE = os.path.join(SCODOC_VAR_DIR, "log", "scodoc_exc.log") # MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # Flask uploads (16Mo, en ligne avec nginx) + # flask_json: + JSON_ADD_STATUS = False + JSON_USE_ENCODE_METHODS = True class ProdConfig(Config): diff --git a/requirements-3.9.txt b/requirements-3.9.txt index 456a1d89..43a9383c 100755 --- a/requirements-3.9.txt +++ b/requirements-3.9.txt @@ -26,6 +26,7 @@ flask-babel==3.0.1 Flask-Bootstrap==3.3.7.1 Flask-Caching==2.0.2 Flask-HTTPAuth==4.7.0 +Flask-JSON==0.3.5 Flask-Login==0.6.2 Flask-Mail==0.9.1 Flask-Migrate==4.0.4 diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index 0a9d2402..99c8fdf7 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -17,7 +17,6 @@ import os import requests from dotenv import load_dotenv import pytest -from app.scodoc import sco_utils as scu # --- Lecture configuration (variables d'env ou .env) try: @@ -34,6 +33,7 @@ API_PASSWORD = os.environ.get("API_PASSWORD", os.environ.get("API_PASSWD", "test API_USER_ADMIN = os.environ.get("API_USER_ADMIN", "admin_api") API_PASSWORD_ADMIN = os.environ.get("API_PASSWD_ADMIN", "admin_api") DEPT_ACRONYM = "TAPI" +SCO_TEST_API_TIMEOUT = 5 print(f"SCODOC_URL={SCODOC_URL}") print(f"API URL={API_URL}") @@ -78,7 +78,7 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None): url, headers=headers or {}, verify=CHECK_CERTIFICATE, - timeout=scu.SCO_TEST_API_TIMEOUT, + timeout=SCO_TEST_API_TIMEOUT, ) if reply.status_code != 200: raise APIError( @@ -111,7 +111,7 @@ def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None, dep json=data, headers=headers or {}, verify=CHECK_CERTIFICATE, - timeout=10, + timeout=SCO_TEST_API_TIMEOUT, ) if r.status_code != 200: raise APIError(errmsg or f"erreur status={r.status_code} !", r.json()) diff --git a/tests/api/test_api_permissions.py b/tests/api/test_api_permissions.py index fbaab34a..602834f1 100644 --- a/tests/api/test_api_permissions.py +++ b/tests/api/test_api_permissions.py @@ -88,7 +88,9 @@ def test_permissions(api_headers): assert r.status_code == 401 # Demande un jeton pour "other" - r = requests.post(API_URL + "/tokens", auth=("other", "other"), timeout=10) + r = requests.post( + API_URL + "/tokens", auth=("other", "other"), timeout=scu.SCO_TEST_API_TIMEOUT + ) assert r.status_code == 200 token = r.json()["token"] headers = {"Authorization": f"Bearer {token}"} diff --git a/tests/ressources/formations/scodoc_formation_INFO_BUT_INFO_v1.xml b/tests/ressources/formations/scodoc_formation_INFO_BUT_INFO_v1.xml deleted file mode 100644 index 70f0af60..00000000 --- a/tests/ressources/formations/scodoc_formation_INFO_BUT_INFO_v1.xml +++ /dev/null @@ -1,526 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index 0c634b06..cd06048b 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -42,7 +42,7 @@ from tools.fakeportal.gen_nomprenoms import nomprenom random.seed(12345678) # tests reproductibles # La formation à utiliser: -FORMATION_XML_FILENAME = "tests/ressources/formations/scodoc_formation_RT_BUT_RT_v1.xml" +FORMATION_XML_FILENAME = "tests/ressources/formations/scodoc_formation_BUT_RT_v1.xml" REFCOMP_FILENAME = ( "ressources/referentiels/but2022/competences/but-RT-05012022-081735.xml" )