Développement ScoDoc: Introduction¶
Composants logiciels¶
- le code est écrit en Python 3.11.
- le code doit être formatté par black qui est normalement intégré à votre éditeur (VSCode et PyCharm sont deux choix judicieux).
- outre Python, les principaux composants logiciels sont:
- Flask: le framework Web, dont on utilise notamment: - l'ORM SQLAlchemy - les templates Jinja2
- Postgresql
- Redis cache persistant
- NGINX serveur Web frontal
- gunicorn WSGI HTTP server
- et bien sûr Linux (Debian 12 en 2023-2024) et systemd.
Principaux objets¶
Les objets manipulés par ScoDoc sont pour la plupart stockés en base postgres et accédé soit directement en SQL (anciennes parties de ScoDoc), soit à travers l'ORM SQLAlchemy (recommandé pour tout nouveau code).
Les modèles correspondant sont déclarés dans /opt/scodoc/app/models/
.
Principales classes (les noms des classes Python sont en CamelCase
).
- Étudiants (classe
Identite
): nom, codes INE/NIP, etc - Formations: programmes pédagogiques, contenant
- Unités d'Enseignement (
UniteEns
); - Matières et Modules (
Module
, avec son type standard, bonus, ressources ou SAÉ). - FormSemestre: instanciation d'une session de formation, avec un programme pédagogique donné (Formation), les dates de début et fin, des étudiants inscrits, des responsables, divers codes, et les ModuleImpl mis en œuvre.
- ModuleImpl: la mise en place d'un module pédagogique (le ModuleImpl est au Module ce que le FormSemestre est à la Formation): lié à un module, avec un enseignant responsable et des étudiants inscrits.
- Inscriptions: tables d'association avec codes et/ou état (démission, défaillant): FormsemestreInscription ModuleImplInscription.
Vues et décorateurs¶
Une vue ordinaire (Web) pourrait ressembler à cela. Noter la présence de décorateurs:
@scodoc
récupère le département (présent dans l'URL) et initialise quelques trucs, notammentg.scodoc_dept
(l'acronyme du département courant) etg.scodoc_dept_id
(l'id du dépt. courant).@permission_required
: permet de contrôler l'accès, en se basant sur les permissions définies dans la classePermission
.
@bp.route("/un_exemple")
@scodoc
@permission_required(Permission.EditFormation)
def un_exemple():
# Récupérer le ou les arguments: exemple avec formation_id
formation_id = int(request.args["formation_id"])
# Charger le ou les objets utilies:
formation = models.Formation.query.get(
formation_id=formation_id
).first_or_404()
# Effectuer au besoin un traitement
resultat = ...
# Afficher le résultat
return render_template(
"exemple_template.html",
resultat=resultat, # par exemple
formation=formation,
... # etc
)
Vues de l'API et permissions¶
L'API REST est documentée ici : ScoDoc9API.
Les fonctions de l'API sont donc accessibles via les routes de la forme
https://scodoc.monsite.tld/ScoDoc/api/fonction
et aussi https://scodoc.monsite.tld/ScoDoc/api/<dept_acronyme>/fonction
.
La seconde forme précise un département.
La seconde forme est notamment utilisée par les pages web de ScoDoc. Elle permet
un calcul des permissions liées à un département: l'idée est de donner accès à
l'API à un utilisateur qui n'ait pas la permission (par ex. ScoView
) dans tous les
départements, ce qui est en général le cas des utilisateurs Web, mais est aussi
utile pour sécuriser certains usages de l'API.
Une vue API (avec accès via token API et/ou cookie Web) se déclare donc ainsi:
@bp.route("/formsemestres/query")
@api_web_bp.route("/formsemestres/query")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestres_query():
...
Son usage par un utilisateur n'ayant accès qu'à un seul département passera par
la route départementale
http://scodoc.monsite.tld:5000/ScoDoc/<dept_acronyme>/api/formsemestres/query
.
Exemple complet d'usage¶
Création du rôle, de l'utilisateur, association à un département, requêtage.
flask create-role LecteurAPI2 # 2 car LecteurAPi était déjà pris sur mon serveur de test
flask edit-role -a ScoView LecteurAPI2
flask user-create lecteur_rt LecteurAPI2 RT # Seulement dans dept RT
flask user-password lecteur_rt
puis
http -a lecteur_rt:mot_de_passe POST 'http://localhost:5000/ScoDoc/api/tokens'
# récupérer le token...
http GET http://localhost:5000/ScoDoc/api/RT/formsemestres/query "Authorization:Bearer xxxxxxxxxxx"
# -> réponse ok
http GET http://localhost:5000/ScoDoc/api/formsemestres/query "Authorization:Bearer xxxxxxxxxxx"
# -> 401, "Non autorise (logic)"
Côté programmation serveur¶
Reprenons le même exemple (voir `app/api/formsemestres.py`` ligne 91, https://git.scodoc.org/ScoDoc/ScoDoc/src/branch/master/app/api/formsemestres.py#L91):
@bp.route("/formsemestres/query")
@api_web_bp.route("/formsemestres/query")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def formsemestres_query():
...
formsemestres = FormSemestre.query
if g.scodoc_dept:
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
...
En effet, g.scodoc_dept
et g.scodoc_dept_id
sont positionnés par le
décorateur si on a un appel via la route départementale.
Il est donc important, pour toutes les vues API, de prendre soin de ne pas
divulguer d'informations hors du département spécifié, en filtrant la ou les
requêtes si g.scodoc_dept
est non None
.
Caches¶
Il est bon de savoir que les requêtes SQL de SQLAlchemy ne sont pas cachées: ni la requête elle même (construction du SQL à partir des appels à l'ORM), ni son résultat.
Le module sco_cache.py
offre la possibilité de cacher des objets python
identifiés par un id unique dans le cache Redis. Ce cache est persistant, il
faut donc invalider les objets quand on écrit des données susceptibles de les
modifier, et penser à le vider quand on modifie le code.
Par exemple:
git pull
flask clear-cache
La commande redis-cli FLUSHALL
permet aussi de vider le cache sans avoir à
lancer flask (plus rapide).