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>
52 lines
2 KiB
HTML
52 lines
2 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ quiz.title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="quiz-play-wrap">
|
|
<div class="quiz-play-header">
|
|
<span class="quiz-play-title">{{ quiz.icon }} {{ quiz.title }}</span>
|
|
<div class="progress-bar-wrap">
|
|
<div class="progress-bar" id="progress-bar" style="width:0%"></div>
|
|
</div>
|
|
<span class="progress-text" id="progress-text">1 / {{ quiz.questions|length }}</span>
|
|
</div>
|
|
|
|
<form method="post" id="quiz-form" action="{{ url_for('public.quiz_play', quiz_id=quiz.id) }}">
|
|
{% for i, q in quiz.questions | enumerate %}
|
|
<div class="question-block" data-index="{{ i }}" {% if i > 0 %}hidden{% endif %}>
|
|
<p class="question-num">Question {{ i + 1 }} / {{ quiz.questions|length }}</p>
|
|
<h2 class="question-text">{{ q.text }}</h2>
|
|
<div class="choices">
|
|
{% for j, choice in q.choices | enumerate %}
|
|
<label class="choice-label" id="label-{{ i }}-{{ j }}">
|
|
<input type="radio" name="q{{ i }}" value="{{ j }}"
|
|
onchange="onChoice({{ i }}, {{ j }})">
|
|
<span class="choice-letter">{{ ['A','B','C','D'][j] }}</span>
|
|
<span class="choice-text">{{ choice }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<input type="hidden" name="q{{ i }}" id="ans-{{ i }}" value="">
|
|
</div>
|
|
{% endfor %}
|
|
|
|
<div class="quiz-nav">
|
|
<button type="button" id="btn-prev" class="btn btn-outline" onclick="navigate(-1)" hidden>
|
|
← Précédente
|
|
</button>
|
|
<button type="button" id="btn-next" class="btn btn-primary" onclick="navigate(1)">
|
|
Suivante →
|
|
</button>
|
|
<button type="submit" id="btn-submit" class="btn btn-success" hidden>
|
|
Terminer le quiz ✓
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script id="quiz-data" type="application/json">{{ quiz_json | safe }}</script>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="{{ url_for('static', filename='quiz.js') }}"></script>
|
|
{% endblock %}
|