1
0
forked from ScoDoc/ScoDoc

Modification contrôle d'accès. Routes API basic+token. Revision routes API.

This commit is contained in:
Emmanuel Viennet 2022-07-27 16:03:14 +02:00
parent c9a6fe0743
commit dcd7cf78fd
23 changed files with 686 additions and 439 deletions

View File

@ -41,6 +41,7 @@ migrate = Migrate(compare_type=True)
login = LoginManager() login = LoginManager()
login.login_view = "auth.login" login.login_view = "auth.login"
login.login_message = "Identifiez-vous pour accéder à cette page." login.login_message = "Identifiez-vous pour accéder à cette page."
mail = Mail() mail = Mail()
bootstrap = Bootstrap() bootstrap = Bootstrap()
moment = Moment() moment = Moment()
@ -249,8 +250,8 @@ def create_app(config_class=DevConfig):
from app.views import notes_bp from app.views import notes_bp
from app.views import users_bp from app.views import users_bp
from app.views import absences_bp from app.views import absences_bp
from app.api import bp as api_bp from app.api import api_bp
from app.api import api_web_bp as api_web_bp from app.api import api_web_bp
# https://scodoc.fr/ScoDoc # https://scodoc.fr/ScoDoc
app.register_blueprint(scodoc_bp) app.register_blueprint(scodoc_bp)
@ -265,7 +266,7 @@ def create_app(config_class=DevConfig):
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences" absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
) )
app.register_blueprint(api_bp, url_prefix="/ScoDoc/api") app.register_blueprint(api_bp, url_prefix="/ScoDoc/api")
app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/apiweb") app.register_blueprint(api_web_bp, url_prefix="/ScoDoc/<scodoc_dept>/api")
scodoc_log_formatter = LogRequestFormatter( scodoc_log_formatter = LogRequestFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"

View File

@ -4,7 +4,7 @@
from flask import Blueprint from flask import Blueprint
from flask import request from flask import request
bp = Blueprint("api", __name__) api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__) api_web_bp = Blueprint("apiweb", __name__)

View File

@ -8,9 +8,9 @@
from flask import jsonify from flask import jsonify
from app.api import bp from app.api import api_bp as bp
from app.api.errors import error_response from app.api.errors import error_response
from app.api.auth import permission_required_api from app.decorators import scodoc, permission_required
from app.models import Identite from app.models import Identite
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
@ -21,7 +21,8 @@ from app.scodoc.sco_permissions import Permission
@bp.route("/absences/etudid/<int:etudid>", methods=["GET"]) @bp.route("/absences/etudid/<int:etudid>", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def absences(etudid: int = None): def absences(etudid: int = None):
""" """
Retourne la liste des absences d'un étudiant donné Retourne la liste des absences d'un étudiant donné
@ -65,7 +66,8 @@ def absences(etudid: int = None):
@bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"]) @bp.route("/absences/etudid/<int:etudid>/just", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def absences_just(etudid: int = None): def absences_just(etudid: int = None):
""" """
Retourne la liste des absences justifiées d'un étudiant donné Retourne la liste des absences justifiées d'un étudiant donné
@ -120,7 +122,8 @@ def absences_just(etudid: int = None):
"/absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>", "/absences/abs_group_etat/group_id/<int:group_id>/date_debut/<string:date_debut>/date_fin/<string:date_fin>",
methods=["GET"], methods=["GET"],
) )
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def abs_groupe_etat(group_id: int, date_debut=None, date_fin=None): 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) Liste des absences d'un groupe (possibilité de choisir entre deux dates)

View File

