GGThedandClaude Opus 5 838b247649 feat(perf): borner les vues de liste, et choisir le motif une bonne fois
MNT-14. Chaque vue de liste faisait .all() sur sa table. L'audit evaluait
l'impact a nul -- justement, a l'echelle d'une association etudiante -- et
recommandait de choisir le motif maintenant plutot que de le retro-adapter
plus tard. C'est ce que ceci est.

Applique a list_users, list_evaluations et team_matches.list_matches. Pour
cette derniere, la pagination borne aussi la boucle sur les participants,
qui est le N+1 que le constat designait comme le premier a se degrader.

Trois decisions, parce que ce sont celles qui se prennent deux fois
differemment sinon.

error_out=False : les numeros de page arrivent par l'URL, donc ?page=999 est
une chose qu'on tape ou qu'un signet perime contient. Le defaut de
Flask-SQLAlchemy y repond par un 404, ce qui est deroutant pour quelqu'un qui
est simplement alle une page trop loin.

Un plafond sur per_page : c'est aussi un parametre d'URL, et sans plafond
?per_page=100000 redonne a la main exactement la requete non bornee que la
pagination existe pour empecher.

page_url est un global Jinja plutot qu'une valeur que chaque vue passe. Ce
qui se rate avec des liens de pagination, c'est le reste de la chaine de
requete : la liste d'evaluations porte sort et order, celle des matchs
d'equipe porte team_id, et un lien qui les perd reinitialise silencieusement
la vue que la personne regardait. Les deux tests qui l'epinglent tombent si
page_url cesse de les recopier -- verifie par mutation.

Les tris sont completes par une cle unique : une requete paginee sans ORDER
BY stable peut montrer la meme ligne deux fois et jamais une autre.

Au passage, huit entrees fuzzy corrigees dans les catalogues, dont deux
laissees par le commit SEC-16 : une entree fuzzy est ignoree a l'execution,
donc ces messages retombaient en anglais.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-11 20:20:42 -04:00
2026-08-06 14:46:51 -04:00

Plateforme centralisée de tryouts

Application interne du club e-sport de l'UdeS : inscriptions aux sélections, évaluations, gestion des équipes, disponibilités, contrats, et notifications Discord.

Le site est servi en français, l'anglais reste accessible par le sélecteur de la barre latérale (voir docs/translations.md).


Démarrer

python -m venv .venv
.venv/Scripts/pip install -r requirements.txt -r requirements-dev.txt
cp app/.env.example .env      # puis remplir SECRET_KEY et DATABASE_URL
.venv/Scripts/python run.py   # développement, http://127.0.0.1:5000

SECRET_KEY et DATABASE_URL sont obligatoires : create_app() refuse de démarrer sans eux. DATABASE_URL doit pointer sur PostgreSQL ; le pilote psycopg 3 est nommé automatiquement si l'URL n'en nomme pas.

Production : python wsgi.py (Waitress derrière nginx). Voir docs/deployment.md.

Ce sont les deux seuls points d'entrée.

Vérifier

.venv/Scripts/python -m pytest        # suite complète
.venv/Scripts/python -m ruff check .  # lint
.venv/Scripts/python -m ruff format --check .

Les trois tournent en CI et y sont bloquants.


Ce que fait l'application

  • Comptes et rôles — cinq rôles : président (admin), gérant (manager), coach, joueur (player), recruteur (scout). Le président attribue les rôles.
  • Sélections — organisation des tryouts, trois formats de match (équipe contre équipe, joueur contre joueur, scrim), évaluation des joueurs sur dix critères.
  • Équipes — effectifs de la saison, matchs et entraînements. Le formulaire d'entraînement affiche les disponibilités des joueurs.
  • Disponibilités — créneaux hebdomadaires des joueurs, créneaux réservables des coachs.
  • Notes — un coach écrit des notes d'équipe (visibles par l'équipe) et des notes nominatives (visibles par le joueur concerné).
  • Un-à-un — un joueur demande une séance à son coach ; le coach répond depuis le site ou par une réaction sur le message privé Discord.
  • Contrats — dépôt d'un contrat par le staff, signature par le joueur.

