diff --git a/app/api/__init__.py b/app/api/__init__.py index d270708d69..0e3e72092f 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -3,11 +3,18 @@ from flask import Blueprint from flask import request +from app.scodoc import sco_utils as scu api_bp = Blueprint("api", __name__) api_web_bp = Blueprint("apiweb", __name__) +@api_bp.errorhandler(404) +def api_error_handler(e): + "erreurs API => json" + return scu.json_error(404, message=str(e)) + + def requested_format(default_format="json", allowed_formats=None): """Extract required format from query string. * default value is json. A list of allowed formats may be provided diff --git a/app/api/absences.py b/app/api/absences.py index 0442877b2a..39951078a0 100644 --- a/app/api/absences.py +++ b/app/api/absences.py @@ -9,7 +9,7 @@ from flask import jsonify from app.api import api_bp as bp -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.decorators import scodoc, permission_required from app.models import Identite @@ -53,7 +53,7 @@ def absences(etudid: int = None): """ etud = Identite.query.get(etudid) if etud is None: - return error_response(404, message="etudiant inexistant") + return json_error(404, message="etudiant inexistant") # Absences de l'étudiant ndb.open_db_connection() abs_list = sco_abs.list_abs_date(etud.id) @@ -97,7 +97,7 @@ def absences_just(etudid: int = None): """ etud = Identite.query.get(etudid) if etud is None: - return error_response(404, message="etudiant inexistant") + return json_error(404, message="etudiant inexistant") # Absences justifiées de l'étudiant abs_just = [ diff --git a/app/api/billets_absences.py b/app/api/billets_absences.py index bc35b870a9..f15a9b4956 100644 --- a/app/api/billets_absences.py +++ b/app/api/billets_absences.py @@ -15,7 +15,7 @@ import app from app import db from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.models import BilletAbsence from app.models.etudiants import Identite from app.scodoc import sco_abs_billets @@ -47,7 +47,7 @@ def billets_absence_create(): description = data.get("description", "") justified = data.get("justified", False) if None in (etudid, abs_begin, abs_end): - return error_response( + return json_error( 404, message="Paramètre manquant: etudid, abs_bein, abs_end requis" ) query = Identite.query.filter_by(etudid=etudid) diff --git a/app/api/departements.py b/app/api/departements.py index 9a72d9ae09..e886d11440 100644 --- a/app/api/departements.py +++ b/app/api/departements.py @@ -17,7 +17,7 @@ from flask_login import login_required import app from app import db, log from app.api import api_bp as bp -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.decorators import scodoc, permission_required from app.models import Departement, FormSemestre from app.models import departements @@ -103,12 +103,12 @@ def departement_create(): data = request.get_json(force=True) # may raise 400 Bad Request acronym = str(data.get("acronym", "")) if not acronym: - return error_response(404, "missing acronym") + return json_error(404, "missing acronym") visible = bool(data.get("visible", True)) try: dept = departements.create_dept(acronym, visible=visible) except ScoValueError as exc: - return error_response(404, exc.args[0] if exc.args else "") + return json_error(404, exc.args[0] if exc.args else "") return jsonify(dept.to_dict()) @@ -128,7 +128,7 @@ def departement_edit(acronym): data = request.get_json(force=True) # may raise 400 Bad Request visible = bool(data.get("visible", None)) if visible is None: - return error_response(404, "missing argument: visible") + return json_error(404, "missing argument: visible") visible = bool(visible) dept.visible = visible db.session.add(dept) diff --git a/app/api/errors.py b/app/api/errors.py deleted file mode 100644 index 9bf5491002..0000000000 --- a/app/api/errors.py +++ /dev/null @@ -1,41 +0,0 @@ -# Authentication code borrowed from Miguel Grinberg's Mega Tutorial -# (see https://github.com/miguelgrinberg/microblog) - -# Under The MIT License (MIT) - -# Copyright (c) 2017 Miguel Grinberg - -# Permission is hereby granted, free of charge, to any person obtaining a copy of -# this software and associated documentation files (the "Software"), to deal in -# the Software without restriction, including without limitation the rights to -# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -# the Software, and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from flask import jsonify -from werkzeug.http import HTTP_STATUS_CODES - - -def error_response(status_code, message=None): - """Réponse sur erreur""" - payload = {"error": HTTP_STATUS_CODES.get(status_code, "Unknown error")} - if message: - payload["message"] = message - response = jsonify(payload) - response.status_code = status_code - return response - - -def bad_request(message): - "400 Bad Request response" - return error_response(400, message) diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 208eba11a9..de195b6b19 100644 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -15,7 +15,7 @@ from sqlalchemy import desc, or_ import app from app.api import api_bp as bp, api_web_bp -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.api import tools from app.decorators import scodoc, permission_required from app.models import ( @@ -116,7 +116,7 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None): etud = tools.get_etud(etudid, nip, ine) if etud is None: - return error_response( + return json_error( 404, message="étudiant inconnu", ) @@ -148,7 +148,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None): elif ine is not None: query = Identite.query.filter_by(code_ine=ine) else: - return error_response( + return json_error( 404, message="parametre manquant", ) @@ -185,12 +185,12 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None) elif ine is not None: q_etud = Identite.query.filter_by(code_ine=ine) else: - return error_response(404, message="parametre manquant") + return json_error(404, message="parametre manquant") if g.scodoc_dept is not None: q_etud = q_etud.filter_by(dept_id=g.scodoc_dept_id) etud = q_etud.join(Admission).order_by(desc(Admission.annee)).first() if etud is None: - return error_response(404, message="etudiant inexistant") + return json_error(404, message="etudiant inexistant") query = FormSemestre.query.filter( FormSemestreInscription.etudid == etud.id, FormSemestreInscription.formsemestre_id == FormSemestre.id, @@ -328,7 +328,7 @@ def etudiant_bulletin_semestre( 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 != g.scodoc_dept: - return error_response(404, "formsemestre non trouve") + return json_error(404, "formsemestre non trouve") if etudid is not None: query = Identite.query.filter_by(id=etudid) elif nip is not None: @@ -336,11 +336,11 @@ def etudiant_bulletin_semestre( elif ine is not None: query = Identite.query.filter_by(code_ine=ine, dept_id=dept.id) else: - return error_response(404, message="parametre manquant") + return json_error(404, message="parametre manquant") etud = query.first() if etud is None: - return error_response(404, message="etudiant inexistant") + return json_error(404, message="etudiant inexistant") app.set_sco_dept(dept.acronym) @@ -400,7 +400,7 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None): query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre = query.first() if formsemestre is None: - return error_response( + return json_error( 404, message="formsemestre inconnu", ) diff --git a/app/api/evaluations.py b/app/api/evaluations.py index e804d6b9bd..5fe479f2b0 100644 --- a/app/api/evaluations.py +++ b/app/api/evaluations.py @@ -15,7 +15,7 @@ import app from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.models import Evaluation, ModuleImpl, FormSemestre from app.scodoc import sco_evaluation_db from app.scodoc.sco_permissions import Permission diff --git a/app/api/formations.py b/app/api/formations.py index 21b8a58f9d..e87f9941ff 100644 --- a/app/api/formations.py +++ b/app/api/formations.py @@ -13,7 +13,7 @@ from flask_login import login_required import app from app.api import api_bp as bp, api_web_bp -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.decorators import scodoc, permission_required from app.models.formations import Formation from app.models.formsemestre import FormSemestre @@ -210,7 +210,7 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False): try: data = sco_formations.formation_export(formation_id, export_ids) except ValueError: - return error_response(500, message="Erreur inconnue") + return json_error(500, message="Erreur inconnue") return jsonify(data) diff --git a/app/api/formsemestres.py b/app/api/formsemestres.py index e05a7ed317..8d55cf18e6 100644 --- a/app/api/formsemestres.py +++ b/app/api/formsemestres.py @@ -13,7 +13,7 @@ from flask_login import login_required import app from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required -from app.api.errors import error_response +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 @@ -107,7 +107,7 @@ def formsemestres_query(): try: annee_scolaire_int = int(annee_scolaire) except ValueError: - return error_response(404, "invalid annee_scolaire: not int") + return json_error(404, "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( @@ -119,7 +119,7 @@ def formsemestres_query(): try: dept_id = int(dept_id) except ValueError: - return error_response(404, "invalid dept_id: not int") + return json_error(404, "invalid dept_id: not int") formsemestres = formsemestres.filter_by(dept_id=dept_id) if etape_apo is not None: formsemestres = formsemestres.join(FormSemestreEtape).filter( @@ -417,7 +417,7 @@ def formsemestre_resultat(formsemestre_id: int): """ format_spec = request.args.get("format", None) if format_spec is not None and format_spec != "raw": - return error_response(404, "invalid format specification") + return json_error(404, "invalid format specification") convert_values = format_spec != "raw" query = FormSemestre.query.filter_by(id=formsemestre_id) diff --git a/app/api/jury.py b/app/api/jury.py index a8f0c92b5a..7cf067cc31 100644 --- a/app/api/jury.py +++ b/app/api/jury.py @@ -15,7 +15,7 @@ import app from app import db, log from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.but import jury_but_recap from app.models import FormSemestre, FormSemestreInscription, Identite from app.scodoc.sco_permissions import Permission diff --git a/app/api/logos.py b/app/api/logos.py index 5a3a6ab3d9..ef3a7f9736 100644 --- a/app/api/logos.py +++ b/app/api/logos.py @@ -35,7 +35,7 @@ from flask_login import login_required from app.api import api_bp as bp, api_web_bp from app.api import requested_format -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.models import Departement from app.scodoc.sco_logos import list_logos, find_logo from app.decorators import scodoc, permission_required @@ -49,10 +49,10 @@ from app.scodoc.sco_permissions import Permission @permission_required(Permission.ScoView) def api_get_glob_logos(): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): - return error_response(401, message="accès interdit") + return json_error(403, message="accès interdit") required_format = requested_format() # json only if required_format is None: - return error_response(400, "Illegal format") + return json_error(400, "Illegal format") logos = list_logos()[None] return jsonify(list(logos.keys())) @@ -62,10 +62,10 @@ def api_get_glob_logos(): @permission_required(Permission.ScoView) def api_get_glob_logo(logoname): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): - return error_response(401, message="accès interdit") + return json_error(403, message="accès interdit") logo = find_logo(logoname=logoname) if logo is None: - return error_response(404, message="logo not found") + return json_error(404, message="logo not found") logo.select() return send_file( logo.filepath, @@ -80,7 +80,7 @@ def api_get_glob_logo(logoname): def api_get_local_logos(departement): dept_id = Departement.from_acronym(departement).id if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): - return error_response(401, message="accès interdit") + return json_error(403, message="accès interdit") logos = list_logos().get(dept_id, dict()) return jsonify(list(logos.keys())) @@ -92,10 +92,10 @@ def api_get_local_logo(departement, logoname): # format = requested_format("jpg", ['png', 'jpg']) XXX ? dept_id = Departement.from_acronym(departement).id if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): - return error_response(401, message="accès interdit") + return json_error(403, message="accès interdit") logo = find_logo(logoname=logoname, dept_id=dept_id) if logo is None: - return error_response(404, message="logo not found") + return json_error(404, message="logo not found") logo.select() return send_file( logo.filepath, diff --git a/app/api/partitions.py b/app/api/partitions.py index 9af1fb303c..abef92e8cf 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -14,7 +14,7 @@ import app from app import db, log from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import GroupDescr, Partition from app.models.groups import group_membership @@ -137,7 +137,7 @@ def etud_in_group_query(group_id: int): """Etudiants du groupe, filtrés par état""" etat = request.args.get("etat") if etat not in {scu.INSCRIT, scu.DEMISSION, scu.DEF}: - return error_response(404, "etat: valeur invalide") + return json_error(404, "etat: valeur invalide") query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( @@ -169,7 +169,7 @@ def set_etud_group(etudid: int, group_id: int): ) group = query.first_or_404() if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: - return error_response(404, "etud non inscrit au formsemestre du groupe") + return json_error(404, "etud non inscrit au formsemestre du groupe") groups = ( GroupDescr.query.filter_by(partition_id=group.partition.id) .join(group_membership) @@ -261,13 +261,13 @@ def group_create(partition_id: int): query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() if not partition.groups_editable: - return error_response(404, "partition non editable") + return json_error(404, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is None: - return error_response(404, "missing group name or invalid data format") + return json_error(404, "missing group name or invalid data format") if not GroupDescr.check_name(partition, group_name): - return error_response(404, "invalid group_name") + return json_error(404, "invalid group_name") group_name = group_name.strip() group = GroupDescr(group_name=group_name, partition_id=partition_id) @@ -293,7 +293,7 @@ def group_delete(group_id: int): ) group: GroupDescr = query.first_or_404() if not group.partition.groups_editable: - return error_response(404, "partition non editable") + return json_error(404, "partition non editable") formsemestre_id = group.partition.formsemestre_id log(f"deleting {group}") db.session.delete(group) @@ -317,12 +317,12 @@ def group_edit(group_id: int): ) group: GroupDescr = query.first_or_404() if not group.partition.groups_editable: - return error_response(404, "partition non editable") + return json_error(404, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is not None: if not GroupDescr.check_name(group.partition, group_name, existing=True): - return error_response(404, "invalid group_name") + return json_error(404, "invalid group_name") group.group_name = group_name.strip() db.session.add(group) db.session.commit() @@ -358,14 +358,14 @@ def partition_create(formsemestre_id: int): data = request.get_json(force=True) # may raise 400 Bad Request partition_name = data.get("partition_name") if partition_name is None: - return error_response(404, "missing partition_name or invalid data format") + return json_error(404, "missing partition_name or invalid data format") if partition_name == scu.PARTITION_PARCOURS: - return error_response(404, f"invalid partition_name {scu.PARTITION_PARCOURS}") + return json_error(404, f"invalid partition_name {scu.PARTITION_PARCOURS}") if not Partition.check_name(formsemestre, partition_name): - return error_response(404, "invalid partition_name") + return json_error(404, "invalid partition_name") numero = data.get("numero", 0) if not isinstance(numero, int): - return error_response(404, "invalid type for numero") + return json_error(404, "invalid type for numero") args = { "formsemestre_id": formsemestre_id, "partition_name": partition_name.strip(), @@ -376,7 +376,7 @@ def partition_create(formsemestre_id: int): boolean_field, False if boolean_field != "groups_editable" else True ) if not isinstance(value, bool): - return error_response(404, f"invalid type for {boolean_field}") + return json_error(404, f"invalid type for {boolean_field}") args[boolean_field] = value partition = Partition(**args) @@ -407,7 +407,7 @@ def formsemestre_order_partitions(formsemestre_id: int): if not isinstance(partition_ids, int) and not all( isinstance(x, int) for x in partition_ids ): - return error_response( + return json_error( 404, message="paramètre liste des partitions invalide", ) @@ -444,7 +444,7 @@ def partition_order_groups(partition_id: int): if not isinstance(group_ids, int) and not all( isinstance(x, int) for x in group_ids ): - return error_response( + return json_error( 404, message="paramètre liste de groupe invalide", ) @@ -487,18 +487,18 @@ def partition_edit(partition_id: int): # if partition_name is not None and partition_name != partition.partition_name: if partition.is_parcours(): - return error_response(404, f"can't rename {scu.PARTITION_PARCOURS}") + return json_error(404, f"can't rename {scu.PARTITION_PARCOURS}") if not Partition.check_name( partition.formsemestre, partition_name, existing=True ): - return error_response(404, "invalid partition_name") + return json_error(404, "invalid partition_name") partition.partition_name = partition_name.strip() modified = True numero = data.get("numero") if numero is not None and numero != partition.numero: if not isinstance(numero, int): - return error_response(404, "invalid type for numero") + return json_error(404, "invalid type for numero") partition.numero = numero modified = True @@ -506,9 +506,9 @@ def partition_edit(partition_id: int): value = data.get(boolean_field) if value is not None and value != getattr(partition, boolean_field): if not isinstance(value, bool): - return error_response(404, f"invalid type for {boolean_field}") + return json_error(404, f"invalid type for {boolean_field}") if boolean_field == "groups_editable" and partition.is_parcours(): - return error_response(404, f"can't change {scu.PARTITION_PARCOURS}") + return json_error(404, f"can't change {scu.PARTITION_PARCOURS}") setattr(partition, boolean_field, value) modified = True @@ -540,7 +540,7 @@ def partition_delete(partition_id: int): query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() if not partition.partition_name: - return error_response(404, "ne peut pas supprimer la partition par défaut") + return json_error(404, "ne peut pas supprimer la partition par défaut") is_parcours = partition.is_parcours() formsemestre: FormSemestre = partition.formsemestre log(f"deleting partition {partition}") diff --git a/app/api/tools.py b/app/api/tools.py index a471cb8887..08fd8aeb6d 100644 --- a/app/api/tools.py +++ b/app/api/tools.py @@ -10,7 +10,7 @@ from flask_login import current_user from sqlalchemy import desc, or_ from app import models -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.models import Departement, Identite, Admission from app.scodoc.sco_permissions import Permission @@ -39,7 +39,7 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite: elif ine is not None: query = Identite.query.filter_by(code_ine=ine) else: - return error_response( + return json_error( 404, message="parametre manquant", ) diff --git a/app/api/users.py b/app/api/users.py index 198744222d..ee788f50fc 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -12,10 +12,10 @@ from flask import g, jsonify, request from flask_login import current_user, login_required -import app from app import db, log from app.api import api_bp as bp, api_web_bp -from app.api.errors import error_response +from app.models.etudiants import Identite +from app.scodoc.sco_utils import json_error from app.auth.models import User, Role, UserRole from app.decorators import scodoc, permission_required from app.models import Departement @@ -35,11 +35,11 @@ def user_info(uid: int): """ user: User = User.query.get(uid) if user is None: - return error_response(404, "user not found") + return json_error(404, "user not found") if g.scodoc_dept: allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersView) if user.dept not in allowed_depts: - return error_response(404, "user not found") + return json_error(404, "user not found") return jsonify(user.to_dict()) @@ -101,20 +101,20 @@ def user_create(): data = request.get_json(force=True) # may raise 400 Bad Request user_name = data.get("user_name") if not user_name: - return error_response(404, "empty user_name") + return json_error(404, "empty user_name") user = User.query.filter_by(user_name=user_name).first() if user: - return error_response(404, f"user_create: user {user} already exists\n") + return json_error(404, f"user_create: user {user} already exists\n") dept = data.get("dept") if dept == "@all": dept = None allowed_depts = current_user.get_depts_with_permission(Permission.ScoUsersAdmin) if dept not in allowed_depts: - return error_response(403, "user_create: departement non autorise") + return json_error(403, "user_create: departement non autorise") if (dept is not None) and ( Departement.query.filter_by(acronym=dept).first() is None ): - return error_response(404, "user_create: departement inexistant") + return json_error(404, "user_create: departement inexistant") nom = data.get("nom") prenom = data.get("prenom") active = scu.to_bool(data.get("active", True)) @@ -151,12 +151,12 @@ def user_edit(uid: int): if (None not in allowed_depts) and ( (orig_dept not in allowed_depts) or (dest_dept not in allowed_depts) ): - return error_response(403, "user_edit: departement non autorise") + return json_error(403, "user_edit: departement non autorise") if dest_dept != orig_dept: if (dest_dept is not None) and ( Departement.query.filter_by(acronym=dest_dept).first() is None ): - return error_response(404, "user_edit: departement inexistant") + return json_error(404, "user_edit: departement inexistant") user.dept = dest_dept user.nom = data.get("nom", user.nom) @@ -189,7 +189,7 @@ def user_role_add(uid: int, role_name: str, dept: str = None): _ = Departement.query.filter_by(acronym=dept).first_or_404() allowed_depts = current_user.get_depts_with_permission(Permission.ScoSuperAdmin) if (None not in allowed_depts) and (dept not in allowed_depts): - return error_response(403, "user_role_add: departement non autorise") + return json_error(403, "user_role_add: departement non autorise") user.add_role(role, dept) db.session.add(user) db.session.commit() @@ -217,7 +217,7 @@ def user_role_remove(uid: int, role_name: str, dept: str = None): _ = Departement.query.filter_by(acronym=dept).first_or_404() allowed_depts = current_user.get_depts_with_permission(Permission.ScoSuperAdmin) if (None not in allowed_depts) and (dept not in allowed_depts): - return error_response(403, "user_role_remove: departement non autorise") + return json_error(403, "user_role_remove: departement non autorise") query = UserRole.query.filter(UserRole.role == role, UserRole.user == user) if dept is not None: @@ -276,7 +276,7 @@ def role_permission_add(role_name: str, perm_name: str): role: Role = Role.query.filter_by(name=role_name).first_or_404() permission = Permission.get_by_name(perm_name) if permission is None: - return error_response(404, "role_permission_add: permission inconnue") + return json_error(404, "role_permission_add: permission inconnue") role.add_permission(permission) db.session.add(role) db.session.commit() @@ -299,7 +299,7 @@ def role_permission_remove(role_name: str, perm_name: str): role: Role = Role.query.filter_by(name=role_name).first_or_404() permission = Permission.get_by_name(perm_name) if permission is None: - return error_response(404, "role_permission_remove: permission inconnue") + return json_error(404, "role_permission_remove: permission inconnue") role.remove_permission(permission) db.session.add(role) db.session.commit() @@ -319,7 +319,7 @@ def role_create(role_name: str): """ role: Role = Role.query.filter_by(name=role_name).first() if role: - return error_response(404, "role_create: role already exists") + return json_error(404, "role_create: role already exists") role = Role(name=role_name) data = request.get_json(force=True) # may raise 400 Bad Request permissions = data.get("permissions") @@ -327,7 +327,7 @@ def role_create(role_name: str): try: role.set_named_permissions(permissions) except ScoValueError: - return error_response(404, "role_create: invalid permissions") + return json_error(404, "role_create: invalid permissions") db.session.add(role) db.session.commit() return jsonify(role.to_dict()) @@ -352,12 +352,12 @@ def role_edit(role_name: str): try: role.set_named_permissions(permissions) except ScoValueError: - return error_response(404, "role_create: invalid permissions") + return json_error(404, "role_create: invalid permissions") role_name = data.get("role_name") if role_name and role_name != role.name: existing_role: Role = Role.query.filter_by(name=role_name).first() if existing_role: - return error_response(404, "role_edit: role name already exists") + return json_error(404, "role_edit: role name already exists") role.name = role_name db.session.add(role) db.session.commit() diff --git a/app/auth/logic.py b/app/auth/logic.py index 1d1e8b73b3..4dfa186047 100644 --- a/app/auth/logic.py +++ b/app/auth/logic.py @@ -9,7 +9,7 @@ from flask import g, redirect, request, url_for from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth import flask_login from app import login -from app.api.errors import error_response +from app.scodoc.sco_utils import json_error from app.auth.models import User basic_auth = HTTPBasicAuth() @@ -80,8 +80,6 @@ def load_user_from_request(req: flask.Request) -> User: @login.unauthorized_handler def unauthorized(): "flask-login: si pas autorisé, redirige vers page login, sauf si API" - from app.api.errors import error_response as api_error_response - if request.blueprint == "api" or request.blueprint == "apiweb": - return api_error_response(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)") + return json_error(http.HTTPStatus.UNAUTHORIZED, "Non autorise (logic)") return redirect(url_for("auth.login")) diff --git a/app/but/bulletin_but.py b/app/but/bulletin_but.py index 379021374e..472bb63220 100644 --- a/app/but/bulletin_but.py +++ b/app/but/bulletin_but.py @@ -10,11 +10,10 @@ import collections import datetime import numpy as np -from flask import url_for, g +from flask import g, has_request_context, url_for from app.comp.res_but import ResultatsSemestreBUT from app.models import FormSemestre, Identite -from app.models import but_validations from app.models.groups import GroupDescr from app.models.ues import UniteEns from app.scodoc import sco_bulletins, sco_utils as scu @@ -170,7 +169,9 @@ class BulletinBUT: "notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id, - ), + ) + if has_request_context() + else "na", "moyenne": { # # moyenne indicative de module: moyenne des UE, # # ignorant celles sans notes (nan) @@ -228,7 +229,9 @@ class BulletinBUT: "notes.evaluation_listenotes", scodoc_dept=g.scodoc_dept, evaluation_id=e.id, - ), + ) + if has_request_context() + else "na", } return d diff --git a/app/models/etudiants.py b/app/models/etudiants.py index c4754f8eb4..987f640fb2 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -6,7 +6,7 @@ import datetime from functools import cached_property -from flask import abort, url_for +from flask import abort, has_request_context, url_for from flask import g, request import sqlalchemy from sqlalchemy import desc, text @@ -196,7 +196,8 @@ class Identite(db.Model): "nationalite": self.nationalite or "", "boursier": self.boursier or "", } - if include_urls: + if include_urls and has_request_context(): + # test request context so we can use this func in tests under the flask shell d["fiche_url"] = url_for( "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id ) diff --git a/app/scodoc/sco_bulletins.py b/app/scodoc/sco_bulletins.py index 22405fcfbc..168dca58ec 100644 --- a/app/scodoc/sco_bulletins.py +++ b/app/scodoc/sco_bulletins.py @@ -37,6 +37,7 @@ from flask_login import current_user from app import email from app import log +from app.scodoc.sco_utils import json_error from app.but import bulletin_but from app.comp import res_sem from app.comp.res_compat import NotesTableCompat @@ -74,9 +75,13 @@ def get_formsemestre_bulletin_etud_json( ) -> str: """Le JSON du bulletin d'un étudiant, quel que soit le type de formation.""" if formsemestre.formation.is_apc(): - r = bulletin_but.BulletinBUT(formsemestre) + bul = bulletin_but.BulletinBUT(formsemestre) + if not etud.id in bul.res.identdict: + return error_response( + 404, "get_formsemestre_bulletin_etud_json: invalid etud" + ) return jsonify( - r.bulletin_etud( + bul.bulletin_etud( etud, formsemestre, force_publishing=force_publishing, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 2fa6617f91..1d265f6a0a 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -53,6 +53,7 @@ import requests import flask from flask import g, request from flask import flash, url_for, make_response, jsonify +from werkzeug.http import HTTP_STATUS_CODES from config import Config from app import log @@ -819,15 +820,26 @@ def get_request_args(): return vals -def json_error(message, success=False, status=404): +def json_error(status_code, message=None): """Simple JSON response, for errors""" - response = { - "success": success, - "status": status, - "message": message, + payload = { + "error": HTTP_STATUS_CODES.get(status_code, "Unknown error"), + "status": status_code, } + if message: + payload["message"] = message + response = jsonify(payload) + response.status_code = status_code log(f"Error: {response}") - return jsonify(response), status + return response + + +def json_ok_response(status_code=200, payload=None): + """Simple JSON respons for "success" """ + payload = payload or {"OK": True} + response = jsonify(payload) + response.status_code = status_code + return response def get_scodoc_version(): diff --git a/app/views/notes.py b/app/views/notes.py index 2792ae8939..11bde73c31 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -36,17 +36,22 @@ import time from xml.etree import ElementTree import flask -from flask import abort, flash, jsonify, redirect, render_template, url_for +from flask import abort, flash, redirect, render_template, url_for from flask import current_app, g, request from flask_login import current_user +from app import db +from app import models +from app.auth.models import User +from app.but import apc_edit_ue, jury_but_recap from app.but import jury_but, jury_but_validation_auto from app.but.forms import jury_but_forms from app.but import jury_but_pv from app.but import jury_but_view + from app.comp import res_sem -from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_compat import NotesTableCompat +from app.models import ScolarNews from app.models.config import ScoDocSiteConfig from app.models.etudiants import Identite from app.models.formsemestre import FormSemestre @@ -54,13 +59,8 @@ from app.models.formsemestre import FormSemestreUEComputationExpr from app.models.moduleimpls import ModuleImpl from app.models.modules import Module from app.models.ues import UniteEns +from app.views import notes_bp as bp -from app import api -from app import db -from app import models -from app.models import ScolarNews, but_validations -from app.auth.models import User -from app.but import apc_edit_ue, jury_but_recap from app.decorators import ( scodoc, scodoc7func, @@ -68,7 +68,6 @@ from app.decorators import ( permission_required_compat_scodoc7, ) -from app.views import notes_bp as bp # --------------- @@ -775,7 +774,7 @@ def formsemestre_list( formsemestre_id = int(formsemestre_id) if formsemestre_id is not None else None formation_id = int(formation_id) if formation_id is not None else None except ValueError: - return api.errors.error_response(404, "invalid id") + return scu.json_error(404, "invalid id") # XAPI: new json api args = {} L = locals() diff --git a/app/views/pn_modules.py b/app/views/pn_modules.py index 36e9251bbe..3364a08d18 100644 --- a/app/views/pn_modules.py +++ b/app/views/pn_modules.py @@ -128,26 +128,26 @@ def set_module_ue_coef(): try: module_id = int(request.form["module_id"]) except ValueError: - return scu.json_error("invalid module_id", 400) + return scu.json_error(404, "invalid module_id") try: ue_id = int(request.form["ue_id"]) except ValueError: - return scu.json_error("invalid ue_id", 400) + return scu.json_error(404, "invalid ue_id") try: coef = float(request.form["coef"].replace(",", ".")) except ValueError: - return scu.json_error("invalid coef", 400) + return scu.json_error(404, "invalid coef") module = models.Module.query.get(module_id) if module is None: - return scu.json_error(f"module not found ({module_id})", 404) + return scu.json_error(404, f"module not found ({module_id})") ue = models.UniteEns.query.get(ue_id) if not ue: - return scu.json_error(f"UE not found ({ue_id})", 404) + return scu.json_error(404, f"UE not found ({ue_id})") module.set_ue_coef(ue, coef) db.session.commit() module.formation.invalidate_cached_sems() - return scu.json_error("ok", success=True, status=201) + return scu.json_ok_response(201) @bp.route("/edit_modules_ue_coefs") diff --git a/tests/api/setup_test_api.py b/tests/api/setup_test_api.py index a24be6b5d0..631b890d23 100644 --- a/tests/api/setup_test_api.py +++ b/tests/api/setup_test_api.py @@ -55,24 +55,24 @@ class APIError(Exception): pass -def GET(path: str, headers={}, errmsg=None, dept=None): +def GET(path: str, headers: dict = None, errmsg=None, dept=None): """Get and returns as JSON""" if dept: url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path else: url = API_URL + path - r = requests.get(url, headers=headers or CUR_HEADERS, verify=CHECK_CERTIFICATE) + r = requests.get(url, headers=headers or {}, verify=CHECK_CERTIFICATE) if r.status_code != 200: raise APIError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""") return r.json() # decode la reponse JSON -def POST_JSON(path: str, data: dict = {}, headers={}, errmsg=None): +def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None): """Post""" r = requests.post( API_URL + path, json=data, - headers=headers, + headers=headers or {}, verify=CHECK_CERTIFICATE, ) if r.status_code != 200: diff --git a/tests/api/test_api_logos.py b/tests/api/test_api_logos.py index 3aee0c6da7..d12ceb98ca 100644 --- a/tests/api/test_api_logos.py +++ b/tests/api/test_api_logos.py @@ -46,7 +46,7 @@ def test_admin_access(create_admin_token): headers = {"Authorization": f"Bearer {token}"} with app.test_client() as client: response = client.get(API_URL + "/logos", headers=headers) - assert response.status_code == 401 + assert response.status_code == 403 def test_lambda_access(create_lambda_token): @@ -57,7 +57,7 @@ def test_lambda_access(create_lambda_token): headers = {"Authorization": f"Bearer {token}"} with app.test_client() as client: response = client.get(API_URL + "/logos", headers=headers) - assert response.status_code == 401 + assert response.status_code == 403 def test_initial_with_header_and_footer(create_super_token): diff --git a/tools/fakedatabase/create_test_api_database.py b/tools/fakedatabase/create_test_api_database.py index e4d06e6d97..66e3055865 100644 --- a/tools/fakedatabase/create_test_api_database.py +++ b/tools/fakedatabase/create_test_api_database.py @@ -201,33 +201,23 @@ def create_formsemestre( def inscrit_etudiants(etuds: list, formsemestre: FormSemestre): - """Inscrit les etudiants aux semestres et à tous ses modules""" - for etud in etuds: - aleatoire = random.randint(0, 10) - if aleatoire <= 3: - sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( - formsemestre.id, - etud.id, - group_ids=[], - etat="I", - method="init db test", - ) - elif 3 < aleatoire <= 6: - sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( - formsemestre.id, - etud.id, - group_ids=[], - etat="D", - method="init db test", - ) + """Inscrit les étudiants au semestre et à tous ses modules. + 1/5 DEF, 1/5 DEF + """ + for i, etud in enumerate(etuds): + if (i + 1) % 5 == 0: + etat = "D" + elif (i + 2) % 5 == 0: + etat = "DEF" else: - sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( - formsemestre.id, - etud.id, - group_ids=[], - etat="DEF", - method="init db test", - ) + etat = "I" + sco_formsemestre_inscriptions.do_formsemestre_inscription_with_modules( + formsemestre.id, + etud.id, + group_ids=[], + etat=etat, + method="init db test", + ) def create_evaluations(formsemestre: FormSemestre): @@ -368,11 +358,11 @@ def init_test_database(): mapp.set_sco_dept(dept.acronym) user_lecteur, user_autre = create_users(depts) with sco_cache.DeferredSemCacheManager(): - etuds = create_etuds(dept) + etuds = create_etuds(dept, nb=20) formation = import_formation(dept.id) formsemestre = create_formsemestre(formation, user_lecteur) create_evaluations(formsemestre) - inscrit_etudiants(etuds, formsemestre) + inscrit_etudiants(etuds[:16], formsemestre) saisie_notes_evaluations(formsemestre, user_lecteur) add_absences(formsemestre) create_etape_apo(formsemestre)