alpinux.site.2026/dynamic/routes/public.py
Cédrix 27847dfad3 feat: dynamic.alpinux.org — quiz interactifs Flask + AlpID OIDC
App Flask complète pour https://dynamic.alpinux.org :
- 10 quiz Linux, 5 niveaux (Découverte → Expert), 50+ questions
- Public : Découverte, Débutant, Intermédiaire (6 quiz)
- Membres AlpID : Avancé, Expert (4 quiz — Git, Admin, Sécurité, Bash)
- Navigation question par question avec avance automatique après choix
- Score calculé côté serveur, enregistré en SQLite si connecté
- Page profil : meilleurs scores par quiz + historique des tentatives

Authentification :
- OIDC via authlib + AlpID (Keycloak), SSO partagé avec Gitea/Nextcloud
- Décorateur @login_required, redirection post-login sur l'URL d'origine
- /auth/login, /auth/callback, /auth/logout

Structure :
- dynamic/app.py, db.py, quiz.py, auth_utils.py
- dynamic/routes/ (public.py, auth.py, protected.py)
- dynamic/templates/ (base, index, quiz/*, profil/)
- dynamic/static/ (style.css thème Alpinux, quiz.js vanilla)
- dynamic/data/quizzes.json (source de vérité des questions)
- dynamic/.env.example

Infrastructure :
- scripts/dynamic.alpinux.org.vhost.conf (Apache reverse proxy)
- scripts/dynamic.alpinux.org.service (systemd Gunicorn)
- docs/technique/deploiement-dynamic.md (procédure complète)
- mkdocs.yml : page de déploiement ajoutée à la nav Technique
- .gitignore : exclut venv/ et .env

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 12:09:37 +02:00

85 lines
2.6 KiB
Python

import json
from flask import Blueprint, render_template, request, redirect, url_for, session, abort
from auth_utils import current_user, login_required
from quiz import get_all, get_by_id
from db import save_result
public_bp = Blueprint('public', __name__)
@public_bp.route('/')
def index():
quizzes = get_all()
return render_template('index.html', quizzes=quizzes, user=current_user())
@public_bp.route('/quiz/')
def quiz_list():
quizzes = get_all()
return render_template('quiz/list.html', quizzes=quizzes, user=current_user())
@public_bp.route('/quiz/<quiz_id>/')
def quiz_intro(quiz_id):
quiz = get_by_id(quiz_id)
if not quiz:
abort(404)
return render_template('quiz/intro.html', quiz=quiz, user=current_user())
@public_bp.route('/quiz/<quiz_id>/jouer', methods=['GET', 'POST'])
def quiz_play(quiz_id):
quiz = get_by_id(quiz_id)
if not quiz:
abort(404)
if quiz['members_only'] and not current_user():
session['next_url'] = url_for('public.quiz_play', quiz_id=quiz_id)
return redirect(url_for('auth.login'))
if request.method == 'POST':
answers = {}
for i in range(len(quiz['questions'])):
val = request.form.get(f'q{i}')
answers[i] = int(val) if val is not None else -1
score = sum(
1 for i, q in enumerate(quiz['questions'])
if answers.get(i) == q['answer']
)
total = len(quiz['questions'])
if current_user():
save_result(
current_user()['sub'],
current_user()['name'],
quiz_id, score, total,
)
# Stocke résultat et détail en session pour la page résultat
session['last_result'] = {
'quiz_id': quiz_id,
'score': score,
'total': total,
'answers': answers,
}
return redirect(url_for('public.quiz_result', quiz_id=quiz_id))
return render_template('quiz/play.html',
quiz=quiz,
quiz_json=json.dumps(quiz),
user=current_user())
@public_bp.route('/quiz/<quiz_id>/resultat')
def quiz_result(quiz_id):
quiz = get_by_id(quiz_id)
result = session.pop('last_result', None)
if not quiz or not result or result['quiz_id'] != quiz_id:
return redirect(url_for('public.quiz_intro', quiz_id=quiz_id))
return render_template('quiz/result.html',
quiz=quiz,
result=result,
user=current_user())