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>
83 lines
2.7 KiB
HTML
83 lines
2.7 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Mon profil{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-header">
|
|
<h1>👤 {{ user.name }}</h1>
|
|
<p class="text-muted">{{ user.email }}</p>
|
|
</div>
|
|
|
|
<div class="profil-stats">
|
|
<div class="stat-card">
|
|
<span class="stat-num">{{ stats.quizzes_done }}</span>
|
|
<span class="stat-label">quiz terminés</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-num">{{ stats.quizzes_total }}</span>
|
|
<span class="stat-label">quiz disponibles</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-num">{{ stats.attempts }}</span>
|
|
<span class="stat-label">tentatives au total</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 class="section-title">Mes meilleurs scores</h2>
|
|
{% if best_scores %}
|
|
<div class="scores-grid">
|
|
{% for quiz_id, q in quizzes.items() %}
|
|
{% if quiz_id in best_scores %}
|
|
{% set bs = best_scores[quiz_id] %}
|
|
{% set pct = (bs.score / bs.total * 100) | int %}
|
|
<div class="score-item">
|
|
<span class="score-item-icon">{{ q.icon }}</span>
|
|
<div class="score-item-body">
|
|
<strong>{{ q.title }}</strong>
|
|
<span class="level-badge level-badge-{{ q.level_id }}">{{ q.level }}</span>
|
|
<div class="mini-score-bar-wrap">
|
|
<div class="mini-score-bar {% if pct >= 80 %}great{% elif pct >= 60 %}good{% else %}ok{% endif %}"
|
|
style="width:{{ pct }}%"></div>
|
|
</div>
|
|
<span class="mini-score-text">{{ bs.score }} / {{ bs.total }} ({{ pct }}%)</span>
|
|
</div>
|
|
<a href="{{ url_for('public.quiz_play', quiz_id=quiz_id) }}" class="btn btn-sm btn-outline">Rejouer</a>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<p>Vous n'avez pas encore complété de quiz. <a href="{{ url_for('public.quiz_list') }}">Commencez maintenant !</a></p>
|
|
{% endif %}
|
|
|
|
<h2 class="section-title" style="margin-top:2.5rem">Historique des tentatives</h2>
|
|
{% if results %}
|
|
<div class="history-table-wrap">
|
|
<table class="history-table">
|
|
<thead>
|
|
<tr><th>Quiz</th><th>Score</th><th>Date</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for r in results %}
|
|
<tr>
|
|
<td>
|
|
{% if r['quiz_id'] in quizzes %}
|
|
{{ quizzes[r['quiz_id']].icon }} {{ quizzes[r['quiz_id']].title }}
|
|
{% else %}
|
|
{{ r['quiz_id'] }}
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<span class="{% if (r['score'] / r['total']) >= 0.8 %}score-great{% elif (r['score'] / r['total']) >= 0.6 %}score-good{% else %}score-ok{% endif %}">
|
|
{{ r['score'] }} / {{ r['total'] }}
|
|
</span>
|
|
</td>
|
|
<td class="text-muted">{{ r['completed_at'][:16] }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<p>Aucune tentative enregistrée.</p>
|
|
{% endif %}
|
|
{% endblock %}
|