1
0
forked from ScoDoc/ScoDoc

API scodoc7, exemple/test usage, progres sur l'API scodoc9

This commit is contained in:
Emmanuel Viennet 2021-10-28 00:52:23 +02:00
parent db937ca7c5
commit d2f41b6a21
11 changed files with 327 additions and 146 deletions

View File

@ -6,3 +6,4 @@ from flask import Blueprint
bp = Blueprint("api", __name__) bp = Blueprint("api", __name__)
from app.api import sco_api from app.api import sco_api
from app.api import tokens

View File

@ -33,6 +33,7 @@ token_auth = HTTPTokenAuth()
@basic_auth.verify_password @basic_auth.verify_password
def verify_password(username, password): def verify_password(username, password):
# breakpoint()
user = User.query.filter_by(user_name=username).first() user = User.query.filter_by(user_name=username).first()
if user and user.check_password(password): if user and user.check_password(password):
return user return user
@ -51,3 +52,17 @@ def verify_token(token):
@token_auth.error_handler @token_auth.error_handler
def token_auth_error(status): def token_auth_error(status):
return error_response(status) return error_response(status)
def token_permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
scodoc_dept = getattr(g, "scodoc_dept", None)
if not current_user.has_permission(permission, scodoc_dept):
abort(403)
return f(*args, **kwargs)
return login_required(decorated_function)
return decorator

View File

@ -48,9 +48,9 @@ from app.api.errors import bad_request
from app import models from app import models
@bp.route("/ScoDoc/api/list_depts", methods=["GET"]) @bp.route("list_depts", methods=["GET"])
@token_auth.login_required @token_auth.login_required
def list_depts(): def list_depts():
depts = models.Departement.query.filter_by(visible=True).all() depts = models.Departement.query.filter_by(visible=True).all()
data = {"items": [d.to_dict() for d in depts]} data = [d.to_dict() for d in depts]
return jsonify(data) return jsonify(data)

View File

@ -213,6 +213,9 @@ class User(UserMixin, db.Model):
@staticmethod @staticmethod
def check_token(token): def check_token(token):
"""Retreive user for given token, chek token's validity
and returns the user object.
"""
user = User.query.filter_by(token=token).first() user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow(): if user is None or user.token_expiration < datetime.utcnow():
return None return None

View File