Comment c'est construit

Backend Python 3.12 / Flask, rendu serveur en Jinja2, CSS et JavaScript maison, sans framework front. Base PostgreSQL via SQLAlchemy. Bot Discord (discord.py) dans un fil du même processus que le serveur web.

docs/architecture.md contient les diagrammes (classes, paquets, flux d'une requête).


Sécurité

En place et vérifié par des tests :

  • Limitation de débit sur la connexion (10 requêtes/minute par IP).
  • Cookies de session HttpOnly, SameSite=Lax, Secure, avec expiration effective.
  • CSRF sur tous les formulaires, y compris la déconnexion (en POST).
  • HTTPS forcé en production, HSTS.
  • CSP sans unsafe-inline sur script-src : aucun gestionnaire d'événement en ligne, chaque bloc <script> porte un nonce par requête.
  • Redirections validées (rien ne sort du site).
  • Validation par schémas marshmallow sur les formulaires de compte, avec politique de mot de passe.
  • Autorisation centralisée dans app/permissions.py.
  • Journal d'authentification (logs/auth.log) : connexions, échecs, changements de rôle, suppressions de compte.
  • Téléversements vérifiés par extension et par signature de fichier.

Ce qui n'est pas fait, pour que personne ne s'y fie :

  • Aucune migration de schéma. db.create_all() crée les tables manquantes et ne modifie jamais une table existante : une colonne ajoutée à un modèle est absente de la production.
  • Les secrets de l'historique git ne sont pas révoqués — jeton du bot, mot de passe PostgreSQL, SECRET_KEY.
  • trusted_proxy='*' reste le défaut : l'en-tête X-Forwarded-For est accepté de n'importe quelle source, donc la limitation par IP est contournable. C'est désormais la variable TRUSTED_PROXY plutôt qu'une constante — docs/deployment.md donne la valeur pour chaque topologie.
  • Les comptes créés par le formulaire d'inscription sont actifs immédiatement : il n'y a pas d'étape de validation par le staff. Décision de produit en attente, voir docs/roles-and-permissions.md.
  • L'identité Discord transite encore par un champ caché du formulaire d'inscription : elle n'est pas prouvée par le passage OAuth2.

docs/security-checklist.md détaille la liste avant mise en production.

Documentation

Document Pour
docs/deployment.md Installer, déployer, revenir en arrière
docs/roles-and-permissions.md Qui peut faire quoi, et où c'est décidé
docs/database-restore.md Sauvegarder et restaurer
docs/database-schema.md Sortir de create_all() : relevé, Alembic, migrations
docs/incident-runbook.md Quand quelque chose ne va pas
docs/architecture.md Diagrammes
docs/translations.md Ajouter ou corriger une traduction
docs/security-checklist.md Avant une mise en production

Intégration Discord

Messages privés au coach lors d'une demande d'un-à-un, aux joueurs à la création d'un match ou d'un entraînement les concernant, et rappel 24 h avant un match. Les réponses se font par réaction sur le message ou depuis le site.

L'état des messages en attente de réponse est dans discord_pending.json, non versionné : c'est de l'état d'exécution, propre à chaque serveur.

Mise en place

Le bot du club existe déjà ; ce qui suit ne concerne qu'une nouvelle installation.

  1. Créer une application sur le portail développeur Discord, puis un bot.
  2. Copier le jeton dans DISCORD_BOT_TOKEN.
  3. Activer Message Content Intent dans les Privileged Gateway Intents. C'est le seul intent privilégié demandé : il sert à lire le motif d'un refus écrit en réponse au message.
  4. Chaque personne doit partager un serveur avec le bot (ou l'avoir en ami) pour recevoir un message privé, et renseigner son identifiant Discord dans son profil (Discord → Paramètres → Avancés → Mode développeur, puis clic droit sur son profil → Copier l'identifiant).

/health indique si le bot tourne et s'il est connecté.

S
Description
No description provided
Readme
2.3 MiB
Languages
Python 67.9%
HTML 27%
CSS 3.5%
JavaScript 1.6%