DocScoDoc/docs/DevConventions.md

273 lines
7.5 KiB
Markdown

# Conventions de codage pour ScoDoc
Le projet étant très ancien, le code est passé par différentes conventions et
frameworks. Le style de python et celui du _frontend_ web ont considérablement
évolués ces dernières décennies.
Pour les nouveaux codes, respecter les principes suivants:
- ScoDoc est avant tout une application serveur écrite en Python, qui offre des
services via une interface Web, une API, et accessoirement la ligne de
commande unix.
- Les pages Web générées doivent l'être en Python côté serveur. On peut utiliser
du JS pour les pages dynamiques complexes (eg éditeur de partition) mais
éviter d'utiliser du JS pour générer des éléments statiques: l'abus de JS
conduit à dupliquer du code (qui existe en général dans le serveur) et
augmente les coûts de maintenance.
## Conventions de codage
### Formatage du code
- l'usage de **[black](https://black.readthedocs.io/en/stable/)** est obligatoire
- l'usage de pylint et mypy est fortement recommandé
- Pour le code JS, indentation avec 2 espaces (sous VS Code, utiliser _Prettier_).
### Conventions standard Python
On se tient à la PEP8, c'est à dire en résumé:
`UneClasse`, `une_fonction`, `_une_fonction_privee`, `une_variable`, `UNE_CONSTANTE_GLOBALE`.
Les noms de fichiers sont en minuscules sans accents ni espaces, `comme_ceci.py`.
### Annotations de type (typehints)
On annote les paramètres et résultats des fonctions.
```py
def _upload_justificatif_files(
just: Justificatif, form: AjoutJustificatifEtudForm
) -> bool:
...
```
## Pages et templates
Les vues utilisent un template _Jinja2_, fichier avec extension j2.
On s'astreint, sauf cas particulier, à un nommage identique de la route, la vue et du template.
```py
@bp.route("/un_exemple")
def un_exemple(...):
...
return render_template(".../un_exemple.j2")
```
Le template aura typiquement avec la structure suivante:
```jinja
{% extends "sco_page.j2" %}
{% block app_content %}
<h1>Titre</h1>
{% endblock app_content %}
```
Pour les pages dans un semestre (avec barre menu etc.), on utilisera `<h2>` au
lieu de `<h1>`.
### Templates habituels
Les templates suivants sont les plus utilisés pour les pages:
- `base.j2` pages hors département (accueil, configuration, ...)
- `sco_page.j2` page standard dans un formsemestre: avec sidebar et barre de
menu du semestre. Le contenu de la variable `content` est placé comme HTML
brut (_safe_) dans la partie contenu. Passer `sco=ScoData(formsemestre=formsemestre)`
en argument au template.
- `sco_page_dept.j2` page dans un département (avec sidebar) mais sans formsemestre (pas de
barre de menu)
### Constantes définies dans les templates ScoDoc
Le `context_processor` `inject_sco_utils()` ajoute des objets pour accès à tous
les templates ScoDoc:
- `scu` : le module `sco_utils.py` (diverses fonctions utilitaires et variables
de config.)
- `sco` : des données sur le contexte, notamment les préférences, le
formsemestre et l'étudiant courants (resp. `sco.formsemestre` et `sco.etud`)
### Appel d'un template page
Pour mémoire, l'appel d'un template dans une vue se fait ainsi:
```py
render_template( "votre_template.j2",
title="Changer de référentiel de compétences",
autre_variable=...
)
```
### Passage des anciennes pages ScoDoc 7 aux templates Jinja
Les anciennes pages étaient générées en python selon la structure:
```py
html_sco_header.sco_header(
cssstyles=["css/....css"],
javascripts=[
"js/....js",
],
page_title="titre de la page",
)
... contenu ...
html_sco_header.sco_footer()
```
La migration la plus simple consiste à utilise `sco_page.j2`:
```py
render_template(
"sco_page.j2",
content=contenu,
title="titre de la page",
cssstyles=["..."],
javascripts=["..."],
)
```
Mais si on souhaite générer le contenu dans un template, cela prendra la forme:
```jinja-html
{% extends "sco_page.j2" %}
{% block title %}
...titre...
{% endblock title %}
{% block styles %}
{{ super() }}
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/un_style_scodoc.css">
<style>
... styles locaux ...
</style>
{% endblock styles %}
{% block app_content %}
<div class="pageContent">
<h2>titre page</h2>
... contenu ...
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script src="{{scu.STATIC_DIR}}/js/un_script_scodoc.js"></script>
<script>
... script js local ...
</script>
{% endblock %}
```
### Classes css
`scodoc.css` (et à partir de 9.7, `scodoc97.css`) définissent quelques classes à usage général:
- `div.scobox`: boite (bords arrondis, fond uni) regroupant des éléments.
- `div.scobox-title` le titre de la boite
- `div.scobox-buttons` boutons en bas de boite.
- `p.help`, `div.help`: explications, aide.
- ...
## Accès à la base de donnée
Sauf exception (requêtes exotiques, problèmes de performance) à discuter, les
accès à la base se font à travers l'ORM _SQLAlchemy_. Les modèles sont définis
dans `app/models`, sauf les comptes utilisateurs qui sont dans
`/app/auth/models.py`.
Au moment de la définition de nouveaux modèles, éviter si possible les champs
_nullables_, penser à créer là où ce sera utile des index.
## Tableaux
ScoDoc génère beaucoup de tableaux, sauf exception destinés à l'affichage Web et à l'export xlsx.
On utilise la super-classe `Table` de `app/tables/table_builder.py`.
Le rendu HTML utilise _DataTables_.
## Dates et heures
ScoDoc, contrairement à de nombreuses applications, est centré sur les
_étudiants_ et les enseignements, qui sont censés opérer dans le fuseau horaire
du serveur. Cela signifie que, quelle que soit l'emplacement de l'utilisateur,
les heures affichées et saisies par ScoDoc seront données dans le fuseau horaire
du serveur.
L'API publie et reçoit des heures au format ISO avec fuseau horaire (_timezone_) et n'est pas
concernée par cette remarque.
## Sélection de groupes
De nombreuses pages ont besoin d'offrir un moyen de sélectionner un ou plusieurs
groupes d'étudiants, comme dans cet exemple:
![menu multiselect des groupes](screens/multiselect_groups.png)
La vue doit récupérer la liste des groupes sélectionnés ainsi:
```py
group_ids = request.args.getlist("group_ids")
```
ou s'il s'agit un `POST`:
```py
group_ids = request.form.getlist("group_ids")
```
On converti ensuite les arguments en entiers:
```py
try:
group_ids = [int(gid) for gid in group_ids]
except ValueError as exc:
raise ScoValueError("group_ids invalide") from exc
```
et on peut utiliser le code ancien `sco_groups_view`:
```py
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids,
formsemestre_id=formsemestre.id,
select_all_when_unspecified=True,
)
```
`groups_infos` est un objet qui permet de récupérer les informations et de générer le menu.
Par exemple:
```py
Choix des groupes&nbsp;:
{sco_groups_view.menu_groups_choice(groups_infos, submit_on_change=True)
if groups_infos else ''}
```
La fonction `form_groups_choice` génère un formulaire complet.
Autres attributs/méthodes utiles de `groups_infos`:
- `get_etudids():list[int]` : les ids des étudiants des groupes sélectionnés;
- `groups_titles:str` : titres des groupes;
- `groups_filename:str`: un (morceau de) nom de fichier pour ces groupes.
!!! note "Voir aussi"
- [Introduction au développement ScoDoc](DevInternals.md)
- [Guide développeurs](GuideDeveloppeurs.md)
- [API ScoDoc 9](ScoDoc9API.md)
- [Modélisation du BUT](ModelisationParcoursBUT.md)
- [Contacts](Contact.md)