@ -50,6 +50,7 @@ def scodoc(func):
@wraps(func) @wraps(func)
def scodoc_function(*args, **kwargs): def scodoc_function(*args, **kwargs):
# current_app.logger.info("@scodoc")
# interdit les POST si pas loggué # interdit les POST si pas loggué
if request.method == "POST" and not current_user.is_authenticated: if request.method == "POST" and not current_user.is_authenticated:
current_app.logger.info( current_app.logger.info(
@ -71,6 +72,7 @@ def scodoc(func):
# current_app.logger.info("setting dept to None") # current_app.logger.info("setting dept to None")
g.scodoc_dept = None g.scodoc_dept = None
g.scodoc_dept_id = -1 # invalide g.scodoc_dept_id = -1 # invalide
return func(*args, **kwargs) return func(*args, **kwargs)
return scodoc_function return scodoc_function
@ -100,8 +102,8 @@ def permission_required_compat_scodoc7(permission):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
# current_app.logger.warning("PERMISSION; kwargs=%s" % str(kwargs))
# cherche les paramètre d'auth: # cherche les paramètre d'auth:
# current_app.logger.info("@permission_required_compat_scodoc7")
auth_ok = False auth_ok = False
if request.method == "GET": if request.method == "GET":
user_name = request.args.get("__ac_name") user_name = request.args.get("__ac_name")
@ -116,7 +118,6 @@ def permission_required_compat_scodoc7(permission):
if u and u.check_password(user_password): if u and u.check_password(user_password):
auth_ok = True auth_ok = True
flask_login.login_user(u) flask_login.login_user(u)
# reprend le chemin classique: # reprend le chemin classique:
scodoc_dept = getattr(g, "scodoc_dept", None) scodoc_dept = getattr(g, "scodoc_dept", None)
@ -153,6 +154,7 @@ def scodoc7func(func):
2. or be called directly from Python. 2. or be called directly from Python.
""" """
# current_app.logger.info("@scodoc7func")
# Détermine si on est appelé via une route ("toplevel") # Détermine si on est appelé via une route ("toplevel")
# ou par un appel de fonction python normal. # ou par un appel de fonction python normal.
top_level = not hasattr(g, "scodoc7_decorated") top_level = not hasattr(g, "scodoc7_decorated")

View File

@ -1047,8 +1047,8 @@ def EtatAbsencesDate(group_ids=[], date=None): # list of groups to display
# ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail)
@bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat @bp.route("/AddBilletAbsence", methods=["GET", "POST"]) # API ScoDoc 7 compat
@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoAbsAddBillet)
@scodoc7func @scodoc7func
def AddBilletAbsence( def AddBilletAbsence(
begin, begin,
@ -1238,8 +1238,8 @@ def listeBilletsEtud(etudid=False, format="html"):
@bp.route( @bp.route(
"/XMLgetBilletsEtud", methods=["GET", "POST"] "/XMLgetBilletsEtud", methods=["GET", "POST"]
) # pour compat anciens clients PHP ) # pour compat anciens clients PHP
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def XMLgetBilletsEtud(etudid=False): def XMLgetBilletsEtud(etudid=False):
"""Liste billets pour un etudiant""" """Liste billets pour un etudiant"""
@ -1464,8 +1464,8 @@ def ProcessBilletAbsenceForm(billet_id):
# @bp.route("/essai_api7") # @bp.route("/essai_api7")
# @permission_required_compat_scodoc7(Permission.ScoView)
# @scodoc # @scodoc
# @permission_required_compat_scodoc7(Permission.ScoView)
# @scodoc7func # @scodoc7func
# def essai_api7(x="xxx"): # def essai_api7(x="xxx"):
# "un essai" # "un essai"
@ -1474,8 +1474,8 @@ def ProcessBilletAbsenceForm(billet_id):
@bp.route("/XMLgetAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP @bp.route("/XMLgetAbsEtud", methods=["GET", "POST"]) # pour compat anciens clients PHP
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def XMLgetAbsEtud(beg_date="", end_date=""): def XMLgetAbsEtud(beg_date="", end_date=""):
"""returns list of absences in date interval""" """returns list of absences in date interval"""

View File

@ -264,10 +264,10 @@ sco_publish(
@bp.route( @bp.route(
"formsemestre_bulletinetud", methods=["GET", "POST"] "/formsemestre_bulletinetud", methods=["GET", "POST"]
) # POST pour compat anciens clients PHP (deprecated) ) # POST pour compat anciens clients PHP (deprecated)
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def formsemestre_bulletinetud( def formsemestre_bulletinetud(
etudid=None, etudid=None,
@ -642,8 +642,8 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.ScoChangeFormatio
@bp.route( @bp.route(
"/formsemestre_list", methods=["GET", "POST"] "/formsemestre_list", methods=["GET", "POST"]
) # pour compat anciens clients PHP ) # pour compat anciens clients PHP
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def formsemestre_list( def formsemestre_list(
format="json", format="json",
@ -669,8 +669,8 @@ def formsemestre_list(
@bp.route( @bp.route(
"/XMLgetFormsemestres", methods=["GET", "POST"] "/XMLgetFormsemestres", methods=["GET", "POST"]
) # pour compat anciens clients PHP ) # pour compat anciens clients PHP
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None): def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None):
"""List all formsemestres matching etape, XML format """List all formsemestres matching etape, XML format

View File

@ -358,8 +358,8 @@ def search_etud_by_name():
@bp.route( @bp.route(
"/Notes/XMLgetEtudInfos", methods=["GET", "POST"] "/Notes/XMLgetEtudInfos", methods=["GET", "POST"]
) # pour compat anciens clients PHP ) # pour compat anciens clients PHP
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def etud_info(etudid=None, format="xml"): def etud_info(etudid=None, format="xml"):
"Donne les informations sur un etudiant" "Donne les informations sur un etudiant"

View File

@ -1,133 +0,0 @@
#!/usr/bin/env python3
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Exemple connexion sur ScoDoc et utilisation de l'API
- Ouverture session
- Liste semestres
- Liste modules
- Creation d'une évaluation
- Saisie d'une note
Attention: cet exemple est en Python 3 (>= 3.6)
"""
import requests
import urllib3
import pdb
from pprint import pprint as pp
from flask import g, url_for
# A modifier pour votre serveur:
CHECK_CERTIFICATE = False # set to True in production
BASEURL = "https://scodoc.xxx.net/ScoDoc/RT/Scolarite"
USER = "XXX"
PASSWORD = "XXX"
# ---
if not CHECK_CERTIFICATE:
urllib3.disable_warnings()
class ScoError(Exception):
pass
def GET(s, path, errmsg=None):
"""Get and returns as JSON"""
r = s.get(BASEURL + "/" + path, verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.json() # decode la reponse JSON
def POST(s, path, data, errmsg=None):
"""Post"""
r = s.post(BASEURL + "/" + path, data=data, verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.text
# --- Ouverture session (login)
s = requests.Session()
s.post(
"https://deb11.viennet.net/api/auth/login",
data={"user_name": USER, "password": PASSWORD},
)
r = s.get(BASEURL, auth=(USER, PASSWORD), verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise ScoError("erreur de connection: vérifier adresse et identifiants")
# --- Recupere la liste de tous les semestres:
sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")
# sems est une liste de semestres (dictionnaires)
for sem in sems:
if sem["etat"]:
break
if sem["etat"] == "0":
raise ScoError("Aucun semestre non verrouillé !")
# Affiche le semestre trouvé:
pp(sem)
# ---- Récupère la description de ce semestre:
# semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" )
# ---- Liste les modules et prend le premier
mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}")
print(f"{len(mods)} modules dans le semestre {sem['titre']}")
mod = mods[0]
# ---- Etudiants inscrits dans ce module
inscrits = GET(
s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}"
)
print(f"{len(inscrits)} inscrits dans ce module")
# prend le premier inscrit, au hasard:
etudid = inscrits[0]["etudid"]
# ---- Création d'une evaluation le dernier jour du semestre
jour = sem["date_fin"]
evaluation_id = POST(
s,
"/Notes/do_evaluation_create",
data={
"moduleimpl_id": mod["moduleimpl_id"],
"coefficient": 1,
"jour": jour, # "5/9/2019",
"heure_debut": "9h00",
"heure_fin": "10h00",
"note_max": 20, # notes sur 20
"description": "essai",
},
errmsg="échec création évaluation",
)
print(
f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
)
print(
"Pour vérifier, aller sur: ",
url_for(
"notes.moduleimpl_status",
scodoc_dept="DEPT",
moduleimpl_id=mod["moduleimpl_id"],
),
)
# ---- Saisie d'une note
junk = POST(
s,
"/Notes/save_note",
data={
"etudid": etudid,
"evaluation_id": evaluation_id,
"value": 16.66, # la note !
"comment": "test API",
},
)

View File

@ -0,0 +1,149 @@
#!/usr/bin/env python3
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Exemple utilisation API ScoDoc 9 avec jeton obtenu par basic athentication
Utilisation: créer les variables d'environnement: (indiquer les valeurs
pour le serveur ScoDoc que vous voulez interroger)
export SCODOC_URL="https://scodoc.xxx.net/"
export SCODOC_USER="xxx"
export SCODOC_PASSWD="xxx"
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
Travail en cours, un seul point d'API (list_depts).
"""
from dotenv import load_dotenv
import os
import pdb
import requests
import urllib3
from pprint import pprint as pp
# --- Lecture configuration (variables d'env ou .env)
BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env"))
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"]
SCODOC_DEPT = os.environ["SCODOC_DEPT"]
DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite/"
SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"]
print(f"SCODOC_URL={SCODOC_URL}")
# ---
if not CHECK_CERTIFICATE:
urllib3.disable_warnings()
class ScoError(Exception):
pass
def GET(path: str, headers={}, errmsg=None):
"""Get and returns as JSON"""
r = requests.get(
DEPT_URL + "/" + path, headers=headers or HEADERS, verify=CHECK_CERTIFICATE
)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.json() # decode la reponse JSON
def POST(s, path: str, data: dict, errmsg=None):
"""Post"""
r = s.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.text
# --- Obtention du jeton (token)
r = requests.post(
SCODOC_URL + "/ScoDoc/api/tokens", auth=(SCODOC_USER, SCODOC_PASSWORD)
)
assert r.status_code == 200
token = r.json()["token"]
HEADERS = {"Authorization": f"Bearer {token}"}
r = requests.get(
SCODOC_URL + "/ScoDoc/api/list_depts", headers=HEADERS, verify=CHECK_CERTIFICATE
)
if r.status_code != 200:
raise ScoError("erreur de connexion: vérifier adresse et identifiants")
pp(r.json())
# # --- Recupere la liste de tous les semestres:
# sems = GET(s, "Notes/formsemestre_list?format=json", "Aucun semestre !")
# # sems est une liste de semestres (dictionnaires)
# for sem in sems:
# if sem["etat"]:
# break
# if sem["etat"] == "0":
# raise ScoError("Aucun semestre non verrouillé !")
# # Affiche le semestre trouvé:
# pp(sem)
# # ---- Récupère la description de ce semestre:
# # semdescr = GET(s, f"Notes/formsemestre_description?formsemestre_id={sem['formsemestre_id']}&with_evals=0&format=json" )
# # ---- Liste les modules et prend le premier
# mods = GET(s, f"/Notes/moduleimpl_list?formsemestre_id={sem['formsemestre_id']}")
# print(f"{len(mods)} modules dans le semestre {sem['titre']}")
# mod = mods[0]
# # ---- Etudiants inscrits dans ce module
# inscrits = GET(
# s, f"Notes/do_moduleimpl_inscription_list?moduleimpl_id={mod['moduleimpl_id']}"
# )
# print(f"{len(inscrits)} inscrits dans ce module")
# # prend le premier inscrit, au hasard:
# etudid = inscrits[0]["etudid"]
# # ---- Création d'une evaluation le dernier jour du semestre
# jour = sem["date_fin"]
# evaluation_id = POST(
# s,
# "/Notes/do_evaluation_create",
# data={
# "moduleimpl_id": mod["moduleimpl_id"],
# "coefficient": 1,
# "jour": jour, # "5/9/2019",
# "heure_debut": "9h00",
# "heure_fin": "10h00",
# "note_max": 20, # notes sur 20
# "description": "essai",
# },
# errmsg="échec création évaluation",
# )
# print(
# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
# )
# print(
# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}",
# )
# # ---- Saisie d'une note
# junk = POST(
# s,
# "/Notes/save_note",
# data={
# "etudid": etudid,
# "evaluation_id": evaluation_id,
# "value": 16.66, # la note !
# "comment": "test API",
# },
# )

View File

@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Exemple connexion sur ScoDoc 9 et utilisation de l'ancienne API ScoDoc 7
à la mode "PHP": les gens passaient directement __ac_name et __ac_password
dans chaque requête, en POST ou en GET.
Cela n'a jamais été documenté mais était implitement supporté. C'est "deprecated"
et ne sera plus supporté à partir de juillet 2022.
Ce script va tester:
- Liste semestres
- Liste modules
- Creation d'une évaluation
- Saisie d'une note
Utilisation: créer les variables d'environnement: (indiquer les valeurs
pour le serveur ScoDoc que vous voulez interroger)
export SCODOC_URL="https://scodoc.xxx.net/"
export SCODOC_USER="xxx"
export SCODOC_PASSWD="xxx"
export CHECK_CERTIFICATE=0 # ou 1 si serveur de production avec certif SSL valide
(on peut aussi placer ces valeurs dans un fichier .env du répertoire tests/api).
"""
from dotenv import load_dotenv
import os
import pdb
import requests
import urllib3
from pprint import pprint as pp
# --- Lecture configuration (variables d'env ou .env)
BASEDIR = os.path.abspath(os.path.dirname(__file__))
load_dotenv(os.path.join(BASEDIR, ".env"))
CHECK_CERTIFICATE = bool(int(os.environ.get("CHECK_CERTIFICATE", False)))
SCODOC_URL = os.environ["SCODOC_URL"]
SCODOC_DEPT = os.environ["SCODOC_DEPT"]
DEPT_URL = SCODOC_URL + "/ScoDoc/" + SCODOC_DEPT + "/Scolarite"
SCODOC_USER = os.environ["SCODOC_USER"]
SCODOC_PASSWORD = os.environ["SCODOC_PASSWD"]
print(f"SCODOC_URL={SCODOC_URL}")
# ---
if not CHECK_CERTIFICATE:
urllib3.disable_warnings()
class ScoError(Exception):
pass
def GET(path: str, params=None, errmsg=None):
"""Get and returns as JSON"""
# ajoute auth
params["__ac_name"] = SCODOC_USER
params["__ac_password"] = SCODOC_PASSWORD
r = requests.get(DEPT_URL + "/" + path, params=params, verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.json() # decode la reponse JSON
def POST(path: str, data: dict, errmsg=None):
"""Post"""
data["__ac_name"] = SCODOC_USER
data["__ac_password"] = SCODOC_PASSWORD
r = requests.post(DEPT_URL + "/" + path, data=data, verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise ScoError(errmsg or "erreur !")
return r.text
# ---
# pas besoin d'ouvrir une session, on y va directement:
# --- Recupere la liste de tous les semestres:
sems = GET("Notes/formsemestre_list", params={"format": "json"})
# sems est une liste de semestres (dictionnaires)
for sem in sems:
if sem["etat"]:
break
if sem["etat"] == "0":
raise ScoError("Aucun semestre non verrouillé !")
# Affiche le semestre trouvé:
pp(sem)
# Les fonctions ci-dessous ne fonctionne plus en ScoDoc 9
# Voir https://scodoc.org/git/viennet/ScoDoc/issues/149
# # ---- Liste les modules et prend le premier
# mods = GET("/Notes/moduleimpl_list", params={"formsemestre_id": sem["formsemestre_id"]})
# print(f"{len(mods)} modules dans le semestre {sem['titre']}")
# mod = mods[0]
# # ---- Etudiants inscrits dans ce module
# inscrits = GET(
# "Notes/do_moduleimpl_inscription_list",
# params={"moduleimpl_id": mod["moduleimpl_id"]},
# )
# print(f"{len(inscrits)} inscrits dans ce module")
# # prend le premier inscrit, au hasard:
# etudid = inscrits[0]["etudid"]
# # ---- Création d'une evaluation le dernier jour du semestre
# jour = sem["date_fin"]
# evaluation_id = POST(
# "/Notes/do_evaluation_create",
# data={
# "moduleimpl_id": mod["moduleimpl_id"],
# "coefficient": 1,
# "jour": jour, # "5/9/2019",
# "heure_debut": "9h00",
# "heure_fin": "10h00",
# "note_max": 20, # notes sur 20
# "description": "essai",
# },
# errmsg="échec création évaluation",
# )
# print(
# f"Evaluation créée dans le module {mod['moduleimpl_id']}, evaluation_id={evaluation_id}"
# )
# print(
# f"Pour vérifier, aller sur: {DEPT_URL}/Notes/moduleimpl_status?moduleimpl_id={mod['moduleimpl_id']}",
# )
# # ---- Saisie d'une note
# junk = POST(
# "/Notes/save_note",
# data={
# "etudid": etudid,
# "evaluation_id": evaluation_id,
# "value": 16.66, # la note !
# "comment": "test API",
# },
# )