@ -1,148 +0,0 @@
# -*- coding: UTF-8 -*
# Authentication code borrowed from Miguel Grinberg's Mega Tutorial
# (see https://github.com/miguelgrinberg/microblog)
# and modified for ScoDoc
# 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 functools import wraps
from flask import g
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth
import flask_login
from flask_login import current_user
from app import log
from app.auth.models import User
from app.api import bp, api_web_bp
from app.api.errors import error_response
from app.decorators import scodoc, permission_required
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()
@basic_auth.verify_password
def verify_password(username, password):
"Verify password for this user"
user: User = User.query.filter_by(user_name=username).first()
if user and user.check_password(password):
g.current_user = user
# note: est aussi basic_auth.current_user()
return user
@basic_auth.error_handler
def basic_auth_error(status):
"error response (401 for invalid auth.)"
return error_response(status)
@token_auth.verify_token
def verify_token(token) -> User:
"""Retrouve l'utilisateur à partir du jeton.
Si la requête n'a pas de jeton, token == "".
"""
user = User.check_token(token) if token else None
if user is not None:
flask_login.login_user(user)
g.current_user = user
return user
@token_auth.error_handler
def token_auth_error(status):
"Réponse en cas d'erreur d'auth."
return error_response(status)
@token_auth.get_user_roles
def get_user_roles(user):
return user.roles
def token_permission_required(permission):
"Décorateur pour les fonctions de l'API ScoDoc"
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
current_user = basic_auth.current_user()
if not current_user or not current_user.has_permission(permission, None):
if current_user:
message = f"API permission denied (user {current_user})"
else:
message = f"API permission denied (no user supplied)"
log(message)
# raise werkzeug.exceptions.Forbidden(description=message)
return error_response(403, message=None)
if not hasattr(g, "scodoc_dept"):
g.scodoc_dept = None
return f(*args, **kwargs)
# return decorated_function(token_auth.login_required())
return decorated_function
return decorator
def permission_required_api(permission_web, permission_api):
"""Décorateur pour les fonctions de l'API accessibles en mode jeton
ou en mode web.
Si cookie d'authentification web, utilise pour se logger et calculer les
permissions.
Sinon, tente le jeton jwt.
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
scodoc_dept = getattr(g, "scodoc_dept", None)
if not current_user.has_permission(permission_web, scodoc_dept):
# try API
return token_auth.login_required(
token_permission_required(permission_api)(f)
)(*args, **kwargs)
return f(*args, **kwargs)
return decorated_function
return decorator
def web_publish(route, function, permission, methods=("GET",)):
"""Declare a route for a python function protected by permission
using web http cookie
"""
return api_web_bp.route(route, methods=methods)(
scodoc(permission_required(permission)(function))
)
def api_publish(route, function, permission, methods=("GET",)):
"""Declare a route for a python function protected by permission
using API token
"""
return bp.route(route, methods=methods)(
token_auth.login_required(token_permission_required(permission)(function))
)

View File

@ -4,8 +4,8 @@ from flask import jsonify
import app import app
from app import models from app import models
from app.api import bp from app.api import api_bp as bp
from app.api.auth import permission_required_api from app.decorators import scodoc, permission_required
from app.models import Departement, FormSemestre from app.models import Departement, FormSemestre
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -22,21 +22,24 @@ def get_departement(dept_ident: str) -> Departement:
@bp.route("/departements", methods=["GET"]) @bp.route("/departements", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def departements(): def departements():
"""Liste les départements""" """Liste les départements"""
return jsonify([dept.to_dict() for dept in Departement.query]) return jsonify([dept.to_dict() for dept in Departement.query])
@bp.route("/departements_ids", methods=["GET"]) @bp.route("/departements_ids", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def departements_ids(): def departements_ids():
"""Liste des ids de départements""" """Liste des ids de départements"""
return jsonify([dept.id for dept in Departement.query]) return jsonify([dept.id for dept in Departement.query])
@bp.route("/departement/<string:acronym>", methods=["GET"]) @bp.route("/departement/<string:acronym>", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def departement(acronym: str): def departement(acronym: str):
""" """
Info sur un département. Accès par acronyme. Info sur un département. Accès par acronyme.
@ -55,7 +58,8 @@ def departement(acronym: str):
@bp.route("/departement/id/<int:dept_id>", methods=["GET"]) @bp.route("/departement/id/<int:dept_id>", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def departement_by_id(dept_id: int): def departement_by_id(dept_id: int):
""" """
Info sur un département. Accès par id. Info sur un département. Accès par id.
@ -65,7 +69,8 @@ def departement_by_id(dept_id: int):
@bp.route("/departement/<string:acronym>/etudiants", methods=["GET"]) @bp.route("/departement/<string:acronym>/etudiants", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def dept_etudiants(acronym: str): def dept_etudiants(acronym: str):
""" """
Retourne la liste des étudiants d'un département Retourne la liste des étudiants d'un département
@ -93,7 +98,8 @@ def dept_etudiants(acronym: str):
@bp.route("/departement/id/<int:dept_id>/etudiants", methods=["GET"]) @bp.route("/departement/id/<int:dept_id>/etudiants", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def dept_etudiants_by_id(dept_id: int): def dept_etudiants_by_id(dept_id: int):
""" """
Retourne la liste des étudiants d'un département d'id donné. Retourne la liste des étudiants d'un département d'id donné.
@ -103,7 +109,8 @@ def dept_etudiants_by_id(dept_id: int):
@bp.route("/departement/<string:acronym>/formsemestres_ids", methods=["GET"]) @bp.route("/departement/<string:acronym>/formsemestres_ids", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def dept_formsemestres_ids(acronym: str): def dept_formsemestres_ids(acronym: str):
"""liste des ids formsemestre du département""" """liste des ids formsemestre du département"""
dept = Departement.query.filter_by(acronym=acronym).first_or_404() dept = Departement.query.filter_by(acronym=acronym).first_or_404()
@ -111,7 +118,8 @@ def dept_formsemestres_ids(acronym: str):
@bp.route("/departement/id/<int:dept_id>/formsemestres_ids", methods=["GET"]) @bp.route("/departement/id/<int:dept_id>/formsemestres_ids", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def dept_formsemestres_ids_by_id(dept_id: int): def dept_formsemestres_ids_by_id(dept_id: int):
"""liste des ids formsemestre du département""" """liste des ids formsemestre du département"""
dept = Departement.query.get_or_404(dept_id) dept = Departement.query.get_or_404(dept_id)
@ -119,7 +127,8 @@ def dept_formsemestres_ids_by_id(dept_id: int):
@bp.route("/departement/<string:acronym>/formsemestres_courants", methods=["GET"]) @bp.route("/departement/<string:acronym>/formsemestres_courants", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def dept_formsemestres_courants(acronym: str): def dept_formsemestres_courants(acronym: str):
""" """
Liste des semestres actifs d'un département d'acronyme donné Liste des semestres actifs d'un département d'acronyme donné
@ -173,7 +182,8 @@ def dept_formsemestres_courants(acronym: str):
@bp.route("/departement/id/<int:dept_id>/formsemestres_courants", methods=["GET"]) @bp.route("/departement/id/<int:dept_id>/formsemestres_courants", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def dept_formsemestres_courants_by_id(dept_id: int): def dept_formsemestres_courants_by_id(dept_id: int):
""" """
Liste des semestres actifs d'un département d'id donné Liste des semestres actifs d'un département d'id donné

View File

@ -10,41 +10,46 @@
from flask import g, jsonify from flask import g, jsonify
from flask_login import current_user from flask_login import current_user
from flask_login import login_required
from sqlalchemy import or_ from sqlalchemy import or_
import app import app
from app.api import bp from app.api import api_bp as bp, api_web_bp
from app.api.errors import error_response from app.api.errors import error_response
from app.api.auth import permission_required_api, api_publish, web_publish
from app.api import tools from app.api import tools
from app.decorators import scodoc, permission_required
from app.models import Departement, FormSemestreInscription, FormSemestre, Identite from app.models import Departement, FormSemestreInscription, FormSemestre, Identite
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
# Un exemple:
@bp.route("/api_function/<int:arg>")
@api_web_bp.route("/api_function/<int:arg>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def api_function(arg: int): def api_function(arg: int):
"""Une fonction quelconque de l'API""" """Une fonction quelconque de l'API"""
# u = current_user return jsonify(
# dept = g.scodoc_dept # peut être None si accès API {"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept}
return jsonify({"current_user": current_user.to_dict(), "dept": g.scodoc_dept}) )
api_publish("/api_function/<int:arg>", api_function, Permission.APIView)
web_publish("/api_function/<int:arg>", api_function, Permission.ScoView)
@bp.route("/etudiants/courants", defaults={"long": False}) @bp.route("/etudiants/courants", defaults={"long": False})
@bp.route("/etudiants/courants/long", defaults={"long": True}) @bp.route("/etudiants/courants/long", defaults={"long": True})
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/etudiants/courants", defaults={"long": False})
@api_web_bp.route("/etudiants/courants/long", defaults={"long": True})
@login_required
@scodoc
@permission_required(Permission.ScoView)
def etudiants_courants(long=False): def etudiants_courants(long=False):
""" """
La liste des étudiants des semestres "courants" (tous département) La liste des étudiants des semestres "courants" (tous départements)
(date du jour comprise dans la période couverte par le sem.) (date du jour comprise dans la période couverte par le sem.)
dans lesquels l'utilisateur a le rôle APIView (donc tous si le dept du dans lesquels l'utilisateur a la permission ScoView
rôle est None). (donc tous si le dept du rôle est None).
Exemple de résultat : Exemple de résultat :
[ [
@ -89,9 +94,7 @@ def etudiants_courants(long=False):
"villedomicile": "VALPARAISO", "villedomicile": "VALPARAISO",
} }
""" """
allowed_depts = current_user.get_depts_with_permission( allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
Permission.APIView | Permission.ScoView
)
etuds = Identite.query.filter( etuds = Identite.query.filter(
Identite.id == FormSemestreInscription.etudid, Identite.id == FormSemestreInscription.etudid,
FormSemestreInscription.formsemestre_id == FormSemestre.id, FormSemestreInscription.formsemestre_id == FormSemestre.id,
@ -110,10 +113,15 @@ def etudiants_courants(long=False):
return jsonify(data) return jsonify(data)
@bp.route("/etudiant/etudid/<int:etudid>", methods=["GET"]) @bp.route("/etudiant/etudid/<int:etudid>")
@bp.route("/etudiant/nip/<string:nip>", methods=["GET"]) @bp.route("/etudiant/nip/<string:nip>")
@bp.route("/etudiant/ine/<string:ine>", methods=["GET"]) @bp.route("/etudiant/ine/<string:ine>")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/etudiant/etudid/<int:etudid>")
@api_web_bp.route("/etudiant/nip/<string:nip>")
@api_web_bp.route("/etudiant/ine/<string:ine>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def etudiant(etudid: int = None, nip: str = None, ine: str = None): def etudiant(etudid: int = None, nip: str = None, ine: str = None):
""" """
Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé. Retourne les informations de l'étudiant correspondant, ou 404 si non trouvé.
@ -167,7 +175,11 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
@bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"]) @bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
@bp.route("/etudiants/nip/<string:nip>", methods=["GET"]) @bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
@bp.route("/etudiants/ine/<string:ine>", methods=["GET"]) @bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/etudiants/etudid/<int:etudid>", methods=["GET"])
@api_web_bp.route("/etudiants/nip/<string:nip>", methods=["GET"])
@api_web_bp.route("/etudiants/ine/<string:ine>", methods=["GET"])
@scodoc
@permission_required(Permission.ScoView)
def etudiants(etudid: int = None, nip: str = None, ine: str = None): def etudiants(etudid: int = None, nip: str = None, ine: str = None):
""" """
Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie Info sur le ou les étudiants correspondant. Comme /etudiant mais renvoie
@ -176,9 +188,7 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a Dans 99% des cas, la liste contient un seul étudiant, mais si l'étudiant a
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.). été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
""" """
allowed_depts = current_user.get_depts_with_permission( allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
Permission.APIView | Permission.ScoView
)
if etudid is not None: if etudid is not None:
query = Identite.query.filter_by(id=etudid) query = Identite.query.filter_by(id=etudid)
elif nip is not None: elif nip is not None:
@ -201,14 +211,20 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres") @bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@bp.route("/etudiant/nip/<string:nip>/formsemestres") @bp.route("/etudiant/nip/<string:nip>/formsemestres")
@bp.route("/etudiant/ine/<string:ine>/formsemestres") @bp.route("/etudiant/ine/<string:ine>/formsemestres")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
@api_web_bp.route("/etudiant/nip/<string:nip>/formsemestres")
@api_web_bp.route("/etudiant/ine/<string:ine>/formsemestres")
@scodoc
@permission_required(Permission.ScoView)
def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None): 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. Liste des semestres qu'un étudiant a suivi, triés par ordre chronologique.
Attention, si accès via NIP ou INE, les semestres peuvent être de départements différents Accès par etudid, nip ou ine.
(si l'étudiant a changé de département). L'id du département est `dept_id`.
Accès par etudid, nip ou ine Attention, si accès via NIP ou INE, les semestres peuvent être de départements
différents (si l'étudiant a changé de département). L'id du département est `dept_id`.
Si accès par département, ne retourne que les formsemestre suivis dans le département.
Exemple de résultat : Exemple de résultat :
[ [
@ -265,6 +281,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
message="parametre manquant", message="parametre manquant",
) )
if g.scodoc_dept is not None:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestres = query.order_by(FormSemestre.date_debut) formsemestres = query.order_by(FormSemestre.date_debut)
return jsonify( return jsonify(
@ -287,22 +306,12 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
methods=["GET"], methods=["GET"],
defaults={"version": "long", "pdf": False}, defaults={"version": "long", "pdf": False},
) )
# Version PDF non fonctionnelle # Version PDF non testée
@bp.route( @bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
methods=["GET"], methods=["GET"],
defaults={"version": "long", "pdf": True}, defaults={"version": "long", "pdf": True},
) )
# @bp.route(
# "/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
# methods=["GET"],
# defaults={"version": "long", "pdf": True},
# )
# @bp.route(
# "/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
# methods=["GET"],
# defaults={"version": "long", "pdf": True},
# )
@bp.route( @bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"], methods=["GET"],
@ -333,8 +342,60 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
methods=["GET"], methods=["GET"],
defaults={"version": "short", "pdf": True}, defaults={"version": "short", "pdf": True},
) )
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route(
def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner en version pdf "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"],
defaults={"version": "long", "pdf": False},
)
@api_web_bp.route(
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"],
defaults={"version": "long", "pdf": False},
)
@api_web_bp.route(
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin",
methods=["GET"],
defaults={"version": "long", "pdf": False},
)
# Version PDF non testée
@api_web_bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
methods=["GET"],
defaults={"version": "long", "pdf": True},
)
@api_web_bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"],
defaults={"version": "short", "pdf": False},
)
@api_web_bp.route(
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"],
defaults={"version": "short", "pdf": False},
)
@api_web_bp.route(
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short",
methods=["GET"],
defaults={"version": "short", "pdf": False},
)
@api_web_bp.route(
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
methods=["GET"],
defaults={"version": "short", "pdf": True},
)
@api_web_bp.route(
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
methods=["GET"],
defaults={"version": "short", "pdf": True},
)
@api_web_bp.route(
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/bulletin/short/pdf",
methods=["GET"],
defaults={"version": "short", "pdf": True},
)
@scodoc
@permission_required(Permission.ScoView)
def etudiant_bulletin_semestre(
formsemestre_id, formsemestre_id,
etudid: int = None, etudid: int = None,
nip: str = None, nip: str = None,
@ -354,7 +415,8 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner
""" """
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
dept = Departement.query.filter_by(id=formsemestre.dept_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")
if etudid is not None: if etudid is not None:
query = Identite.query.filter_by(id=etudid) query = Identite.query.filter_by(id=etudid)
elif nip is not None: elif nip is not None:
@ -391,25 +453,14 @@ def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups", "/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/groups",
methods=["GET"], methods=["GET"],
) )
@bp.route( @scodoc
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/groups", @permission_required(Permission.ScoView)
methods=["GET"], def etudiant_groups(formsemestre_id: int, etudid: int = None):
)
@bp.route(
"/etudiant/ine/<string:ine>/formsemestre/<int:formsemestre_id>/groups",
methods=["GET"],
)
@permission_required_api(Permission.ScoView, Permission.APIView)
def etudiant_groups(
formsemestre_id: int, etudid: int = None, nip: int = None, ine: int = None
):
""" """
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre
etudid : l'etudid d'un étudiant etudid : l'etudid d'un étudiant
nip : le code nip d'un étudiant
ine : le code ine d'un étudiant
Exemple de résultat : Exemple de résultat :
[ [
@ -438,30 +489,18 @@ def etudiant_groups(
] ]
""" """
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre = query.first()
if formsemestre is None: if formsemestre is None:
return error_response( return error_response(
404, 404,
message="formsemestre inconnu", message="formsemestre inconnu",
) )
dept = Departement.query.get(formsemestre.dept_id) dept = formsemestre.departement
if etudid is not None: etud = Identite.query.filter_by(id=etudid, dept_id=dept.id).first_or_404(etudid)
query = Identite.query.filter_by(id=etudid)
elif nip is not None:
query = Identite.query.filter_by(code_nip=nip, dept_id=dept.id)
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",
)
etud = query.first()
if etud is None:
return error_response(
404,
message="etudiant inconnu",
)
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
data = sco_groups.get_etud_groups(etud.id, formsemestre.id) data = sco_groups.get_etud_groups(etud.id, formsemestre.id)

View File

@ -8,21 +8,24 @@
ScoDoc 9 API : accès aux évaluations ScoDoc 9 API : accès aux évaluations
""" """
from flask import jsonify from flask import g, jsonify
from flask_login import login_required
import app import app
from app import models from app.api import api_bp as bp, api_web_bp
from app.api import bp from app.decorators import scodoc, permission_required
from app.api.auth import permission_required_api
from app.api.errors import error_response from app.api.errors import error_response
from app.models import Evaluation from app.models import Evaluation, ModuleImpl, FormSemestre
from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes from app.scodoc.sco_evaluation_db import do_evaluation_get_all_notes
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"]) @bp.route("/evaluations/<int:moduleimpl_id>")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/evaluations/<int:moduleimpl_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def evaluations(moduleimpl_id: int): def evaluations(moduleimpl_id: int):
""" """
Retourne la liste des évaluations d'un moduleimpl Retourne la liste des évaluations d'un moduleimpl
@ -54,17 +57,21 @@ def evaluations(moduleimpl_id: int):
... ...
] ]
""" """
# Récupération de toutes les évaluations query = Evaluation.query.filter_by(id=moduleimpl_id)
evals = Evaluation.query.filter_by(id=moduleimpl_id) if g.scodoc_dept:
query = (
# Mise en forme des données query.join(ModuleImpl)
data = [d.to_dict() for d in evals] .join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
return jsonify(data) )
return jsonify([d.to_dict() for d in query])
@bp.route("/evaluation/eval_notes/<int:evaluation_id>", methods=["GET"]) @bp.route("/evaluation/<int:evaluation_id>/notes")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/evaluation/<int:evaluation_id>/notes")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def evaluation_notes(evaluation_id: int): def evaluation_notes(evaluation_id: int):
""" """
Retourne la liste des notes à partir de l'id d'une évaluation donnée Retourne la liste des notes à partir de l'id d'une évaluation donnée
@ -94,7 +101,15 @@ def evaluation_notes(evaluation_id: int):
... ...
} }
""" """
evaluation = models.Evaluation.query.filter_by(id=evaluation_id).first_or_404() query = Evaluation.query.filter_by(id=evaluation_id)
if g.scodoc_dept:
query = (
query.join(ModuleImpl)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
)
evaluation = query.first_or_404()
dept = evaluation.moduleimpl.formsemestre.departement dept = evaluation.moduleimpl.formsemestre.departement
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)

View File

@ -8,43 +8,58 @@
ScoDoc 9 API : accès aux formations ScoDoc 9 API : accès aux formations
""" """
from flask import jsonify from flask import g, jsonify
from flask_login import login_required
import app import app
from app import models from app.api import api_bp as bp, api_web_bp
from app.api import bp
from app.api.errors import error_response from app.api.errors import error_response
from app.api.auth import permission_required_api from app.decorators import scodoc, permission_required
from app.models.formations import Formation from app.models.formations import Formation
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@bp.route("/formations", methods=["GET"]) @bp.route("/formations")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formations")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formations(): def formations():
""" """
Retourne la liste de toutes les formations (tous départements) Retourne la liste de toutes les formations (tous départements)
""" """
data = [d.to_dict() for d in models.Formation.query] query = Formation.query
return jsonify(data) if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
return jsonify([d.to_dict() for d in query])
@bp.route("/formations_ids", methods=["GET"]) @bp.route("/formations_ids")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formations_ids")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formations_ids(): def formations_ids():
""" """
Retourne la liste de toutes les id de formations (tous départements) Retourne la liste de toutes les id de formations (tous départements)
Exemple de résultat : [ 17, 99, 32 ] Exemple de résultat : [ 17, 99, 32 ]
""" """
data = [d.id for d in models.Formation.query] query = Formation.query
return jsonify(data) if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
return jsonify([d.id for d in query])
@bp.route("/formation/<int:formation_id>", methods=["GET"]) @bp.route("/formation/<int:formation_id>")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formation/<int:formation_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formation_by_id(formation_id: int): def formation_by_id(formation_id: int):
""" """
La formation d'id donné La formation d'id donné
@ -66,21 +81,31 @@ def formation_by_id(formation_id: int):
"formation_id": 1 "formation_id": 1
} }
""" """
formation = models.Formation.query.get_or_404(formation_id) query = Formation.query.filter_by(id=formation_id)
return jsonify(formation.to_dict()) if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
return jsonify(query.first_or_404().to_dict())
@bp.route( @bp.route(
"/formation/formation_export/<int:formation_id>", "/formation/<int:formation_id>/export",
methods=["GET"],
defaults={"export_ids": False}, defaults={"export_ids": False},
) )
@bp.route( @bp.route(
"/formation/formation_export/<int:formation_id>/with_ids", "/formation/<int:formation_id>/export/with_ids",
methods=["GET"],
defaults={"export_ids": True}, defaults={"export_ids": True},
) )
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route(
"/formation/<int:formation_id>/export",
defaults={"export_ids": False},
)
@api_web_bp.route(
"/formation/<int:formation_id>/export/with_ids",
defaults={"export_ids": True},
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formation_export_by_formation_id(formation_id: int, export_ids=False): def formation_export_by_formation_id(formation_id: int, export_ids=False):
""" """
Retourne la formation, avec UE, matières, modules Retourne la formation, avec UE, matières, modules
@ -177,7 +202,10 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
] ]
} }
""" """
formation = Formation.query.get_or_404(formation_id) query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formation = query.first_or_404(formation_id)
app.set_sco_dept(formation.departement.acronym) app.set_sco_dept(formation.departement.acronym)
try: try:
data = sco_formations.formation_export(formation_id, export_ids) data = sco_formations.formation_export(formation_id, export_ids)
@ -187,11 +215,36 @@ def formation_export_by_formation_id(formation_id: int, export_ids=False):
return jsonify(data) return jsonify(data)
@bp.route("/formation/moduleimpl/<int:moduleimpl_id>", methods=["GET"]) @bp.route("/formation/<int:formation_id>/referentiel_competences")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formation/<int:formation_id>/referentiel_competences")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def referentiel_competences(formation_id: int):
"""
Retourne le référentiel de compétences
formation_id : l'id d'une formation
return null si pas de référentiel associé.
"""
query = Formation.query.filter_by(id=formation_id)
if g.scodoc_dept:
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())
@bp.route("/moduleimpl/<int:moduleimpl_id>")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def moduleimpl(moduleimpl_id: int): def moduleimpl(moduleimpl_id: int):
""" """
Retourne un module moduleimpl en fonction de son id Retourne un moduleimpl en fonction de son id
moduleimpl_id : l'id d'un moduleimpl moduleimpl_id : l'id d'un moduleimpl
@ -224,24 +277,8 @@ def moduleimpl(moduleimpl_id: int):
} }
} }
""" """
modimpl = models.ModuleImpl.query.get_or_404(moduleimpl_id) query = ModuleImpl.query.filter_by(id=moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl = query.first_or_404()
return jsonify(modimpl.to_dict()) return jsonify(modimpl.to_dict())
@bp.route(
"/formation/<int:formation_id>/referentiel_competences",
methods=["GET"],
)
@permission_required_api(Permission.ScoView, Permission.APIView)
def referentiel_competences(formation_id: int):
"""
Retourne le référentiel de compétences
formation_id : l'id d'une formation
return null si pas de référentiel associé.
"""
formation = models.Formation.query.get_or_404(formation_id)
if formation.referentiel_competence is None:
return jsonify(None)
return jsonify(formation.referentiel_competence.to_dict())

View File

@ -7,17 +7,23 @@
""" """
ScoDoc 9 API : accès aux formsemestres ScoDoc 9 API : accès aux formsemestres
""" """
from flask import abort, jsonify, request from flask import g, jsonify, request
from flask_login import login_required
import app import app
from app import models from app.api import api_bp as bp, api_web_bp
from app.api import bp from app.decorators import scodoc, permission_required
from app.api.auth import permission_required_api
from app.api.errors import error_response from app.api.errors import error_response
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Evaluation, FormSemestre, FormSemestreEtape, ModuleImpl from app.models import (
Departement,
Evaluation,
FormSemestre,
FormSemestreEtape,
ModuleImpl,
)
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -25,8 +31,11 @@ from app.scodoc.sco_utils import ModuleType
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@bp.route("/formsemestre/<int:formsemestre_id>", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formsemestre/<int:formsemestre_id>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_infos(formsemestre_id: int): def formsemestre_infos(formsemestre_id: int):
""" """
Information sur le formsemestre indiqué. Information sur le formsemestre indiqué.
@ -64,12 +73,18 @@ def formsemestre_infos(formsemestre_id: int):
} }
""" """
formsemestre: FormSemestre = models.FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
return jsonify(formsemestre.to_dict_api()) return jsonify(formsemestre.to_dict_api())
@bp.route("/formsemestres/query", methods=["GET"]) @bp.route("/formsemestres/query")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formsemestres/query")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestres_query(): def formsemestres_query():
""" """
Retourne les formsemestres filtrés par Retourne les formsemestres filtrés par
@ -85,6 +100,8 @@ def formsemestres_query():
dept_acronym = request.args.get("dept_acronym") dept_acronym = request.args.get("dept_acronym")
dept_id = request.args.get("dept_id") dept_id = request.args.get("dept_id")
formsemestres = FormSemestre.query formsemestres = FormSemestre.query
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
if etape_apo is not None: if etape_apo is not None:
formsemestres = formsemestres.join(FormSemestreEtape).filter( formsemestres = formsemestres.join(FormSemestreEtape).filter(
FormSemestreEtape.etape_apo == etape_apo FormSemestreEtape.etape_apo == etape_apo
@ -100,9 +117,7 @@ def formsemestres_query():
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
) )
if dept_acronym is not None: if dept_acronym is not None:
formsemestres = formsemestres.join(models.Departement).filter_by( formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
acronym=dept_acronym
)
if dept_id is not None: if dept_id is not None:
try: try:
dept_id = int(dept_id) dept_id = int(dept_id)
@ -113,8 +128,11 @@ def formsemestres_query():
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres]) return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def bulletins(formsemestre_id: int): def bulletins(formsemestre_id: int):
""" """
Retourne les bulletins d'un formsemestre donné Retourne les bulletins d'un formsemestre donné
@ -123,7 +141,10 @@ def bulletins(formsemestre_id: int):
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
""" """
formsemestre = models.FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
data = [] data = []
@ -134,11 +155,11 @@ def bulletins(formsemestre_id: int):
return jsonify(data) return jsonify(data)
@bp.route( @bp.route("/formsemestre/<int:formsemestre_id>/programme")
"/formsemestre/<int:formsemestre_id>/programme", @api_web_bp.route("/formsemestre/<int:formsemestre_id>/programme")
methods=["GET"], @login_required
) @scodoc
@permission_required_api(Permission.ScoView, Permission.APIView) @permission_required(Permission.ScoView)
def formsemestre_programme(formsemestre_id: int): def formsemestre_programme(formsemestre_id: int):
""" """
Retourne la liste des Ues, ressources et SAE d'un semestre Retourne la liste des Ues, ressources et SAE d'un semestre
@ -204,7 +225,10 @@ def formsemestre_programme(formsemestre_id: int):
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ] "modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
} }
""" """
formsemestre: FormSemestre = models.FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
ues = formsemestre.query_ues() ues = formsemestre.query_ues()
m_list = { m_list = {
ModuleType.RESSOURCE: [], ModuleType.RESSOURCE: [],
@ -226,29 +250,41 @@ def formsemestre_programme(formsemestre_id: int):
@bp.route( @bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants", "/formsemestre/<int:formsemestre_id>/etudiants",
methods=["GET"],
defaults={"etat": scu.INSCRIT}, defaults={"etat": scu.INSCRIT},
) )
@bp.route( @bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires", "/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
methods=["GET"],
defaults={"etat": scu.DEMISSION}, defaults={"etat": scu.DEMISSION},
) )
@bp.route( @bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/defaillants", "/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
methods=["GET"],
defaults={"etat": scu.DEF}, defaults={"etat": scu.DEF},
) )
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants",
defaults={"etat": scu.INSCRIT},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
defaults={"etat": scu.DEMISSION},
)
@api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
defaults={"etat": scu.DEF},
)
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_etudiants(formsemestre_id: int, etat: str): def formsemestre_etudiants(formsemestre_id: int, etat: str):
""" """
Retourne la liste des étudiants d'un formsemestre Retourne la liste des étudiants d'un formsemestre
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre
""" """
formsemestre: FormSemestre = models.FormSemestre.query.filter_by( query = FormSemestre.query.filter_by(id=formsemestre_id)
id=formsemestre_id if g.scodoc_dept:
).first_or_404() query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
inscriptions = [ins for ins in formsemestre.inscriptions if ins.etat == etat] inscriptions = [ins for ins in formsemestre.inscriptions if ins.etat == etat]
etuds = [ins.etud.to_dict_short() for ins in inscriptions] etuds = [ins.etud.to_dict_short() for ins in inscriptions]
@ -260,8 +296,11 @@ def formsemestre_etudiants(formsemestre_id: int, etat: str):
return jsonify(etuds) return jsonify(etuds)
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def etat_evals(formsemestre_id: int): def etat_evals(formsemestre_id: int):
""" """
Informations sur l'état des évaluations d'un formsemestre. Informations sur l'état des évaluations d'un formsemestre.
@ -297,7 +336,10 @@ def etat_evals(formsemestre_id: int):
}, },
] ]
""" """
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
@ -364,8 +406,11 @@ def etat_evals(formsemestre_id: int):
return jsonify(result) return jsonify(result)
@bp.route("/formsemestre/<int:formsemestre_id>/resultats", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>/resultats")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formsemestre/<int:formsemestre_id>/resultats")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_resultat(formsemestre_id: int): def formsemestre_resultat(formsemestre_id: int):
"""Tableau récapitulatif des résultats """Tableau récapitulatif des résultats
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules. Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
@ -375,7 +420,10 @@ def formsemestre_resultat(formsemestre_id: int):
return error_response(404, "invalid format specification") return error_response(404, "invalid format specification")
convert_values = format_spec != "raw" convert_values = format_spec != "raw"
formsemestre = FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym) app.set_sco_dept(formsemestre.departement.acronym)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
rows, footer_rows, titles, column_ids = res.get_table_recap( rows, footer_rows, titles, column_ids = res.get_table_recap(

View File

@ -2,9 +2,8 @@
# from flask import jsonify # from flask import jsonify
# from app import models # from app import models
# from app.api import bp # from app.api import api_bp as bp
# from app.api.errors import error_response # from app.api.errors import error_response
# from app.api.auth import permission_required_api
# from app.scodoc.sco_prepajury import feuille_preparation_jury # from app.scodoc.sco_prepajury import feuille_preparation_jury
# from app.scodoc.sco_pvjury import formsemestre_pvjury # from app.scodoc.sco_pvjury import formsemestre_pvjury

View File

@ -31,19 +31,22 @@ Contrib @jmp
from datetime import datetime from datetime import datetime
from flask import jsonify, g, send_file from flask import jsonify, g, send_file
from flask_login import login_required
from app.api import bp from app.api import api_bp as bp, api_web_bp
from app.api import requested_format from app.api import requested_format
from app.api.auth import token_auth
from app.api.errors import error_response from app.api.errors import error_response
from app.models import Departement from app.models import Departement
from app.scodoc.sco_logos import list_logos, find_logo from app.scodoc.sco_logos import list_logos, find_logo
from app.api.auth import permission_required_api from app.decorators import scodoc, permission_required
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
# Note: l'API logos n'est accessible qu'en mode global (avec jeton, sans dept)
@bp.route("/logos", methods=["GET"])
@permission_required_api(Permission.ScoView, Permission.APIView) @bp.route("/logos")
@scodoc
@permission_required(Permission.ScoView)
def api_get_glob_logos(): def api_get_glob_logos():
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
return error_response(401, message="accès interdit") return error_response(401, message="accès interdit")
@ -54,8 +57,9 @@ def api_get_glob_logos():
return jsonify(list(logos.keys())) return jsonify(list(logos.keys()))
@bp.route("/logos/<string:logoname>", methods=["GET"]) @bp.route("/logos/<string:logoname>")
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def api_get_glob_logo(logoname): def api_get_glob_logo(logoname):
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
return error_response(401, message="accès interdit") return error_response(401, message="accès interdit")
@ -70,8 +74,9 @@ def api_get_glob_logo(logoname):
) )
@bp.route("/departements/<string:departement>/logos", methods=["GET"]) @bp.route("/departements/<string:departement>/logos")
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def api_get_local_logos(departement): def api_get_local_logos(departement):
dept_id = Departement.from_acronym(departement).id dept_id = Departement.from_acronym(departement).id
if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): if not g.current_user.has_permission(Permission.ScoChangePreferences, departement):
@ -80,8 +85,9 @@ def api_get_local_logos(departement):
return jsonify(list(logos.keys())) return jsonify(list(logos.keys()))
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"]) @bp.route("/departements/<string:departement>/logos/<string:logoname>")
@permission_required_api(Permission.ScoView, Permission.APIView) @scodoc
@permission_required(Permission.ScoView)
def api_get_local_logo(departement, logoname): def api_get_local_logo(departement, logoname):
# format = requested_format("jpg", ['png', 'jpg']) XXX ? # format = requested_format("jpg", ['png', 'jpg']) XXX ?
dept_id = Departement.from_acronym(departement).id dept_id = Departement.from_acronym(departement).id

View File

@ -7,12 +7,14 @@
""" """
ScoDoc 9 API : partitions ScoDoc 9 API : partitions
""" """
from flask import jsonify, request from flask import g, jsonify, request
from flask_login import login_required
import app import app
from app import db, log from app import db, log
from app.api import bp from app import api
from app.api.auth import permission_required_api 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.api.errors import error_response
from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import FormSemestre, FormSemestreInscription, Identite
from app.models import GroupDescr, Partition from app.models import GroupDescr, Partition
@ -22,8 +24,10 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@bp.route("/partition/<int:partition_id>", methods=["GET"]) @bp.route("/partition/<int:partition_id>")
@permission_required_api(Permission.ScoView, Permission.APIView) @login_required
@scodoc
@permission_required(Permission.ScoView)
def partition_info(partition_id: int): def partition_info(partition_id: int):
"""Info sur une partition. """Info sur une partition.
@ -44,12 +48,18 @@ def partition_info(partition_id: int):
} }
``` ```
""" """
partition = Partition.query.get_or_404(partition_id) query = Partition.query.filter_by(id=partition_id)
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 jsonify(partition.to_dict(with_groups=True))
@bp.route("/formsemestre/<int:formsemestre_id>/partitions", methods=["GET"]) @bp.route("/formsemestre/<int:formsemestre_id>/partitions")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/formsemestre/<int:formsemestre_id>/partitions")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_partitions(formsemestre_id: int): def formsemestre_partitions(formsemestre_id: int):
"""Liste de toutes les partitions d'un formsemestre """Liste de toutes les partitions d'un formsemestre
@ -70,7 +80,10 @@ def formsemestre_partitions(formsemestre_id: int):
} }
""" """
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
partitions = sorted(formsemestre.partitions, key=lambda p: p.numero or 0) partitions = sorted(formsemestre.partitions, key=lambda p: p.numero or 0)
return jsonify( return jsonify(
{ {
@ -81,8 +94,11 @@ def formsemestre_partitions(formsemestre_id: int):
) )
@bp.route("/group/<int:group_id>/etudiants", methods=["GET"]) @bp.route("/group/<int:group_id>/etudiants")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/group/<int:group_id>/etudiants")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def etud_in_group(group_id: int): def etud_in_group(group_id: int):
""" """
Retourne la liste des étudiants dans un groupe Retourne la liste des étudiants dans un groupe
@ -103,18 +119,31 @@ def etud_in_group(group_id: int):
... ...
] ]
""" """
group = GroupDescr.query.get_or_404(group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
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 jsonify([etud.to_dict_short() for etud in group.etuds])
@bp.route("/group/<int:group_id>/etudiants/query", methods=["GET"]) @bp.route("/group/<int:group_id>/etudiants/query")
@permission_required_api(Permission.ScoView, Permission.APIView) @api_web_bp.route("/group/<int:group_id>/etudiants/query")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def etud_in_group_query(group_id: int): def etud_in_group_query(group_id: int):
"""Etudiants du groupe, filtrés par état""" """Etudiants du groupe, filtrés par état"""
etat = request.args.get("etat") etat = request.args.get("etat")
if etat not in {scu.INSCRIT, scu.DEMISSION, scu.DEF}: if etat not in {scu.INSCRIT, scu.DEMISSION, scu.DEF}:
return error_response(404, "etat: valeur invalide") return error_response(404, "etat: valeur invalide")
group = GroupDescr.query.get_or_404(group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group = query.first_or_404() # just tro ckeck that group exists in accessible dept
query = ( query = (
Identite.query.join(FormSemestreInscription) Identite.query.join(FormSemestreInscription)
.filter_by(formsemestre_id=group.partition.formsemestre_id, etat=etat) .filter_by(formsemestre_id=group.partition.formsemestre_id, etat=etat)
@ -126,11 +155,21 @@ def etud_in_group_query(group_id: int):
@bp.route("/group/<int:group_id>/set_etudiant/<int:etudid>", methods=["POST"]) @bp.route("/group/<int:group_id>/set_etudiant/<int:etudid>", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/group/<int:group_id>/set_etudiant/<int:etudid>", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def set_etud_group(etudid: int, group_id: int): def set_etud_group(etudid: int, group_id: int):
"""Affecte l'étudiant au groupe indiqué""" """Affecte l'étudiant au groupe indiqué"""
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
group = GroupDescr.query.get_or_404(group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
query.join(Partition)
.join(FormSemestre)
.filter_by(dept_id=group.scodoc_dept_id)
)
group = query.first_or_404()
if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: 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 error_response(404, "etud non inscrit au formsemestre du groupe")
groups = ( groups = (
@ -139,11 +178,11 @@ def set_etud_group(etudid: int, group_id: int):
.filter_by(etudid=etudid) .filter_by(etudid=etudid)
) )
ok = False ok = False
for g in groups: for group in groups:
if g.id == group_id: if group.id == group_id:
ok = True ok = True
else: else:
g.etuds.remove(etud) group.etuds.remove(etud)
if not ok: if not ok:
group.etuds.append(etud) group.etuds.append(etud)
log(f"set_etud_group({etud}, {group})") log(f"set_etud_group({etud}, {group})")
@ -153,11 +192,21 @@ def set_etud_group(etudid: int, group_id: int):
@bp.route("/group/<int:group_id>/remove_etudiant/<int:etudid>", methods=["POST"]) @bp.route("/group/<int:group_id>/remove_etudiant/<int:etudid>", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route(
"/group/<int:group_id>/remove_etudiant/<int:etudid>", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def group_remove_etud(group_id: int, etudid: int): def group_remove_etud(group_id: int, etudid: int):
"""Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien.""" """Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien."""
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
group = GroupDescr.query.get_or_404(group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group = query.first_or_404()
group.etuds.remove(etud) group.etuds.remove(etud)
db.session.commit() db.session.commit()
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
@ -167,20 +216,28 @@ def group_remove_etud(group_id: int, etudid: int):
@bp.route( @bp.route(
"/partition/<int:partition_id>/remove_etudiant/<int:etudid>", methods=["POST"] "/partition/<int:partition_id>/remove_etudiant/<int:etudid>", methods=["POST"]
) )
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route(
"/partition/<int:partition_id>/remove_etudiant/<int:etudid>", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def partition_remove_etud(partition_id: int, etudid: int): def partition_remove_etud(partition_id: int, etudid: int):
"""Enlève l'étudiant de tous les groupes de cette partition """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) (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition)
""" """
etud = Identite.query.get_or_404(etudid) etud = Identite.query.get_or_404(etudid)
partition = Partition.query.get_or_404(partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition = query.first_or_404()
groups = ( groups = (
GroupDescr.query.filter_by(partition_id=partition_id) GroupDescr.query.filter_by(partition_id=partition_id)
.join(group_membership) .join(group_membership)
.filter_by(etudid=etudid) .filter_by(etudid=etudid)
) )
for g in groups: for group in groups:
g.etuds.remove(etud) group.etuds.remove(etud)
db.session.commit() db.session.commit()
app.set_sco_dept(partition.formsemestre.departement.acronym) app.set_sco_dept(partition.formsemestre.departement.acronym)
sco_cache.invalidate_formsemestre(partition.formsemestre_id) sco_cache.invalidate_formsemestre(partition.formsemestre_id)
@ -188,7 +245,10 @@ def partition_remove_etud(partition_id: int, etudid: int):
@bp.route("/partition/<int:partition_id>/group/create", methods=["POST"]) @bp.route("/partition/<int:partition_id>/group/create", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/partition/<int:partition_id>/group/create", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def group_create(partition_id: int): def group_create(partition_id: int):
"""Création d'un groupe dans une partition """Création d'un groupe dans une partition
@ -197,7 +257,10 @@ def group_create(partition_id: int):
"group_name" : nom_du_groupe, "group_name" : nom_du_groupe,
} }
""" """
partition: Partition = Partition.query.get_or_404(partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
if not partition.groups_editable: if not partition.groups_editable:
return error_response(404, "partition non editable") return error_response(404, "partition non editable")
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
@ -218,10 +281,18 @@ def group_create(partition_id: int):
@bp.route("/group/<int:group_id>/delete", methods=["POST"]) @bp.route("/group/<int:group_id>/delete", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/group/<int:group_id>/delete", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def group_delete(group_id: int): def group_delete(group_id: int):
"""Suppression d'un groupe""" """Suppression d'un groupe"""
group = GroupDescr.query.get_or_404(group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group: GroupDescr = query.first_or_404()
if not group.partition.groups_editable: if not group.partition.groups_editable:
return error_response(404, "partition non editable") return error_response(404, "partition non editable")
formsemestre_id = group.partition.formsemestre_id formsemestre_id = group.partition.formsemestre_id
@ -234,10 +305,18 @@ def group_delete(group_id: int):
@bp.route("/group/<int:group_id>/edit", methods=["POST"]) @bp.route("/group/<int:group_id>/edit", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/group/<int:group_id>/edit", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def group_edit(group_id: int): def group_edit(group_id: int):
"""Edit a group""" """Edit a group"""
group: GroupDescr = GroupDescr.query.get_or_404(group_id) query = GroupDescr.query.filter_by(id=group_id)
if g.scodoc_dept:
query = (
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
)
group: GroupDescr = query.first_or_404()
if not group.partition.groups_editable: if not group.partition.groups_editable:
return error_response(404, "partition non editable") return error_response(404, "partition non editable")
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
@ -255,7 +334,12 @@ def group_edit(group_id: int):
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]) @bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def partition_create(formsemestre_id: int): def partition_create(formsemestre_id: int):
"""Création d'une partition dans un semestre """Création d'une partition dans un semestre
@ -268,7 +352,10 @@ def partition_create(formsemestre_id: int):
"groups_editable":bool "groups_editable":bool
} }
""" """
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
if partition_name is None: if partition_name is None:
@ -301,12 +388,20 @@ def partition_create(formsemestre_id: int):
@bp.route("/formsemestre/<int:formsemestre_id>/partitions/order", methods=["POST"]) @bp.route("/formsemestre/<int:formsemestre_id>/partitions/order", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route(
"/formsemestre/<int:formsemestre_id>/partitions/order", methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def formsemestre_order_partitions(formsemestre_id: int): def formsemestre_order_partitions(formsemestre_id: int):
"""Modifie l'ordre des partitions du formsemestre """Modifie l'ordre des partitions du formsemestre
JSON args: [partition_id1, partition_id2, ...] JSON args: [partition_id1, partition_id2, ...]
""" """
formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
partition_ids = request.get_json(force=True) # may raise 400 Bad Request partition_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(partition_ids, int) and not all( if not isinstance(partition_ids, int) and not all(
isinstance(x, int) for x in partition_ids isinstance(x, int) for x in partition_ids
@ -326,12 +421,18 @@ def formsemestre_order_partitions(formsemestre_id: int):
@bp.route("/partition/<int:partition_id>/groups/order", methods=["POST"]) @bp.route("/partition/<int:partition_id>/groups/order", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/partition/<int:partition_id>/groups/order", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def partition_order_groups(partition_id: int): def partition_order_groups(partition_id: int):
"""Modifie l'ordre des groupes de la partition """Modifie l'ordre des groupes de la partition
JSON args: [group_id1, group_id2, ...] JSON args: [group_id1, group_id2, ...]
""" """
partition = Partition.query.get_or_404(partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
group_ids = request.get_json(force=True) # may raise 400 Bad Request group_ids = request.get_json(force=True) # may raise 400 Bad Request
if not isinstance(group_ids, int) and not all( if not isinstance(group_ids, int) and not all(
isinstance(x, int) for x in group_ids isinstance(x, int) for x in group_ids
@ -351,7 +452,10 @@ def partition_order_groups(partition_id: int):
@bp.route("/partition/<int:partition_id>/edit", methods=["POST"]) @bp.route("/partition/<int:partition_id>/edit", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/partition/<int:partition_id>/edit", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def partition_edit(partition_id: int): def partition_edit(partition_id: int):
"""Modification d'une partition dans un semestre """Modification d'une partition dans un semestre
@ -365,7 +469,10 @@ def partition_edit(partition_id: int):
"groups_editable":bool "groups_editable":bool
} }
""" """
partition = Partition.query.get_or_404(partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
data = request.get_json(force=True) # may raise 400 Bad Request data = request.get_json(force=True) # may raise 400 Bad Request
modified = False modified = False
partition_name = data.get("partition_name") partition_name = data.get("partition_name")
@ -403,7 +510,10 @@ def partition_edit(partition_id: int):
@bp.route("/partition/<int:partition_id>/delete", methods=["POST"]) @bp.route("/partition/<int:partition_id>/delete", methods=["POST"])
@permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) @api_web_bp.route("/partition/<int:partition_id>/delete", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoEtudChangeGroups)
def partition_delete(partition_id: int): def partition_delete(partition_id: int):
"""Suppression d'une partition (et de tous ses groupes). """Suppression d'une partition (et de tous ses groupes).
@ -412,7 +522,10 @@ def partition_delete(partition_id: int):
Note 2: Si la partition de parcours est supprimée, les étudiants Note 2: Si la partition de parcours est supprimée, les étudiants
sont désinscrits des parcours. sont désinscrits des parcours.
""" """
partition = Partition.query.get_or_404(partition_id) query = Partition.query.filter_by(id=partition_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
partition: Partition = query.first_or_404()
if not partition.partition_name: if not partition.partition_name:
return error_response(404, "ne peut pas supprimer la partition par défaut") return error_response(404, "ne peut pas supprimer la partition par défaut")
is_parcours = partition.is_parcours() is_parcours = partition.is_parcours()

View File

@ -1,7 +1,7 @@
from flask import jsonify from flask import jsonify
from app import db, log from app import db, log
from app.api import bp from app.api import api_bp as bp
from app.api.auth import basic_auth, token_auth from app.auth.logic import basic_auth, token_auth
@bp.route("/tokens", methods=["POST"]) @bp.route("/tokens", methods=["POST"])

View File

@ -26,9 +26,7 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
Return None si étudiant inexistant. Return None si étudiant inexistant.
""" """
allowed_depts = current_user.get_depts_with_permission( allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
Permission.APIView | Permission.ScoView
)
if etudid is not None: if etudid is not None:
etud: Identite = Identite.query.get(etudid) etud: Identite = Identite.query.get(etudid)

87
app/auth/logic.py Normal file
View File

@ -0,0 +1,87 @@
# -*- coding: UTF-8 -*
"""app.auth.logic.py
"""
import http
import flask
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.auth.models import User
basic_auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth()
@basic_auth.verify_password
def verify_password(username, password):
"""Verify password for this user
Appelé lors d'une demande de jeton (normalement via la route /tokens)
"""
user: User = User.query.filter_by(user_name=username).first()
if user and user.check_password(password):
g.current_user = user
# note: est aussi basic_auth.current_user()
return user
@basic_auth.error_handler
def basic_auth_error(status):
"error response (401 for invalid auth.)"
return error_response(status)
@login.user_loader
def load_user(uid: str) -> User:
"flask-login: accès à un utilisateur"
return User.query.get(int(uid))
@token_auth.verify_token
def verify_token(token) -> User:
"""Retrouve l'utilisateur à partir du jeton.
Si la requête n'a pas de jeton, token == "".
"""
user = User.check_token(token) if token else None
if user is not None:
flask_login.login_user(user)
g.current_user = user
return user
@token_auth.error_handler
def token_auth_error(status):
"Réponse en cas d'erreur d'auth."
return error_response(status)
@token_auth.get_user_roles
def get_user_roles(user):
return user.roles
@login.request_loader
def load_user_from_request(req: flask.Request) -> User:
"""Custom Login using Request Loader"""
# Try token
try:
auth_type, token = req.headers["Authorization"].split(None, 1)
except (ValueError, KeyError):
# The Authorization header is either empty or has no token
return None
if auth_type == "Bearer" and token:
return verify_token(token)
return None
@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 redirect(url_for("auth.login"))

View File

@ -11,6 +11,7 @@ from time import time
from typing import Optional from typing import Optional
import cracklib # pylint: disable=import-error import cracklib # pylint: disable=import-error
from flask import current_app, g from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
@ -523,8 +524,3 @@ def get_super_admin():
) )
assert admin_user assert admin_user
return admin_user return admin_user
@login.user_loader
def load_user(uid):
return User.query.get(int(uid))

View File

@ -3,11 +3,8 @@
auth.routes.py auth.routes.py
""" """
from app.scodoc.sco_exceptions import ScoValueError from flask import current_app, flash, render_template
from flask import current_app, g, flash, render_template
from flask import redirect, url_for, request from flask import redirect, url_for, request
from flask_login.utils import login_required
from werkzeug.urls import url_parse
from flask_login import login_user, logout_user, current_user from flask_login import login_user, logout_user, current_user
from app import db from app import db
@ -17,13 +14,11 @@ from app.auth.forms import (
UserCreationForm, UserCreationForm,
ResetPasswordRequestForm, ResetPasswordRequestForm,
ResetPasswordForm, ResetPasswordForm,
DeactivateUserForm,
) )
from app.auth.models import Role from app.auth.models import Role
from app.auth.models import User from app.auth.models import User
from app.auth.email import send_password_reset_email from app.auth.email import send_password_reset_email
from app.decorators import admin_required from app.decorators import admin_required
from app.decorators import permission_required
_ = lambda x: x # sans babel _ = lambda x: x # sans babel
_l = _ _l = _
@ -31,6 +26,7 @@ _l = _
@bp.route("/login", methods=["GET", "POST"]) @bp.route("/login", methods=["GET", "POST"])
def login(): def login():
"ScoDoc Login form"
if current_user.is_authenticated: if current_user.is_authenticated:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
form = LoginForm() form = LoginForm()
@ -42,9 +38,6 @@ def login():
return redirect(url_for("auth.login")) return redirect(url_for("auth.login"))
login_user(user, remember=form.remember_me.data) login_user(user, remember=form.remember_me.data)
current_app.logger.info("login: success (%s)", form.user_name.data) current_app.logger.info("login: success (%s)", form.user_name.data)
# next_page = request.args.get("next")
# if not next_page or url_parse(next_page).netloc != "":
# next_page = url_for("scodoc.index")
return form.redirect("scodoc.index") return form.redirect("scodoc.index")
message = request.args.get("message", "") message = request.args.get("message", "")
return render_template( return render_template(
@ -54,6 +47,7 @@ def login():
@bp.route("/logout") @bp.route("/logout")
def logout(): def logout():
"Logout current user and redirect to home page"
logout_user() logout_user()
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
@ -109,9 +103,10 @@ def reset_password_request():
@bp.route("/reset_password/<token>", methods=["GET", "POST"]) @bp.route("/reset_password/<token>", methods=["GET", "POST"])
def reset_password(token): def reset_password(token):
"Reset passord après demande par mail"
if current_user.is_authenticated: if current_user.is_authenticated:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
user = User.verify_reset_password_token(token) user: User = User.verify_reset_password_token(token)
if user is None: if user is None:
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
form = ResetPasswordForm() form = ResetPasswordForm()
@ -126,6 +121,7 @@ def reset_password(token):
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"]) @bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
@admin_required @admin_required
def reset_standard_roles_permissions(): def reset_standard_roles_permissions():
"Réinitialise (recrée au besoin) les rôles standards de ScoDoc et leurs permissions"
Role.reset_standard_roles_permissions() Role.reset_standard_roles_permissions()
flash("rôles standard réinitialisés !") flash("rôles standards réinitialisés !")
return redirect(url_for("scodoc.configuration")) return redirect(url_for("scodoc.configuration"))

View File

@ -64,6 +64,7 @@ class Formation(db.Model):
def to_dict(self): def to_dict(self):
e = dict(self.__dict__) e = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
e["departement"] = self.departement.to_dict()
# ScoDoc7 output_formators: (backward compat) # ScoDoc7 output_formators: (backward compat)
e["formation_id"] = self.id e["formation_id"] = self.id
return e return e

View File

@ -12,6 +12,7 @@ _SCO_PERMISSIONS = (
# - ZScoDoc: add/delete departments # - ZScoDoc: add/delete departments
# - tous rôles lors creation utilisateurs # - tous rôles lors creation utilisateurs
(1 << 1, "ScoSuperAdmin", "Super Administrateur"), (1 << 1, "ScoSuperAdmin", "Super Administrateur"),
(1 << 2, "APIView", "Voir"), # deprecated
(1 << 2, "ScoView", "Voir"), (1 << 2, "ScoView", "Voir"),
(1 << 3, "ScoEnsView", "Voir les parties pour les enseignants"), (1 << 3, "ScoEnsView", "Voir les parties pour les enseignants"),
(1 << 4, "ScoObservateur", "Observer (accès lecture restreint aux bulletins)"), (1 << 4, "ScoObservateur", "Observer (accès lecture restreint aux bulletins)"),
@ -50,7 +51,7 @@ _SCO_PERMISSIONS = (
(1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"), (1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"),
# 27 à 39 ... réservé pour "entreprises" # 27 à 39 ... réservé pour "entreprises"
# Api scodoc9 # Api scodoc9
(1 << 40, "APIView", "API: Lecture"), # XXX à revoir
(1 << 41, "APIEditGroups", "API: Modifier les groupes"), (1 << 41, "APIEditGroups", "API: Modifier les groupes"),
(1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"), (1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"),
(1 << 43, "APIAbsChange", "API: Saisir des absences"), (1 << 43, "APIAbsChange", "API: Saisir des absences"),

View File

@ -53,7 +53,6 @@ SCO_ROLES_DEFAULTS = {
p.ScoUsersAdmin, p.ScoUsersAdmin,
p.ScoUsersView, p.ScoUsersView,
p.ScoView, p.ScoView,
p.APIView,
), ),
# RespPE est le responsable poursuites d'études # RespPE est le responsable poursuites d'études
# il peut ajouter des tags sur les formations: # il peut ajouter des tags sur les formations:
@ -78,7 +77,7 @@ SCO_ROLES_DEFAULTS = {
p.RelationsEntreprisesCorrespondants, p.RelationsEntreprisesCorrespondants,
), ),
# LecteurAPI peut utiliser l'API en lecture # LecteurAPI peut utiliser l'API en lecture
"LecteurAPI": (p.APIView,), "LecteurAPI": (p.ScoView,),
# Super Admin est un root: création/suppression de départements # Super Admin est un root: création/suppression de départements
# _tous_ les droits # _tous_ les droits
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département # Afin d'avoir tous les droits, il ne doit pas être asscoié à un département

View File

@ -55,9 +55,13 @@ class ScoError(Exception):
pass pass
def GET(path: str, headers={}, errmsg=None): def GET(path: str, headers={}, errmsg=None, dept=None):
"""Get and returns as JSON""" """Get and returns as JSON"""
r = requests.get(API_URL + path, headers=headers or HEADERS, verify=CHK_CERT) if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
else:
url = API_URL + path
r = requests.get(url, headers=headers or HEADERS, verify=CHK_CERT)
if r.status_code != 200: if r.status_code != 200:
raise ScoError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""") raise ScoError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""")
return r.json() # decode la reponse JSON return r.json() # decode la reponse JSON
@ -170,6 +174,11 @@ POST_JSON(f"/group/5559/delete")
POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"}) POST_JSON(f"/group/5327/edit", data={"group_name": "TDXXX"})
# --------- XXX à passer en dans les tests unitaires # --------- XXX à passer en dans les tests unitaires
# 0- Prend un étudiant au hasard dans le semestre
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants")[10]
etudid = etud["id"]
# 1- Crée une partition, puis la change de nom # 1- Crée une partition, puis la change de nom
js = POST_JSON( js = POST_JSON(
f"/formsemestre/{formsemestre_id}/partition/create", f"/formsemestre/{formsemestre_id}/partition/create",
@ -182,21 +191,58 @@ POST_JSON(
) )
# 2- Crée un groupe # 2- Crée un groupe
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "GG"}) js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G1"})
group_id = js["id"] group_1 = js["id"]
# Prend un étudiant au hasard dans le semestre
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants")[10]
etudid = etud["id"]
# 3- Affecte étudiant au groupe
POST_JSON(f"/group/{group_id}/set_etudiant/{etudid}")
# 4- retire du groupe # 3- Crée deux autres groupes
POST_JSON(f"/group/{group_id}/remove_etudiant/{etudid}") js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G2"})
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G3"})
# 5- Suppression # 4- Affecte étudiant au groupe G1
POST_JSON(f"/group/{group_1}/set_etudiant/{etudid}")
# 5- retire du groupe
POST_JSON(f"/group/{group_1}/remove_etudiant/{etudid}")
# 6- affecte au groupe G2
partition = GET(f"/partition/{partition_id}")
assert len(partition["groups"]) == 3
group_2 = [g for g in partition["groups"].values() if g["name"] == "G2"][0]["id"]
POST_JSON(f"/group/{group_2}/set_etudiant/{etudid}")
# 7- Membres du groupe
etuds_g2 = GET(f"/group/{group_2}/etudiants")
assert len(etuds_g2) == 1
assert etuds_g2[0]["id"] == etudid
# 8- Ordres des groupes
group_3 = [g for g in partition["groups"].values() if g["name"] == "G3"][0]["id"]
POST_JSON(
f"/partition/{partition_id}/groups/order",
data=[group_2, group_1, group_3],
)
new_groups = [g["id"] for g in GET(f"/partition/{partition_id}")["groups"].values()]
assert new_groups == [group_2, group_1, group_3]
# 9- Suppression
POST_JSON(f"/partition/{partition_id}/delete") POST_JSON(f"/partition/{partition_id}/delete")
# ------ # ------
# Tests accès API:
"""
* En mode API:
Avec admin:
- GET, POST ci-dessus : OK
Avec user ayant ScoView (rôle LecteurAPI)
- idem
Avec user sans ScoView:
- GET et POST: erreur 403
* En mode Web:
admin: GET
user : GET = 403
"""
# #
POST_JSON( POST_JSON(

View File

@ -3,7 +3,7 @@
"""Test permissions """Test permissions
On a deux utilisateurs dans la base test API: On a deux utilisateurs dans la base test API:
- "test", avec le rôle LecteurAPI qui a APIView, - "test", avec le rôle LecteurAPI qui a la permission ScoView,
- et "other", qui n'a aucune permission. - et "other", qui n'a aucune permission.
@ -23,7 +23,7 @@ from config import RunningConfig
def test_permissions(api_headers): def test_permissions(api_headers):
""" """
vérification de la permissions APIView et du non accès sans role vérification de la permissions ScoView et du non accès sans role
de toutes les routes de l'API de toutes les routes de l'API
""" """
# Ce test va récupérer toutes les routes de l'API # Ce test va récupérer toutes les routes de l'API

View File

@ -101,8 +101,8 @@ def create_users(dept: Departement) -> tuple:
if role is None: if role is None:
print("Erreur: rôle LecteurAPI non existant") print("Erreur: rôle LecteurAPI non existant")
sys.exit(1) sys.exit(1)
perm_api_view = Permission.get_by_name("APIView") perm_sco_view = Permission.get_by_name("ScoView")
role.add_permission(perm_api_view) role.add_permission(perm_sco_view)
db.session.add(role) db.session.add(role)
user.add_role(role, None) user.add_role(role, None)