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.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"

View File

@ -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__)

View File

@ -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)

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
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é

View File

@ -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)

View File

@ -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)

View File

@ -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())

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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"])

View File

@ -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
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
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))

View File

@ -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"))

View File

@ -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

View File

@ -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"),

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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)