mirror of
https://github.com/arthur-pbty/QCM_physique.git
synced 2026-06-03 15:07:33 +02:00
350 lines
16 KiB
HTML
350 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="tabs">
|
|
<button class="tab-button" id="tab-btn-all">Toutes</button>
|
|
<button class="tab-button" id="tab-btn-random">Aléatoire</button>
|
|
<button class="tab-button" id="tab-btn-manage">Gérer</button>
|
|
</div>
|
|
|
|
<!-- Onglet : Toutes (liste complète) -->
|
|
<div id="tab-all">
|
|
<div class="controls">
|
|
<button class="small-btn btn" id="reset-all">Réinitialiser</button>
|
|
<label style="margin-left:1rem;display:flex;align-items:center;gap:.4rem;">
|
|
<input type="checkbox" id="all-randomize"> Aléatoire
|
|
</label>
|
|
<span id="all-score" style="margin-left:1rem;color:#333"></span>
|
|
<div style="margin-left:auto;color:#666" id="all-count"></div>
|
|
</div>
|
|
<form id="qcm-form">
|
|
{% for q in questions %}
|
|
<div class="question" data-qid="{{ q.id }}">
|
|
<div><strong>Question {{ loop.index }}:</strong> {{ q.text | safe }}</div>
|
|
<div class="answers">
|
|
{% for a in q.answers %}
|
|
<label class="answer" data-idx="{{ a.idx }}">
|
|
<input type="radio" name="q_{{ q.id }}" value="{{ a.idx }}" data-correct="{% if a.correct %}1{% else %}0{% endif %}"> {{ a.text }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="feedback" style="margin-top:.5rem"></div>
|
|
</div>
|
|
{% endfor %}
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Onglet : Aléatoire -->
|
|
<div id="tab-random" style="display:none">
|
|
<div class="controls" style="margin-bottom:.5rem">
|
|
<button class="small-btn btn" id="random-next">Nouvelle question</button>
|
|
<button class="small-btn btn" id="random-reset">Reset score</button>
|
|
<span id="random-score" style="margin-left:1rem"></span>
|
|
</div>
|
|
<div id="random-area"></div>
|
|
</div>
|
|
|
|
<!-- Onglet : Gérer -->
|
|
<div id="tab-manage" style="display:none">
|
|
<div style="margin-bottom:.5rem">Choisissez les questions autorisées</div>
|
|
<div id="manage-list">
|
|
{% for q in questions %}
|
|
<div class="manage-item">
|
|
<label>
|
|
<input type="checkbox" class="manage-allow" data-qid="{{ q.id }}"> Question {{ loop.index }}
|
|
</label>
|
|
<div style="margin-left:1.2rem">{{ q.text | safe }}</div>
|
|
<div style="margin-left:1.2rem;color:#666">
|
|
Dernière actualisation: {{
|
|
(
|
|
q.last_scraped[:10].split('-')[2] ~ '/' ~
|
|
q.last_scraped[:10].split('-')[1] ~ '/' ~
|
|
q.last_scraped[:10].split('-')[0] ~
|
|
' - ' ~
|
|
"%02d"|format((q.last_scraped[11:13] | int + 1) % 24) ~
|
|
'h' ~
|
|
q.last_scraped[14:16]
|
|
) or 'inconnu'
|
|
}}
|
|
</div>
|
|
<div style="margin-left:1.2rem;margin-top:.4rem">
|
|
<strong>Réponses :</strong>
|
|
<ul style="margin-top:.25rem">
|
|
{% for a in q.answers %}
|
|
<li style="margin:.15rem 0;">
|
|
<span {% if a.correct %} style="color:green;font-weight:bold" {% endif %}>{{ a.text }}</span>
|
|
{% if a.correct %} <strong>(bonne)</strong> {% endif %}
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<script id="all-questions" type="application/json">{{ questions_json | safe }}</script>
|
|
<script>
|
|
// embed data for client-side random selection (JSON fourni côté serveur)
|
|
const ALL_QUESTIONS = JSON.parse(document.getElementById('all-questions').textContent);
|
|
|
|
document.addEventListener('DOMContentLoaded', function(){
|
|
// tab switching
|
|
function showTab(id){
|
|
document.getElementById('tab-all').style.display = id==='all' ? '' : 'none';
|
|
document.getElementById('tab-random').style.display = id==='random' ? '' : 'none';
|
|
document.getElementById('tab-manage').style.display = id==='manage' ? '' : 'none';
|
|
// re-typeset MathJax for the shown tab (if needed)
|
|
try{
|
|
if(window.MathJax && window.MathJax.typesetPromise){
|
|
const el = id==='all' ? document.getElementById('tab-all') : (id==='random' ? document.getElementById('tab-random') : document.getElementById('tab-manage'));
|
|
if(el) window.MathJax.typesetPromise([el]);
|
|
}
|
|
}catch(e){ /* ignore */ }
|
|
// toggle active class on tab buttons
|
|
document.querySelectorAll('.tab-button').forEach(function(b){ b.classList.remove('active'); });
|
|
if(id === 'all') document.getElementById('tab-btn-all').classList.add('active');
|
|
if(id === 'random') document.getElementById('tab-btn-random').classList.add('active');
|
|
if(id === 'manage') document.getElementById('tab-btn-manage').classList.add('active');
|
|
}
|
|
document.getElementById('tab-btn-all').addEventListener('click', ()=> { showTab('all'); localStorage.setItem('q_last_tab','all'); });
|
|
document.getElementById('tab-btn-random').addEventListener('click', ()=> { showTab('random'); localStorage.setItem('q_last_tab','random'); const q = pickRandomQuestion(); renderQuestionInto(document.getElementById('random-area'), q); });
|
|
document.getElementById('tab-btn-manage').addEventListener('click', ()=> { showTab('manage'); localStorage.setItem('q_last_tab','manage'); });
|
|
|
|
// restore allowed ids from localStorage or default to all
|
|
function getAllowed(){
|
|
let raw = localStorage.getItem('q_allowed_ids');
|
|
if(!raw) return ALL_QUESTIONS.map(q=>q.id);
|
|
try{ return JSON.parse(raw); }catch(e){ return ALL_QUESTIONS.map(q=>q.id); }
|
|
}
|
|
function setAllowed(arr){ localStorage.setItem('q_allowed_ids', JSON.stringify(arr)); }
|
|
|
|
// init manage checkboxes
|
|
const allowed = new Set(getAllowed());
|
|
document.querySelectorAll('.manage-allow').forEach(function(cb){
|
|
let id = Number(cb.getAttribute('data-qid'));
|
|
cb.checked = allowed.has(id);
|
|
cb.addEventListener('change', function(){
|
|
let cur = new Set(getAllowed());
|
|
if(cb.checked) cur.add(id); else cur.delete(id);
|
|
setAllowed(Array.from(cur));
|
|
// apply allowed set to the 'Toutes' tab and reapply ordering
|
|
applyAllowedToAllTab();
|
|
applyAllOrderSetting();
|
|
// if random area displays a question that is no longer allowed, clear it
|
|
const randArea = document.getElementById('random-area');
|
|
if(randArea){
|
|
const qEl = randArea.querySelector('.question');
|
|
if(qEl && !cur.has(Number(qEl.getAttribute('data-qid')))) randArea.innerHTML = '<div>Aucune question disponible.</div>';
|
|
}
|
|
});
|
|
});
|
|
|
|
// apply allowed set to the 'Toutes' tab (hide questions not allowed)
|
|
function applyAllowedToAllTab(){
|
|
const allowedIds = new Set(getAllowed());
|
|
const container = document.querySelector('#tab-all form');
|
|
if(!container) return;
|
|
const qs = Array.from(container.querySelectorAll('.question'));
|
|
qs.forEach(function(q){
|
|
const qid = Number(q.getAttribute('data-qid'));
|
|
if(allowedIds.has(qid)) q.style.display = '';
|
|
else q.style.display = 'none';
|
|
});
|
|
const visible = qs.filter(q => q.style.display !== 'none');
|
|
document.getElementById('all-count').textContent = `Questions: ${visible.length}`;
|
|
// if random area displays a question that is no longer allowed, clear it
|
|
const randArea = document.getElementById('random-area');
|
|
if(randArea){
|
|
const qEl = randArea.querySelector('.question');
|
|
if(qEl && !allowedIds.has(Number(qEl.getAttribute('data-qid')))) randArea.innerHTML = '<div>Aucune question disponible.</div>';
|
|
}
|
|
}
|
|
|
|
// random question logic
|
|
function pickRandomQuestion(){
|
|
const allowedIds = new Set(getAllowed());
|
|
const pool = ALL_QUESTIONS.filter(q => allowedIds.has(q.id));
|
|
if(pool.length === 0) return null;
|
|
return pool[Math.floor(Math.random()*pool.length)];
|
|
}
|
|
|
|
function renderQuestionInto(container, q){
|
|
if(!q){ container.innerHTML = '<div>Aucune question disponible.</div>'; return; }
|
|
let html = `<div class="question" data-qid="${q.id}">`;
|
|
html += `<div><strong>Question:</strong> ${q.text}</div>`;
|
|
html += '<div class="answers">';
|
|
// shuffle answers for this question
|
|
const answersShuffled = q.answers.slice().sort(()=>Math.random()-0.5);
|
|
answersShuffled.forEach(function(a){
|
|
html += `<label class="answer"><input type="radio" name="rand_${q.id}" value="${a.idx}" data-correct="${a.correct?1:0}"> ${a.text}</label>`;
|
|
});
|
|
html += '</div><div class="feedback" style="margin-top:.5rem"></div></div>';
|
|
container.innerHTML = html;
|
|
// render LaTeX in the injected content
|
|
try{
|
|
if(window.MathJax && window.MathJax.typesetPromise){
|
|
window.MathJax.typesetPromise([container]);
|
|
} else if(window.MathJax && window.MathJax.typeset){
|
|
window.MathJax.typeset([container]);
|
|
}
|
|
}catch(e){ /* ignore MathJax errors */ }
|
|
|
|
// attach immediate feedback handler
|
|
container.querySelectorAll('input[type=radio]').forEach(function(radio){
|
|
radio.addEventListener('change', function(e){
|
|
const input = e.target;
|
|
const qDiv = input.closest('.question');
|
|
const feedback = qDiv.querySelector('.feedback');
|
|
const isCorrect = input.getAttribute('data-correct') === '1';
|
|
qDiv.querySelectorAll('input').forEach(i=>i.disabled=true);
|
|
if(isCorrect) feedback.innerHTML = '<span style="color:green;font-weight:bold">Bonne réponse</span>';
|
|
else {
|
|
feedback.innerHTML = '<span style="color:red;font-weight:bold">Mauvaise réponse</span>';
|
|
qDiv.querySelectorAll('input').forEach(function(i){ if(i.getAttribute('data-correct')==='1') i.closest('label').style.outline='2px solid green'; });
|
|
}
|
|
input.closest('label').style.background = isCorrect? '#ddffdd' : '#ffdddd';
|
|
// update score display
|
|
updateRandomScore(isCorrect);
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateRandomScore(lastCorrect){
|
|
let s = localStorage.getItem('q_random_score');
|
|
let st = s ? JSON.parse(s) : {correct:0,total:0};
|
|
if(typeof lastCorrect === 'boolean'){ st.total += 1; if(lastCorrect) st.correct +=1; }
|
|
localStorage.setItem('q_random_score', JSON.stringify(st));
|
|
document.getElementById('random-score').textContent = `Score: ${st.correct} / ${st.total}`;
|
|
}
|
|
document.getElementById('random-next').addEventListener('click', function(){
|
|
const q = pickRandomQuestion();
|
|
renderQuestionInto(document.getElementById('random-area'), q);
|
|
});
|
|
document.getElementById('random-reset').addEventListener('click', function(){
|
|
localStorage.removeItem('q_random_score'); updateRandomScore();
|
|
});
|
|
|
|
// init score for random tab
|
|
updateRandomScore();
|
|
|
|
// --- All-tab score handling ---
|
|
// --- All-tab score handling ---
|
|
function updateAllScore(lastCorrect){
|
|
let s = localStorage.getItem('q_all_score');
|
|
let st = s ? JSON.parse(s) : {correct:0,total:0};
|
|
if(typeof lastCorrect === 'boolean'){ st.total += 1; if(lastCorrect) st.correct +=1; }
|
|
localStorage.setItem('q_all_score', JSON.stringify(st));
|
|
const el = document.getElementById('all-score');
|
|
if(el) el.textContent = `Score: ${st.correct} / ${st.total}`;
|
|
}
|
|
|
|
// ensure all answers are displayed in random order for each question
|
|
function shuffleAnswersForQuestion(qElem){
|
|
if(!qElem) return;
|
|
const answers = qElem.querySelector('.answers');
|
|
if(!answers) return;
|
|
const labs = Array.from(answers.querySelectorAll('label'));
|
|
const shuffled = labs.sort(()=>Math.random()-0.5);
|
|
shuffled.forEach(l => answers.appendChild(l));
|
|
}
|
|
|
|
// maintain initial order of question elements but always randomize answers inside each question
|
|
const allContainer = document.querySelector('#tab-all form');
|
|
const initialQs = allContainer ? Array.from(allContainer.querySelectorAll('.question')) : [];
|
|
// shuffle answers initially for every question
|
|
initialQs.forEach(q => shuffleAnswersForQuestion(q));
|
|
|
|
function shuffleAllQuestions(){
|
|
if(!allContainer) return;
|
|
const qs = Array.from(allContainer.querySelectorAll('.question'));
|
|
const visible = qs.filter(q => q.style.display !== 'none');
|
|
// remove visible nodes and re-append them shuffled so hidden ones stay in place
|
|
visible.forEach(q => allContainer.removeChild(q));
|
|
const shuffled = visible.sort(()=>Math.random()-0.5);
|
|
shuffled.forEach(q=> allContainer.appendChild(q));
|
|
// always randomize answers too
|
|
shuffled.forEach(q => shuffleAnswersForQuestion(q));
|
|
document.getElementById('all-count').textContent = `Questions: ${shuffled.length}`;
|
|
}
|
|
|
|
function restoreInitialOrder(){
|
|
if(!allContainer) return;
|
|
// remove currently visible questions first
|
|
const qs = Array.from(allContainer.querySelectorAll('.question'));
|
|
const visible = qs.filter(q => q.style.display !== 'none');
|
|
visible.forEach(q => allContainer.removeChild(q));
|
|
// append initial questions but only those allowed (visible)
|
|
const allowedIds = new Set(getAllowed());
|
|
const toAppend = initialQs.filter(q => allowedIds.has(Number(q.getAttribute('data-qid'))));
|
|
toAppend.forEach(q => allContainer.appendChild(q));
|
|
// still randomize answers for each appended question
|
|
toAppend.forEach(q => shuffleAnswersForQuestion(q));
|
|
document.getElementById('all-count').textContent = `Questions: ${toAppend.length}`;
|
|
}
|
|
|
|
function applyAllOrderSetting(){
|
|
const enabled = localStorage.getItem('q_all_random') === '1';
|
|
if(enabled) shuffleAllQuestions(); else restoreInitialOrder();
|
|
}
|
|
|
|
// checkbox for toggling random display of question order (answers remain random)
|
|
const allRandCb = document.getElementById('all-randomize');
|
|
if(allRandCb){
|
|
allRandCb.checked = localStorage.getItem('q_all_random') === '1';
|
|
allRandCb.addEventListener('change', function(){
|
|
localStorage.setItem('q_all_random', allRandCb.checked ? '1' : '0');
|
|
applyAllOrderSetting();
|
|
});
|
|
}
|
|
|
|
// reset stored all-tab score on page load so refresh clears the score
|
|
localStorage.removeItem('q_all_score');
|
|
|
|
// init reset on all tab
|
|
document.getElementById('reset-all').addEventListener('click', function(){
|
|
document.querySelectorAll('#tab-all .question').forEach(function(q){ q.querySelectorAll('input').forEach(function(i){ i.disabled=false; i.checked=false; }); q.querySelector('.feedback').innerHTML=''; q.querySelectorAll('label').forEach(function(l){ l.style.background=''; l.style.outline=''; });
|
|
// re-randomize answers for this question
|
|
shuffleAnswersForQuestion(q);
|
|
});
|
|
// clear stored score for all tab
|
|
localStorage.removeItem('q_all_score'); updateAllScore();
|
|
// apply order setting (shuffle or restore)
|
|
applyAllOrderSetting();
|
|
});
|
|
|
|
// also attach immediate feedback for the all-tab radios and update the all score
|
|
document.querySelectorAll('#tab-all input[type=radio]').forEach(function(radio){
|
|
radio.addEventListener('change', function(e){
|
|
var input = e.target;
|
|
var qDiv = input.closest('.question');
|
|
var feedback = qDiv.querySelector('.feedback');
|
|
var isCorrect = input.getAttribute('data-correct') === '1';
|
|
qDiv.querySelectorAll('input').forEach(function(i){ i.disabled = true; });
|
|
if(isCorrect){ feedback.innerHTML = '<span style="color:green;font-weight:bold">Bonne réponse</span>'; }
|
|
else { feedback.innerHTML = '<span style="color:red;font-weight:bold">Mauvaise réponse</span>'; qDiv.querySelectorAll('input').forEach(function(i){ if(i.getAttribute('data-correct') === '1'){ var lab = i.closest('label'); if(lab) lab.style.outline = '2px solid green'; } }); }
|
|
var chosenLabel = input.closest('label'); if(chosenLabel){ chosenLabel.style.background = isCorrect ? '#ddffdd' : '#ffdddd'; }
|
|
updateAllScore(isCorrect);
|
|
});
|
|
});
|
|
|
|
// initial apply according to allowed/setting and init all score display
|
|
applyAllowedToAllTab();
|
|
applyAllOrderSetting();
|
|
updateAllScore();
|
|
|
|
// restore last tab from localStorage (or show all)
|
|
const lastTab = localStorage.getItem('q_last_tab') || 'all';
|
|
showTab(lastTab);
|
|
// if last tab was random, show a question immediately
|
|
if(lastTab === 'random'){
|
|
const q0 = pickRandomQuestion();
|
|
renderQuestionInto(document.getElementById('random-area'), q0);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- script de feedback et reset global intégré dans le premier bloc; script dupliqué supprimé -->
|
|
|
|
{% endblock %}
|