Utilisation API ScoDoc + exemple template
This commit is contained in:
parent
4f641f24e6
commit
029bb9ac62
23
README.md
23
README.md
@ -1,3 +1,24 @@
|
|||||||
# AutoSco
|
# AutoSco
|
||||||
|
|
||||||
Composant satellite de ScoDoc pour l'auto-inscription des étudiants
|
Composant satellite de ScoDoc pour l'auto-inscription des étudiants
|
||||||
|
|
||||||
|
## Configuration de l'accès à ScoDoc
|
||||||
|
|
||||||
|
Côté ScoDoc, créer un rôle et un utilisateur dédiés:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flask create-role AutoSco
|
||||||
|
flask edit-role AutoSco -a ScoView
|
||||||
|
flask user-create autosco AutoSco @all
|
||||||
|
flask user-password autosco
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurer les paramètres d'accès dans AutoSco: éditer le fichier
|
||||||
|
`/opt/autosco/.env` et indiquer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SCODOC_URL="http://localhost:5000" # l'URL racine de votre ScoDoc
|
||||||
|
SCODOC_LOGIN="autosco"
|
||||||
|
SCODOC_PASSWORD="xxx" # le mot de passe saisi ci-dessus
|
||||||
|
```
|
||||||
|
|
||||||
|
17
app/templates/index.j2
Normal file
17
app/templates/index.j2
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{# Page accueil #}
|
||||||
|
{% extends 'base.j2' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h1>AutoSco - Accueil</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{% for sem in sems %}
|
||||||
|
<div class="sem">
|
||||||
|
{{sem.date_debut}} : {{sem.titre}}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -2,9 +2,36 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
import version
|
import version
|
||||||
|
|
||||||
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
# le répertoire static, lié à chaque release pour éviter les problèmes de caches
|
||||||
STATIC_DIR = (
|
STATIC_DIR = (
|
||||||
os.environ.get("SCRIPT_NAME", "") + "/AutoSco/static/links/" + version.VERSION
|
os.environ.get("SCRIPT_NAME", "") + "/AutoSco/static/links/" + version.VERSION
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Dates et années scolaires
|
||||||
|
# Ces dates "pivot" sont paramétrables dans les préférences générales
|
||||||
|
# on donne ici les valeurs par défaut.
|
||||||
|
# Les semestres commençant à partir du 1er août 20XX sont
|
||||||
|
# dans l'année scolaire 20XX
|
||||||
|
MONTH_DEBUT_ANNEE_SCOLAIRE = 8 # août
|
||||||
|
|
||||||
|
|
||||||
|
def annee_scolaire() -> int:
|
||||||
|
"""Année de debut de l'annee scolaire courante"""
|
||||||
|
t = time.localtime()
|
||||||
|
year, month = t[0], t[1]
|
||||||
|
return annee_scolaire_debut(year, month)
|
||||||
|
|
||||||
|
|
||||||
|
def annee_scolaire_debut(year, month) -> int:
|
||||||
|
"""Annee scolaire de début.
|
||||||
|
Par défaut (hémisphère nord), l'année du mois de août
|
||||||
|
précédent la date indiquée.
|
||||||
|
"""
|
||||||
|
if int(month) >= MONTH_DEBUT_ANNEE_SCOLAIRE:
|
||||||
|
return int(year)
|
||||||
|
else:
|
||||||
|
return int(year) - 1
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
"""AutoSco / views
|
"""AutoSco / views
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from flask import render_template
|
||||||
|
from app.utils import utils as scu
|
||||||
from app.views import bp
|
from app.views import bp
|
||||||
|
from scodoc import api
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/")
|
@bp.route("/")
|
||||||
def index():
|
def index():
|
||||||
return "hello"
|
annee_scolaire = scu.annee_scolaire()
|
||||||
|
sems = api.get(f"/formsemestres/query?annee_scolaire={annee_scolaire}")
|
||||||
|
# nb: l'utilisaton de l'API départementale permet de n'avoir
|
||||||
|
# que les semestres du département configuré.
|
||||||
|
|
||||||
|
return render_template("index.j2", sems=sems)
|
||||||
|
14
config.py
14
config.py
@ -33,6 +33,20 @@ class Config:
|
|||||||
JSON_ADD_STATUS = False
|
JSON_ADD_STATUS = False
|
||||||
JSON_USE_ENCODE_METHODS = True
|
JSON_USE_ENCODE_METHODS = True
|
||||||
JSON_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z" # "%Y-%m-%dT%H:%M:%S"
|
JSON_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z" # "%Y-%m-%dT%H:%M:%S"
|
||||||
|
# --- Lien avec API ScoDoc
|
||||||
|
SCODOC_URL = os.environ.get("SCODOC_URL", "http://localhost:5000")
|
||||||
|
SCODOC_LOGIN = os.environ.get("SCODOC_LOGIN", "autosco")
|
||||||
|
SCODOC_PASSWORD = os.environ.get("SCODOC_PASSWORD")
|
||||||
|
SCODOC_CHECK_CERTIFICATE = os.environ.get("SCODOC_CHECK_CERTIFICATE", True)
|
||||||
|
API_TIMEOUT = 120 # 2 minutes
|
||||||
|
API_URL = SCODOC_URL + "/ScoDoc/api"
|
||||||
|
SCODOC_DEPT_ACRONYM = "ESPL"
|
||||||
|
|
||||||
|
def __getitem__(self, k) -> str | int | None:
|
||||||
|
return getattr(self, k)
|
||||||
|
|
||||||
|
def get(self, k, default=None):
|
||||||
|
return getattr(self, k, default=default)
|
||||||
|
|
||||||
|
|
||||||
class ProdConfig(Config):
|
class ProdConfig(Config):
|
||||||
|
1
scodoc/__init__.py
Normal file
1
scodoc/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""AutoSco / Lien avec ScoDoc"""
|
126
scodoc/api.py
Normal file
126
scodoc/api.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
"""AutoSco: ScoDoc API usage
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from flask import current_app
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_headers(user: str, password: str, conf: config.Config) -> dict:
|
||||||
|
"Demande de jeton, dict à utiliser dans les en-têtes de requêtes http"
|
||||||
|
ans = requests.post(
|
||||||
|
conf["API_URL"] + "/tokens",
|
||||||
|
auth=(user, password),
|
||||||
|
timeout=conf["API_TIMEOUT"],
|
||||||
|
)
|
||||||
|
if ans.status_code != 200:
|
||||||
|
raise APIError(f"Echec demande jeton par {user}", status_code=ans.status_code)
|
||||||
|
token = ans.json()["token"]
|
||||||
|
return {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
def __init__(self, message: str = "", payload=None, status_code=None):
|
||||||
|
self.message = message
|
||||||
|
self.payload = payload or {}
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"APIError: {self.message} payload={self.payload} status_code={self.status_code}"
|
||||||
|
|
||||||
|
|
||||||
|
class APIAccessor:
|
||||||
|
"Gestion bas niveau des accès à l'API ScoDoc"
|
||||||
|
|
||||||
|
def __init__(self, conf: config.Config, dept_acronym: str | None = None):
|
||||||
|
self.config = conf
|
||||||
|
self.dept_acronym = dept_acronym
|
||||||
|
"si spécifié, utilisera API départementale"
|
||||||
|
user = self.config["SCODOC_LOGIN"]
|
||||||
|
password = self.config["SCODOC_PASSWORD"]
|
||||||
|
self.headers = get_auth_headers(user, password, self.config)
|
||||||
|
|
||||||
|
def get(self, path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
|
||||||
|
"""Get.
|
||||||
|
If raw, returns a a requests.Response
|
||||||
|
Else retrns decoded json or, if other content, requests.Response
|
||||||
|
Special case for non json result (image or pdf):
|
||||||
|
return Content-Disposition string (inline or attachment)
|
||||||
|
If raw, return
|
||||||
|
"""
|
||||||
|
dept = dept or self.dept_acronym
|
||||||
|
if dept:
|
||||||
|
url = self.config["SCODOC_URL"] + f"/ScoDoc/{dept}/api" + path
|
||||||
|
else:
|
||||||
|
url = self.config["API_URL"] + path
|
||||||
|
reply = requests.get(
|
||||||
|
url,
|
||||||
|
headers=self.headers if headers is None else headers,
|
||||||
|
verify=self.config["SCODOC_CHECK_CERTIFICATE"],
|
||||||
|
timeout=self.config["API_TIMEOUT"],
|
||||||
|
)
|
||||||
|
if reply.status_code != 200:
|
||||||
|
print("url", url)
|
||||||
|
print("reply", reply.text)
|
||||||
|
try:
|
||||||
|
payload = r.json()
|
||||||
|
except requests.exceptions.JSONDecodeError:
|
||||||
|
payload = r.text
|
||||||
|
raise APIError(
|
||||||
|
errmsg or f"""erreur get {url} !""",
|
||||||
|
payload,
|
||||||
|
status_code=reply.status_code,
|
||||||
|
)
|
||||||
|
if (not raw) and reply.headers.get("Content-Type", None) == "application/json":
|
||||||
|
return reply.json() # decode la reponse JSON
|
||||||
|
return reply
|
||||||
|
|
||||||
|
def post(
|
||||||
|
self,
|
||||||
|
path: str,
|
||||||
|
data: dict = None,
|
||||||
|
headers: dict = None,
|
||||||
|
errmsg=None,
|
||||||
|
dept=None,
|
||||||
|
raw=False,
|
||||||
|
):
|
||||||
|
"""Post
|
||||||
|
Decode réponse en json, sauf si raw.
|
||||||
|
"""
|
||||||
|
data = data or {}
|
||||||
|
dept = dept or self.dept_acronym
|
||||||
|
if dept:
|
||||||
|
url = self.config["SCODOC_URL"] + f"/ScoDoc/{dept}/api" + path
|
||||||
|
else:
|
||||||
|
url = self.config["API_URL"] + path
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
json=data,
|
||||||
|
headers=self.headers if headers is None else headers,
|
||||||
|
verify=self.config["SCODOC_CHECK_CERTIFICATE"],
|
||||||
|
timeout=self.config["API_TIMEOUT"],
|
||||||
|
)
|
||||||
|
if r.status_code != 200:
|
||||||
|
try:
|
||||||
|
payload = r.json()
|
||||||
|
except requests.exceptions.JSONDecodeError:
|
||||||
|
payload = r.text
|
||||||
|
raise APIError(
|
||||||
|
errmsg or f"erreur url={url} status={r.status_code} !",
|
||||||
|
payload=payload,
|
||||||
|
status_code=r.status_code,
|
||||||
|
)
|
||||||
|
if (not raw) and reply.headers.get("Content-Type", None) == "application/json":
|
||||||
|
return r.json() # decode la reponse JSON
|
||||||
|
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
def get(path, conf: config.Config = None):
|
||||||
|
"""Connect to ScoDoc and get.
|
||||||
|
Utilise département configuré dans la config.
|
||||||
|
"""
|
||||||
|
conf = conf or (current_app.config if current_app else config.RunningConfig())
|
||||||
|
apicnx = APIAccessor(conf, dept_acronym=conf["SCODOC_DEPT_ACRONYM"])
|
||||||
|
return apicnx.get(path)
|
Loading…
Reference in New Issue
Block a user