`{# ... #}` در جنگو فقط تکخطی است. کامنتی که دیشب اضافه کردم سه خط بود، پس
جنگو کامنت حسابش نکرد و کلِ متنش بالای صفحهی مرورِ محتوا چاپ شد.
کلاً برداشته شد. برای توضیحِ چندخطی در قالب باید {% comment %} استفاده شود،
ولی اینجا اصلاً توضیحی لازم نبود.
همهی قالبهای پروژه برای همین الگو بررسی شدند؛ جای دیگری نبود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
8.1 KiB
HTML
156 lines
8.1 KiB
HTML
{% extends "admin/base_site.html" %}
|
|
{% block content %}
|
|
<style>
|
|
/* رنگها از متغیرهای خودِ پنلِ ادمین میآیند، نه ثابت.
|
|
چرا: قبلاً هر پنل background روشن داشت ولی color نداشت. در حالتِ تیرهی
|
|
مرورگر، ادمین رنگِ متن را روشن میکند و نتیجه متنِ سفید روی زمینهی سفید
|
|
میشد — یعنی صفحه عملاً خوانا نبود. مقدارِ دومِ هر var پشتیبانِ حالتِ روشن
|
|
است، پس اگر متغیری نبود هم چیزی خراب نمیشود.
|
|
چیپهای رنگی (badge/choice) عمداً زمینهی روشنِ ثابت دارند و برای همین
|
|
رنگِ متنشان هم صریح و تیره ست شده تا در هر دو حالت خوانا بمانند. */
|
|
.cb { direction: rtl; color: var(--body-fg, #333); }
|
|
.cb-cols { display: flex; gap: 12px; align-items: stretch; flex-wrap: wrap; }
|
|
.cb-col { flex: 1 1 190px; min-width: 170px; border: 1px solid var(--border-color, #ddd);
|
|
border-radius: 8px; background: var(--body-bg, #fff); color: var(--body-fg, #333);
|
|
display: flex; flex-direction: column; max-height: 420px; }
|
|
.cb-col h3 { margin: 0; padding: 8px 10px; font-size: 13px;
|
|
background: var(--darkened-bg, #f6f6f6); color: var(--body-fg, #333);
|
|
border-bottom: 1px solid var(--border-color, #ddd); border-radius: 8px 8px 0 0; }
|
|
.cb-list { overflow-y: auto; flex: 1; }
|
|
.cb-item { padding: 7px 10px; cursor: pointer;
|
|
border-bottom: 1px solid var(--hairline-color, #f0f0f0);
|
|
font-size: 13px; display: flex; justify-content: space-between; gap: 8px; }
|
|
.cb-item:hover { background: var(--selected-row, #eef5ff); }
|
|
.cb-item.sel { background: var(--selected-bg, #dbeafe); font-weight: 600; }
|
|
.cb-count { color: var(--body-quiet-color, #888); font-size: 11px; }
|
|
.cb-empty { padding: 10px; color: var(--body-quiet-color, #999); font-size: 12px; }
|
|
.cb-q { border: 1px solid var(--border-color, #ddd); border-radius: 8px; padding: 12px;
|
|
margin-top: 10px; background: var(--body-bg, #fff); color: var(--body-fg, #333); }
|
|
.cb-q.flag { border-color: #f0a; border-right: 4px solid #e11d48; }
|
|
.cb-q-top { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; }
|
|
.cb-badges { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
.cb-badge { font-size: 11px; padding: 2px 7px; border-radius: 10px; white-space: nowrap; }
|
|
.cb-badge.warn { background: #fee2e2; color: #b91c1c; }
|
|
.cb-badge.info { background: #f1f5f9; color: #475569; }
|
|
.cb-choices { margin: 8px 0 0; padding: 0; list-style: none; display: flex;
|
|
flex-wrap: wrap; gap: 6px; }
|
|
.cb-choices li { font-size: 12px; padding: 3px 9px; border-radius: 6px;
|
|
background: #f1f5f9; color: #334155; }
|
|
.cb-choices li.ok { background: #dcfce7; color: #15803d; font-weight: 600; }
|
|
</style>
|
|
|
|
<div class="cb">
|
|
<p style="color:var(--body-quiet-color,#666)">از راست به چپ انتخاب کنید تا به سوالها برسید.</p>
|
|
|
|
<div class="cb-cols">
|
|
<div class="cb-col">
|
|
<h3>پایه</h3>
|
|
<div class="cb-list" id="col-grade">
|
|
{% for grade in grades %}
|
|
<div class="cb-item" data-id="{{ grade.id }}">{{ grade.title }}</div>
|
|
{% empty %}
|
|
<div class="cb-empty">هیچ پایهای ثبت نشده.</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="cb-col"><h3>فصل</h3><div class="cb-list" id="col-unit"><div class="cb-empty">اول پایه را انتخاب کنید.</div></div></div>
|
|
<div class="cb-col"><h3>درس</h3><div class="cb-list" id="col-node"><div class="cb-empty">—</div></div></div>
|
|
<div class="cb-col"><h3>مبحث</h3><div class="cb-list" id="col-lesson"><div class="cb-empty">—</div></div></div>
|
|
</div>
|
|
|
|
<div id="questions" style="margin-top:18px"></div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
const el = (id) => document.getElementById(id);
|
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) =>
|
|
({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
|
|
function setEmpty(col, text) { col.innerHTML = '<div class="cb-empty">' + esc(text) + '</div>'; }
|
|
|
|
async function load(url) {
|
|
const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
|
|
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
return response.json();
|
|
}
|
|
|
|
function fill(col, rows, countKey, onPick) {
|
|
if (!rows.length) { setEmpty(col, 'چیزی اینجا نیست.'); return; }
|
|
col.innerHTML = '';
|
|
rows.forEach((row) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'cb-item';
|
|
item.innerHTML = '<span>' + esc(row.title) + '</span>'
|
|
+ '<span class="cb-count">' + (row[countKey] ?? 0) + '</span>';
|
|
item.onclick = () => {
|
|
col.querySelectorAll('.cb-item').forEach((n) => n.classList.remove('sel'));
|
|
item.classList.add('sel');
|
|
onPick(row.id);
|
|
};
|
|
col.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function renderQuestions(data) {
|
|
const box = el('questions');
|
|
let html = '<a class="addlink" href="' + data.addUrl + '">افزودنِ سوالِ تازه به این مبحث</a>';
|
|
if (!data.results.length) {
|
|
box.innerHTML = html + '<p style="color:var(--body-quiet-color,#999);margin-top:10px">هنوز سوالی برای این مبحث ساخته نشده.</p>';
|
|
return;
|
|
}
|
|
data.results.forEach((q) => {
|
|
const flagged = q.openReports > 0 || (q.wrongPercent !== null && q.wrongPercent >= 70);
|
|
let badges = '';
|
|
if (q.openReports > 0) {
|
|
badges += '<a class="cb-badge warn" href="' + q.reportsUrl + '">'
|
|
+ q.openReports + ' گزارشِ باز</a>';
|
|
}
|
|
if (q.wrongPercent !== null) {
|
|
badges += '<span class="cb-badge ' + (q.wrongPercent >= 70 ? 'warn' : 'info') + '">'
|
|
+ q.wrongPercent + '٪ غلط از ' + q.totalAnswers + ' پاسخ</span>';
|
|
} else if (q.totalAnswers) {
|
|
badges += '<span class="cb-badge info">' + q.totalAnswers + ' پاسخ</span>';
|
|
}
|
|
html += '<div class="cb-q' + (flagged ? ' flag' : '') + '">'
|
|
+ '<div class="cb-q-top"><div><b>' + esc(q.prompt) + '</b>'
|
|
+ '<div style="color:var(--body-quiet-color,#888);font-size:11px;margin-top:2px">' + esc(q.type) + ' · #' + q.id + '</div></div>'
|
|
+ '<div class="cb-badges">' + badges
|
|
+ '<a class="cb-badge info" href="' + q.editUrl + '">ویرایش</a></div></div>'
|
|
+ '<ul class="cb-choices">'
|
|
+ q.choices.map((c) => '<li class="' + (c.isCorrect ? 'ok' : '') + '">' + esc(c.value) + '</li>').join('')
|
|
+ '</ul></div>';
|
|
});
|
|
box.innerHTML = html;
|
|
}
|
|
|
|
function reset(...cols) { cols.forEach((c) => setEmpty(el(c), '—')); el('questions').innerHTML = ''; }
|
|
|
|
el('col-grade').onclick = null;
|
|
el('col-grade').querySelectorAll('.cb-item').forEach((item) => {
|
|
item.onclick = async () => {
|
|
el('col-grade').querySelectorAll('.cb-item').forEach((n) => n.classList.remove('sel'));
|
|
item.classList.add('sel');
|
|
reset('col-node', 'col-lesson');
|
|
setEmpty(el('col-unit'), 'در حالِ بارگذاری…');
|
|
const data = await load('units/' + item.dataset.id + '/');
|
|
fill(el('col-unit'), data.results, 'node_count', async (unitId) => {
|
|
reset('col-lesson');
|
|
setEmpty(el('col-node'), 'در حالِ بارگذاری…');
|
|
const nodes = await load('nodes/' + unitId + '/');
|
|
fill(el('col-node'), nodes.results, 'lesson_count', async (nodeId) => {
|
|
setEmpty(el('col-lesson'), 'در حالِ بارگذاری…');
|
|
el('questions').innerHTML = '';
|
|
const lessons = await load('lessons/' + nodeId + '/');
|
|
fill(el('col-lesson'), lessons.results, 'question_count', async (lessonId) => {
|
|
el('questions').innerHTML = '<p style="color:var(--body-quiet-color,#999)">در حالِ بارگذاری…</p>';
|
|
renderQuestions(await load('questions/' + lessonId + '/'));
|
|
});
|
|
});
|
|
});
|
|
};
|
|
});
|
|
})();
|
|
</script>
|
|
{% endblock %}
|