forked from ScoDoc/DocScoDoc
Modification contrôle d'accès. Routes API basic+token. Revision routes API.
This commit is contained in:
parent
c9a6fe0743
commit
dcd7cf78fd
@ -41,6 +41,7 @@ migrate = Migrate(compare_type=True)
|
||||
login = LoginManager()
|
||||
login.login_view = "auth.login"
|
||||
login.login_message = "Identifiez-vous pour accéder à cette page."
|
||||
|
||||
mail = Mail()
|
||||
bootstrap = Bootstrap()
|
||||
moment = Moment()
|
||||
@ -249,8 +250,8 @@ def create_app(config_class=DevConfig):
|
||||
from app.views import notes_bp
|
||||
from app.views import users_bp
|
||||
from app.views import absences_bp
|
||||
from app.api import bp as api_bp
|
||||
from app.api import api_web_bp as api_web_bp
|
||||
from app.api import api_bp
|
||||
from app.api import api_web_bp
|
||||
|
||||
# https://scodoc.fr/ScoDoc
|
||||
app.register_blueprint(scodoc_bp)
|
||||
@ -265,7 +266,7 @@ def create_app(config_class=DevConfig):
|
||||
absences_bp, url_prefix="/ScoDoc/<scodoc_dept>/Scolarite/Absences"
|
||||
)
|
||||
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(
|
||||
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
|
||||
|
@ -4,7 +4,7 @@
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
bp = Blueprint("api", __name__)
|
||||
api_bp = Blueprint("api", __name__)
|
||||
api_web_bp = Blueprint("apiweb", __name__)
|
||||
|
||||
|
||||
|
@ -8,9 +8,9 @@
|
||||
|
||||
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.auth import permission_required_api
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Identite
|
||||
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def absences(etudid: int = None):
|
||||
"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def absences_just(etudid: int = None):
|
||||
"""
|
||||
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>",
|
||||
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):
|
||||
"""
|
||||
Liste des absences d'un groupe (possibilité de choisir entre deux dates)
|
||||
|
148
app/api/auth.py
148
app/api/auth.py
@ -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))
|
||||
)
|
@ -4,8 +4,8 @@ from flask import jsonify
|
||||
|
||||
import app
|
||||
from app import models
|
||||
from app.api import bp
|
||||
from app.api.auth import permission_required_api
|
||||
from app.api import api_bp as bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, FormSemestre
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
@ -22,21 +22,24 @@ def get_departement(dept_ident: str) -> Departement:
|
||||
|
||||
|
||||
@bp.route("/departements", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def departements():
|
||||
"""Liste les départements"""
|
||||
return jsonify([dept.to_dict() for dept in Departement.query])
|
||||
|
||||
|
||||
@bp.route("/departements_ids", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def departements_ids():
|
||||
"""Liste des ids de départements"""
|
||||
return jsonify([dept.id for dept in Departement.query])
|
||||
|
||||
|
||||
@bp.route("/departement/<string:acronym>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def departement(acronym: str):
|
||||
"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def departement_by_id(dept_id: int):
|
||||
"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def dept_etudiants(acronym: str):
|
||||
"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def dept_etudiants_by_id(dept_id: int):
|
||||
"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def dept_formsemestres_ids(acronym: str):
|
||||
"""liste des ids formsemestre du département"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def dept_formsemestres_ids_by_id(dept_id: int):
|
||||
"""liste des ids formsemestre du département"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def dept_formsemestres_courants(acronym: str):
|
||||
"""
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def dept_formsemestres_courants_by_id(dept_id: int):
|
||||
"""
|
||||
Liste des semestres actifs d'un département d'id donné
|
||||
|
@ -10,41 +10,46 @@
|
||||
|
||||
from flask import g, jsonify
|
||||
from flask_login import current_user
|
||||
from flask_login import login_required
|
||||
from sqlalchemy import or_
|
||||
|
||||
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.auth import permission_required_api, api_publish, web_publish
|
||||
from app.api import tools
|
||||
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement, FormSemestreInscription, FormSemestre, Identite
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
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):
|
||||
"""Une fonction quelconque de l'API"""
|
||||
# u = current_user
|
||||
# dept = g.scodoc_dept # peut être None si accès API
|
||||
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)
|
||||
return jsonify(
|
||||
{"current_user": current_user.to_dict(), "arg": arg, "dept": g.scodoc_dept}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/etudiants/courants", defaults={"long": False})
|
||||
@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):
|
||||
"""
|
||||
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.)
|
||||
dans lesquels l'utilisateur a le rôle APIView (donc tous si le dept du
|
||||
rôle est None).
|
||||
dans lesquels l'utilisateur a la permission ScoView
|
||||
(donc tous si le dept du rôle est None).
|
||||
|
||||
Exemple de résultat :
|
||||
[
|
||||
@ -89,9 +94,7 @@ def etudiants_courants(long=False):
|
||||
"villedomicile": "VALPARAISO",
|
||||
}
|
||||
"""
|
||||
allowed_depts = current_user.get_depts_with_permission(
|
||||
Permission.APIView | Permission.ScoView
|
||||
)
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
etuds = Identite.query.filter(
|
||||
Identite.id == FormSemestreInscription.etudid,
|
||||
FormSemestreInscription.formsemestre_id == FormSemestre.id,
|
||||
@ -110,10 +113,15 @@ def etudiants_courants(long=False):
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>", methods=["GET"])
|
||||
@bp.route("/etudiant/nip/<string:nip>", methods=["GET"])
|
||||
@bp.route("/etudiant/ine/<string:ine>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/etudiant/etudid/<int:etudid>")
|
||||
@bp.route("/etudiant/nip/<string:nip>")
|
||||
@bp.route("/etudiant/ine/<string:ine>")
|
||||
@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):
|
||||
"""
|
||||
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/nip/<string:nip>", 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):
|
||||
"""
|
||||
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
|
||||
été inscrit dans plusieurs départements, on a plusieurs objets (1 par dept.).
|
||||
"""
|
||||
allowed_depts = current_user.get_depts_with_permission(
|
||||
Permission.APIView | Permission.ScoView
|
||||
)
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
if etudid is not None:
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
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/nip/<string:nip>/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):
|
||||
"""
|
||||
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
|
||||
(si l'étudiant a changé de département). L'id du département est `dept_id`.
|
||||
Accès par etudid, nip ou ine.
|
||||
|
||||
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 :
|
||||
[
|
||||
@ -265,6 +281,9 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
||||
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)
|
||||
|
||||
return jsonify(
|
||||
@ -287,22 +306,12 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
||||
methods=["GET"],
|
||||
defaults={"version": "long", "pdf": False},
|
||||
)
|
||||
# Version PDF non fonctionnelle
|
||||
# Version PDF non testée
|
||||
@bp.route(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/pdf",
|
||||
methods=["GET"],
|
||||
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(
|
||||
"/etudiant/etudid/<int:etudid>/formsemestre/<int:formsemestre_id>/bulletin/short",
|
||||
methods=["GET"],
|
||||
@ -333,8 +342,60 @@ def etudiant_formsemestres(etudid: int = None, nip: int = None, ine: int = None)
|
||||
methods=["GET"],
|
||||
defaults={"version": "short", "pdf": True},
|
||||
)
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
def etudiant_bulletin_semestre( # XXX TODO Ajouter la possibilité de retourner en version pdf
|
||||
@api_web_bp.route(
|
||||
"/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,
|
||||
etudid: int = 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()
|
||||
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:
|
||||
query = Identite.query.filter_by(id=etudid)
|
||||
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",
|
||||
methods=["GET"],
|
||||
)
|
||||
@bp.route(
|
||||
"/etudiant/nip/<string:nip>/formsemestre/<int:formsemestre_id>/groups",
|
||||
methods=["GET"],
|
||||
)
|
||||
@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
|
||||
):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
"""
|
||||
Retourne la liste des groupes auxquels appartient l'étudiant dans le formsemestre indiqué
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
etudid : l'etudid d'un étudiant
|
||||
nip : le code nip d'un étudiant
|
||||
ine : le code ine d'un étudiant
|
||||
|
||||
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:
|
||||
return error_response(
|
||||
404,
|
||||
message="formsemestre inconnu",
|
||||
)
|
||||
dept = Departement.query.get(formsemestre.dept_id)
|
||||
if etudid is not None:
|
||||
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",
|
||||
)
|
||||
dept = formsemestre.departement
|
||||
etud = Identite.query.filter_by(id=etudid, dept_id=dept.id).first_or_404(etudid)
|
||||
|
||||
app.set_sco_dept(dept.acronym)
|
||||
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
||||
|
||||
|
@ -8,21 +8,24 @@
|
||||
ScoDoc 9 API : accès aux évaluations
|
||||
"""
|
||||
|
||||
from flask import jsonify
|
||||
from flask import g, jsonify
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
|
||||
from app import models
|
||||
from app.api import bp
|
||||
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.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_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/evaluations/<int:moduleimpl_id>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/evaluations/<int:moduleimpl_id>")
|
||||
@api_web_bp.route("/evaluations/<int:moduleimpl_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations d'un moduleimpl
|
||||
@ -54,17 +57,21 @@ def evaluations(moduleimpl_id: int):
|
||||
...
|
||||
]
|
||||
"""
|
||||
# Récupération de toutes les évaluations
|
||||
evals = Evaluation.query.filter_by(id=moduleimpl_id)
|
||||
|
||||
# Mise en forme des données
|
||||
data = [d.to_dict() for d in evals]
|
||||
|
||||
return jsonify(data)
|
||||
query = Evaluation.query.filter_by(id=moduleimpl_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
return jsonify([d.to_dict() for d in query])
|
||||
|
||||
|
||||
@bp.route("/evaluation/eval_notes/<int:evaluation_id>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
@api_web_bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def evaluation_notes(evaluation_id: int):
|
||||
"""
|
||||
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
|
||||
app.set_sco_dept(dept.acronym)
|
||||
|
||||
|
@ -8,43 +8,58 @@
|
||||
ScoDoc 9 API : accès aux formations
|
||||
"""
|
||||
|
||||
from flask import jsonify
|
||||
from flask import g, jsonify
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import models
|
||||
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.auth import permission_required_api
|
||||
from app.decorators import scodoc, permission_required
|
||||
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.sco_permissions import Permission
|
||||
|
||||
|
||||
@bp.route("/formations", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formations")
|
||||
@api_web_bp.route("/formations")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formations():
|
||||
"""
|
||||
Retourne la liste de toutes les formations (tous départements)
|
||||
|
||||
"""
|
||||
data = [d.to_dict() for d in models.Formation.query]
|
||||
return jsonify(data)
|
||||
query = Formation.query
|
||||
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"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formations_ids")
|
||||
@api_web_bp.route("/formations_ids")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formations_ids():
|
||||
"""
|
||||
Retourne la liste de toutes les id de formations (tous départements)
|
||||
|
||||
Exemple de résultat : [ 17, 99, 32 ]
|
||||
"""
|
||||
data = [d.id for d in models.Formation.query]
|
||||
return jsonify(data)
|
||||
query = Formation.query
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
return jsonify([d.id for d in query])
|
||||
|
||||
|
||||
@bp.route("/formation/<int:formation_id>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formation/<int:formation_id>")
|
||||
@api_web_bp.route("/formation/<int:formation_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formation_by_id(formation_id: int):
|
||||
"""
|
||||
La formation d'id donné
|
||||
@ -66,21 +81,31 @@ def formation_by_id(formation_id: int):
|
||||
"formation_id": 1
|
||||
}
|
||||
"""
|
||||
formation = models.Formation.query.get_or_404(formation_id)
|
||||
return jsonify(formation.to_dict())
|
||||
query = Formation.query.filter_by(id=formation_id)
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
return jsonify(query.first_or_404().to_dict())
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formation/formation_export/<int:formation_id>",
|
||||
methods=["GET"],
|
||||
"/formation/<int:formation_id>/export",
|
||||
defaults={"export_ids": False},
|
||||
)
|
||||
@bp.route(
|
||||
"/formation/formation_export/<int:formation_id>/with_ids",
|
||||
methods=["GET"],
|
||||
"/formation/<int:formation_id>/export/with_ids",
|
||||
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):
|
||||
"""
|
||||
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)
|
||||
try:
|
||||
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)
|
||||
|
||||
|
||||
@bp.route("/formation/moduleimpl/<int:moduleimpl_id>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formation/<int:formation_id>/referentiel_competences")
|
||||
@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):
|
||||
"""
|
||||
Retourne un module moduleimpl en fonction de son id
|
||||
Retourne un moduleimpl en fonction de son id
|
||||
|
||||
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())
|
||||
|
||||
|
||||
@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())
|
||||
|
@ -7,17 +7,23 @@
|
||||
"""
|
||||
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
|
||||
from app import models
|
||||
from app.api import bp
|
||||
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.comp import res_sem
|
||||
from app.comp.moy_mod import ModuleImplResults
|
||||
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 import sco_groups
|
||||
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
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_infos(formsemestre_id: int):
|
||||
"""
|
||||
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())
|
||||
|
||||
|
||||
@bp.route("/formsemestres/query", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestres/query")
|
||||
@api_web_bp.route("/formsemestres/query")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestres_query():
|
||||
"""
|
||||
Retourne les formsemestres filtrés par
|
||||
@ -85,6 +100,8 @@ def formsemestres_query():
|
||||
dept_acronym = request.args.get("dept_acronym")
|
||||
dept_id = request.args.get("dept_id")
|
||||
formsemestres = FormSemestre.query
|
||||
if g.scodoc_dept:
|
||||
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||
if etape_apo is not None:
|
||||
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
||||
FormSemestreEtape.etape_apo == etape_apo
|
||||
@ -100,9 +117,7 @@ def formsemestres_query():
|
||||
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
||||
)
|
||||
if dept_acronym is not None:
|
||||
formsemestres = formsemestres.join(models.Departement).filter_by(
|
||||
acronym=dept_acronym
|
||||
)
|
||||
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
|
||||
if dept_id is not None:
|
||||
try:
|
||||
dept_id = int(dept_id)
|
||||
@ -113,8 +128,11 @@ def formsemestres_query():
|
||||
return jsonify([formsemestre.to_dict_api() for formsemestre in formsemestres])
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def bulletins(formsemestre_id: int):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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)
|
||||
|
||||
data = []
|
||||
@ -134,11 +155,11 @@ def bulletins(formsemestre_id: int):
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/programme",
|
||||
methods=["GET"],
|
||||
)
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/programme")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/programme")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_programme(formsemestre_id: int):
|
||||
"""
|
||||
Retourne la liste des Ues, ressources et SAE d'un semestre
|
||||
@ -204,7 +225,10 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
"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()
|
||||
m_list = {
|
||||
ModuleType.RESSOURCE: [],
|
||||
@ -226,29 +250,41 @@ def formsemestre_programme(formsemestre_id: int):
|
||||
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/etudiants",
|
||||
methods=["GET"],
|
||||
defaults={"etat": scu.INSCRIT},
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/etudiants/demissionnaires",
|
||||
methods=["GET"],
|
||||
defaults={"etat": scu.DEMISSION},
|
||||
)
|
||||
@bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/etudiants/defaillants",
|
||||
methods=["GET"],
|
||||
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):
|
||||
"""
|
||||
Retourne la liste des étudiants d'un formsemestre
|
||||
|
||||
formsemestre_id : l'id d'un formsemestre
|
||||
"""
|
||||
formsemestre: FormSemestre = models.FormSemestre.query.filter_by(
|
||||
id=formsemestre_id
|
||||
).first_or_404()
|
||||
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)
|
||||
|
||||
inscriptions = [ins for ins in formsemestre.inscriptions if ins.etat == etat]
|
||||
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)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etat_evals(formsemestre_id: int):
|
||||
"""
|
||||
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)
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
@ -364,8 +406,11 @@ def etat_evals(formsemestre_id: int):
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/resultats", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_resultat(formsemestre_id: int):
|
||||
"""Tableau récapitulatif des résultats
|
||||
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
|
||||
@ -375,7 +420,10 @@ def formsemestre_resultat(formsemestre_id: int):
|
||||
return error_response(404, "invalid format specification")
|
||||
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)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
rows, footer_rows, titles, column_ids = res.get_table_recap(
|
||||
|
@ -2,9 +2,8 @@
|
||||
# from flask import jsonify
|
||||
|
||||
# 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.auth import permission_required_api
|
||||
# from app.scodoc.sco_prepajury import feuille_preparation_jury
|
||||
# from app.scodoc.sco_pvjury import formsemestre_pvjury
|
||||
|
||||
|
@ -31,19 +31,22 @@ Contrib @jmp
|
||||
|
||||
from datetime import datetime
|
||||
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.auth import token_auth
|
||||
from app.api.errors import error_response
|
||||
from app.models import Departement
|
||||
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
|
||||
|
||||
# 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():
|
||||
if not g.current_user.has_permission(Permission.ScoSuperAdmin, None):
|
||||
return error_response(401, message="accès interdit")
|
||||
@ -54,8 +57,9 @@ def api_get_glob_logos():
|
||||
return jsonify(list(logos.keys()))
|
||||
|
||||
|
||||
@bp.route("/logos/<string:logoname>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/logos/<string:logoname>")
|
||||
@scodoc
|
||||
@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")
|
||||
@ -70,8 +74,9 @@ def api_get_glob_logo(logoname):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/departements/<string:departement>/logos", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/departements/<string:departement>/logos")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def api_get_local_logos(departement):
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
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()))
|
||||
|
||||
|
||||
@bp.route("/departements/<string:departement>/logos/<string:logoname>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/departements/<string:departement>/logos/<string:logoname>")
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def api_get_local_logo(departement, logoname):
|
||||
# format = requested_format("jpg", ['png', 'jpg']) XXX ?
|
||||
dept_id = Departement.from_acronym(departement).id
|
||||
|
@ -7,12 +7,14 @@
|
||||
"""
|
||||
ScoDoc 9 API : partitions
|
||||
"""
|
||||
from flask import jsonify, request
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app import db, log
|
||||
from app.api import bp
|
||||
from app.api.auth import permission_required_api
|
||||
from app import 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.models import FormSemestre, FormSemestreInscription, Identite
|
||||
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
|
||||
|
||||
|
||||
@bp.route("/partition/<int:partition_id>", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/partition/<int:partition_id>")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def partition_info(partition_id: int):
|
||||
"""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))
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/partitions", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/partitions")
|
||||
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/partitions")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def formsemestre_partitions(formsemestre_id: int):
|
||||
"""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)
|
||||
return jsonify(
|
||||
{
|
||||
@ -81,8 +94,11 @@ def formsemestre_partitions(formsemestre_id: int):
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/etudiants", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/group/<int:group_id>/etudiants")
|
||||
@api_web_bp.route("/group/<int:group_id>/etudiants")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def etud_in_group(group_id: int):
|
||||
"""
|
||||
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])
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/etudiants/query", methods=["GET"])
|
||||
@permission_required_api(Permission.ScoView, Permission.APIView)
|
||||
@bp.route("/group/<int:group_id>/etudiants/query")
|
||||
@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):
|
||||
"""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")
|
||||
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 = (
|
||||
Identite.query.join(FormSemestreInscription)
|
||||
.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"])
|
||||
@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):
|
||||
"""Affecte l'étudiant au groupe indiqué"""
|
||||
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}:
|
||||
return error_response(404, "etud non inscrit au formsemestre du groupe")
|
||||
groups = (
|
||||
@ -139,11 +178,11 @@ def set_etud_group(etudid: int, group_id: int):
|
||||
.filter_by(etudid=etudid)
|
||||
)
|
||||
ok = False
|
||||
for g in groups:
|
||||
if g.id == group_id:
|
||||
for group in groups:
|
||||
if group.id == group_id:
|
||||
ok = True
|
||||
else:
|
||||
g.etuds.remove(etud)
|
||||
group.etuds.remove(etud)
|
||||
if not ok:
|
||||
group.etuds.append(etud)
|
||||
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"])
|
||||
@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):
|
||||
"""Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien."""
|
||||
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)
|
||||
db.session.commit()
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
@ -167,20 +216,28 @@ def group_remove_etud(group_id: int, etudid: int):
|
||||
@bp.route(
|
||||
"/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):
|
||||
"""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)
|
||||
"""
|
||||
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 = (
|
||||
GroupDescr.query.filter_by(partition_id=partition_id)
|
||||
.join(group_membership)
|
||||
.filter_by(etudid=etudid)
|
||||
)
|
||||
for g in groups:
|
||||
g.etuds.remove(etud)
|
||||
for group in groups:
|
||||
group.etuds.remove(etud)
|
||||
db.session.commit()
|
||||
app.set_sco_dept(partition.formsemestre.departement.acronym)
|
||||
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"])
|
||||
@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):
|
||||
"""Création d'un groupe dans une partition
|
||||
|
||||
@ -197,7 +257,10 @@ def group_create(partition_id: int):
|
||||
"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:
|
||||
return error_response(404, "partition non editable")
|
||||
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"])
|
||||
@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):
|
||||
"""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:
|
||||
return error_response(404, "partition non editable")
|
||||
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"])
|
||||
@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):
|
||||
"""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:
|
||||
return error_response(404, "partition non editable")
|
||||
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"])
|
||||
@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):
|
||||
"""Création d'une partition dans un semestre
|
||||
|
||||
@ -268,7 +352,10 @@ def partition_create(formsemestre_id: int):
|
||||
"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
|
||||
partition_name = data.get("partition_name")
|
||||
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"])
|
||||
@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):
|
||||
"""Modifie l'ordre des partitions du formsemestre
|
||||
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
|
||||
if not isinstance(partition_ids, int) and not all(
|
||||
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"])
|
||||
@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):
|
||||
"""Modifie l'ordre des groupes de la partition
|
||||
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
|
||||
if not isinstance(group_ids, int) and not all(
|
||||
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"])
|
||||
@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):
|
||||
"""Modification d'une partition dans un semestre
|
||||
|
||||
@ -365,7 +469,10 @@ def partition_edit(partition_id: int):
|
||||
"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
|
||||
modified = False
|
||||
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"])
|
||||
@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):
|
||||
"""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
|
||||
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:
|
||||
return error_response(404, "ne peut pas supprimer la partition par défaut")
|
||||
is_parcours = partition.is_parcours()
|
||||
|
@ -1,7 +1,7 @@
|
||||
from flask import jsonify
|
||||
from app import db, log
|
||||
from app.api import bp
|
||||
from app.api.auth import basic_auth, token_auth
|
||||
from app.api import api_bp as bp
|
||||
from app.auth.logic import basic_auth, token_auth
|
||||
|
||||
|
||||
@bp.route("/tokens", methods=["POST"])
|
||||
|
@ -26,9 +26,7 @@ def get_etud(etudid=None, nip=None, ine=None) -> models.Identite:
|
||||
|
||||
Return None si étudiant inexistant.
|
||||
"""
|
||||
allowed_depts = current_user.get_depts_with_permission(
|
||||
Permission.APIView | Permission.ScoView
|
||||
)
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.ScoView)
|
||||
|
||||
if etudid is not None:
|
||||
etud: Identite = Identite.query.get(etudid)
|
||||
|
87
app/auth/logic.py
Normal file
87
app/auth/logic.py
Normal 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"))
|
@ -11,6 +11,7 @@ from time import time
|
||||
from typing import Optional
|
||||
|
||||
import cracklib # pylint: disable=import-error
|
||||
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
|
||||
@ -523,8 +524,3 @@ def get_super_admin():
|
||||
)
|
||||
assert admin_user
|
||||
return admin_user
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(uid):
|
||||
return User.query.get(int(uid))
|
||||
|
@ -3,11 +3,8 @@
|
||||
auth.routes.py
|
||||
"""
|
||||
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from flask import current_app, g, flash, render_template
|
||||
from flask import current_app, flash, render_template
|
||||
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 app import db
|
||||
@ -17,13 +14,11 @@ from app.auth.forms import (
|
||||
UserCreationForm,
|
||||
ResetPasswordRequestForm,
|
||||
ResetPasswordForm,
|
||||
DeactivateUserForm,
|
||||
)
|
||||
from app.auth.models import Role
|
||||
from app.auth.models import User
|
||||
from app.auth.email import send_password_reset_email
|
||||
from app.decorators import admin_required
|
||||
from app.decorators import permission_required
|
||||
|
||||
_ = lambda x: x # sans babel
|
||||
_l = _
|
||||
@ -31,6 +26,7 @@ _l = _
|
||||
|
||||
@bp.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
"ScoDoc Login form"
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for("scodoc.index"))
|
||||
form = LoginForm()
|
||||
@ -42,9 +38,6 @@ def login():
|
||||
return redirect(url_for("auth.login"))
|
||||
login_user(user, remember=form.remember_me.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")
|
||||
message = request.args.get("message", "")
|
||||
return render_template(
|
||||
@ -54,6 +47,7 @@ def login():
|
||||
|
||||
@bp.route("/logout")
|
||||
def logout():
|
||||
"Logout current user and redirect to home page"
|
||||
logout_user()
|
||||
return redirect(url_for("scodoc.index"))
|
||||
|
||||
@ -109,9 +103,10 @@ def reset_password_request():
|
||||
|
||||
@bp.route("/reset_password/<token>", methods=["GET", "POST"])
|
||||
def reset_password(token):
|
||||
"Reset passord après demande par mail"
|
||||
if current_user.is_authenticated:
|
||||
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:
|
||||
return redirect(url_for("scodoc.index"))
|
||||
form = ResetPasswordForm()
|
||||
@ -126,6 +121,7 @@ def reset_password(token):
|
||||
@bp.route("/reset_standard_roles_permissions", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
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()
|
||||
flash("rôles standard réinitialisés !")
|
||||
flash("rôles standards réinitialisés !")
|
||||
return redirect(url_for("scodoc.configuration"))
|
||||
|
@ -64,6 +64,7 @@ class Formation(db.Model):
|
||||
def to_dict(self):
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
e["departement"] = self.departement.to_dict()
|
||||
# ScoDoc7 output_formators: (backward compat)
|
||||
e["formation_id"] = self.id
|
||||
return e
|
||||
|
@ -12,6 +12,7 @@ _SCO_PERMISSIONS = (
|
||||
# - ZScoDoc: add/delete departments
|
||||
# - tous rôles lors creation utilisateurs
|
||||
(1 << 1, "ScoSuperAdmin", "Super Administrateur"),
|
||||
(1 << 2, "APIView", "Voir"), # deprecated
|
||||
(1 << 2, "ScoView", "Voir"),
|
||||
(1 << 3, "ScoEnsView", "Voir les parties pour les enseignants"),
|
||||
(1 << 4, "ScoObservateur", "Observer (accès lecture restreint aux bulletins)"),
|
||||
@ -50,7 +51,7 @@ _SCO_PERMISSIONS = (
|
||||
(1 << 27, "RelationsEntreprisesCorrespondants", "Voir les correspondants"),
|
||||
# 27 à 39 ... réservé pour "entreprises"
|
||||
# Api scodoc9
|
||||
(1 << 40, "APIView", "API: Lecture"),
|
||||
# XXX à revoir
|
||||
(1 << 41, "APIEditGroups", "API: Modifier les groupes"),
|
||||
(1 << 42, "APIEditAllNotes", "API: Modifier toutes les notes"),
|
||||
(1 << 43, "APIAbsChange", "API: Saisir des absences"),
|
||||
|
@ -53,7 +53,6 @@ SCO_ROLES_DEFAULTS = {
|
||||
p.ScoUsersAdmin,
|
||||
p.ScoUsersView,
|
||||
p.ScoView,
|
||||
p.APIView,
|
||||
),
|
||||
# RespPE est le responsable poursuites d'études
|
||||
# il peut ajouter des tags sur les formations:
|
||||
@ -78,7 +77,7 @@ SCO_ROLES_DEFAULTS = {
|
||||
p.RelationsEntreprisesCorrespondants,
|
||||
),
|
||||
# LecteurAPI peut utiliser l'API en lecture
|
||||
"LecteurAPI": (p.APIView,),
|
||||
"LecteurAPI": (p.ScoView,),
|
||||
# Super Admin est un root: création/suppression de départements
|
||||
# _tous_ les droits
|
||||
# Afin d'avoir tous les droits, il ne doit pas être asscoié à un département
|
||||
|
@ -55,9 +55,13 @@ class ScoError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def GET(path: str, headers={}, errmsg=None):
|
||||
def GET(path: str, headers={}, errmsg=None, dept=None):
|
||||
"""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:
|
||||
raise ScoError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""")
|
||||
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"})
|
||||
|
||||
# --------- 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
|
||||
js = POST_JSON(
|
||||
f"/formsemestre/{formsemestre_id}/partition/create",
|
||||
@ -182,21 +191,58 @@ POST_JSON(
|
||||
)
|
||||
|
||||
# 2- Crée un groupe
|
||||
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "GG"})
|
||||
group_id = 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}")
|
||||
js = POST_JSON(f"/partition/{partition_id}/group/create", data={"group_name": "G1"})
|
||||
group_1 = js["id"]
|
||||
|
||||
# 4- retire du groupe
|
||||
POST_JSON(f"/group/{group_id}/remove_etudiant/{etudid}")
|
||||
# 3- Crée deux autres groupes
|
||||
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")
|
||||
# ------
|
||||
|
||||
# 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(
|
||||
|
@ -3,7 +3,7 @@
|
||||
"""Test permissions
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ from config import RunningConfig
|
||||
|
||||
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
|
||||
"""
|
||||
# Ce test va récupérer toutes les routes de l'API
|
||||
|
@ -101,8 +101,8 @@ def create_users(dept: Departement) -> tuple:
|
||||
if role is None:
|
||||
print("Erreur: rôle LecteurAPI non existant")
|
||||
sys.exit(1)
|
||||
perm_api_view = Permission.get_by_name("APIView")
|
||||
role.add_permission(perm_api_view)
|
||||
perm_sco_view = Permission.get_by_name("ScoView")
|
||||
role.add_permission(perm_sco_view)
|
||||
db.session.add(role)
|
||||
|
||||
user.add_role(role, None)
|
||||
|
Loading…
Reference in New Issue
Block a user