From 029bb9ac62d9d35c4ce58c3b946cd19e1c831922 Mon Sep 17 00:00:00 2001 From: Ilona Date: Sun, 1 Sep 2024 23:08:25 +0200 Subject: [PATCH] Utilisation API ScoDoc + exemple template --- README.md | 23 +++++++- app/templates/index.j2 | 17 ++++++ app/utils/utils.py | 27 +++++++++ app/views/views.py | 10 +++- config.py | 14 +++++ scodoc/__init__.py | 1 + scodoc/api.py | 126 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 app/templates/index.j2 create mode 100644 scodoc/__init__.py create mode 100644 scodoc/api.py diff --git a/README.md b/README.md index f3e6293..c4af8a0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,24 @@ # AutoSco -Composant satellite de ScoDoc pour l'auto-inscription des étudiants \ No newline at end of file +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 +``` + diff --git a/app/templates/index.j2 b/app/templates/index.j2 new file mode 100644 index 0000000..10c7d4a --- /dev/null +++ b/app/templates/index.j2 @@ -0,0 +1,17 @@ +{# Page accueil #} +{% extends 'base.j2' %} + +{% block content %} + +

AutoSco - Accueil

+ +
+{% for sem in sems %} +
+ {{sem.date_debut}} : {{sem.titre}} +
+{% endfor %} +
+ +{% endblock %} + diff --git a/app/utils/utils.py b/app/utils/utils.py index d0ed8fd..c71c7e4 100644 --- a/app/utils/utils.py +++ b/app/utils/utils.py @@ -2,9 +2,36 @@ """ import os +import time import version # le répertoire static, lié à chaque release pour éviter les problèmes de caches STATIC_DIR = ( 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 diff --git a/app/views/views.py b/app/views/views.py index 3a51732..6c7ab87 100644 --- a/app/views/views.py +++ b/app/views/views.py @@ -1,9 +1,17 @@ """AutoSco / views """ +from flask import render_template +from app.utils import utils as scu from app.views import bp +from scodoc import api @bp.route("/") 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) diff --git a/config.py b/config.py index 10b2b85..a718315 100644 --- a/config.py +++ b/config.py @@ -33,6 +33,20 @@ class Config: JSON_ADD_STATUS = False JSON_USE_ENCODE_METHODS = True 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): diff --git a/scodoc/__init__.py b/scodoc/__init__.py new file mode 100644 index 0000000..4b81c3c --- /dev/null +++ b/scodoc/__init__.py @@ -0,0 +1 @@ +"""AutoSco / Lien avec ScoDoc""" diff --git a/scodoc/api.py b/scodoc/api.py new file mode 100644 index 0000000..23ae7b3 --- /dev/null +++ b/scodoc/api.py @@ -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)