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>
73 lines
2.6 KiB
HTML
73 lines
2.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Résultat — {{ quiz.title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
{% set pct = (result.score / result.total * 100) | int %}
|
|
<div class="result-wrap">
|
|
<div class="result-card">
|
|
<span class="result-icon">
|
|
{% if pct >= 80 %}🏆
|
|
{% elif pct >= 60 %}👍
|
|
{% elif pct >= 40 %}📚
|
|
{% else %}💪{% endif %}
|
|
</span>
|
|
|
|
<h1>{{ quiz.title }}</h1>
|
|
|
|
<div class="score-display">
|
|
<span class="score-num">{{ result.score }}</span>
|
|
<span class="score-sep">/</span>
|
|
<span class="score-total">{{ result.total }}</span>
|
|
</div>
|
|
|
|
<div class="score-bar-wrap">
|
|
<div class="score-bar score-bar-{% if pct >= 80 %}great{% elif pct >= 60 %}good{% elif pct >= 40 %}ok{% else %}low{% endif %}"
|
|
style="width: {{ pct }}%"></div>
|
|
</div>
|
|
|
|
<p class="score-comment">
|
|
{% if pct == 100 %}Score parfait ! 🎉
|
|
{% elif pct >= 80 %}Excellent résultat !
|
|
{% elif pct >= 60 %}Bon travail, continuez !
|
|
{% elif pct >= 40 %}C'est un début, réessayez !
|
|
{% else %}Pas de souci, la pratique fait le maître !
|
|
{% endif %}
|
|
</p>
|
|
|
|
{% if user %}
|
|
<p class="result-saved">✓ Résultat enregistré dans <a href="{{ url_for('protected.profil') }}">votre profil</a>.</p>
|
|
{% else %}
|
|
<p class="result-saved">
|
|
<a href="{{ url_for('auth.login') }}">Connectez-vous</a> pour enregistrer vos scores.
|
|
</p>
|
|
{% endif %}
|
|
|
|
<h2 class="detail-title">Détail des réponses</h2>
|
|
<div class="answer-review">
|
|
{% for i, q in quiz.questions | enumerate %}
|
|
{% set user_ans = result.answers.get(i | string, result.answers.get(i, -1)) %}
|
|
{% set correct = (user_ans == q.answer) %}
|
|
<div class="answer-item {% if correct %}answer-correct{% else %}answer-wrong{% endif %}">
|
|
<p class="answer-q">{{ i + 1 }}. {{ q.text }}</p>
|
|
<p class="answer-given">
|
|
Votre réponse :
|
|
<strong>{% if user_ans >= 0 %}{{ q.choices[user_ans] }}{% else %}(sans réponse){% endif %}</strong>
|
|
{% if correct %}<span class="tag-correct">✓ Correct</span>
|
|
{% else %}<span class="tag-wrong">✗</span>{% endif %}
|
|
</p>
|
|
{% if not correct %}
|
|
<p class="answer-correct-text">
|
|
Bonne réponse : <strong>{{ q.choices[q.answer] }}</strong>
|
|
</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="result-actions">
|
|
<a href="{{ url_for('public.quiz_play', quiz_id=quiz.id) }}" class="btn btn-outline">Recommencer</a>
|
|
<a href="{{ url_for('public.quiz_list') }}" class="btn btn-primary">Autres quiz</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|