# Développement ScoDoc: Introduction

## Composants logiciels

- le code est écrit en Python 3.11.
- le code doit être formatté par [black](https://pypi.org/project/black/) qui
  est normalement intégré à votre éditeur (VSCode et PyCharm sont deux choix
  judicieux).
- outre Python, les principaux composants logiciels sont:
  - [Flask](https://flask-sqlalchemy.palletsprojects.com/en/2.x/): le
    framework Web, dont on utilise notamment:
        - l'ORM [SQLAlchemy](https://www.sqlalchemy.org/)
        - les templates [Jinja2](https://jinja.palletsprojects.com/en/3.0.x/)
  - [Postgresql](https://www.postgresql.org/)
  - [Redis](https://redis.io/) cache persistant
  - [NGINX](https://www.nginx.com/) serveur Web frontal
  - [gunicorn](https://gunicorn.org/) 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, notamment `g.scodoc_dept` (l'acronyme du département courant) et
 `g.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 classe `Permission`.

```py
@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](ScoDoc9API.md).

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:

```py
@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.

```bash
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

```bash
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://scodoc.org/git/ScoDoc/ScoDoc/src/branch/master/app/api/formsemestres.py#L91>):

```py
@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:

```bash
  git pull
  flask clear-cache
```

La commande `redis-cli FLUSHALL` permet aussi de vider le cache sans avoir à
lancer flask (plus rapide).

!!! note "Voir aussi"

    - [Conventions de codage](DevConventions.md)
    - [Guide développeurs](GuideDeveloppeurs.md)
    - [API ScoDoc 9](ScoDoc9API.md)
    - [Modélisation du BUT](ModelisationParcoursBUT.md)
    - [Contacts](Contact.md)
