mirror of
https://github.com/arthur-pbty/QCM_physique.git
synced 2026-06-27 06:27:35 +02:00
first commit
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user