Defense: sync admin/student timers from server every 10s; toasts with ready reason; live eye counter
This commit is contained in:
@@ -260,11 +260,12 @@ function attachPublishToggles() {
|
||||
if (!urlEl) return;
|
||||
var csrfEl = document.querySelector('input[name="_csrf"]');
|
||||
document.querySelectorAll('.pub-toggle').forEach(function (cb) {
|
||||
var labId = cb.getAttribute('data-lab') || '';
|
||||
cb.addEventListener('change', function () {
|
||||
var desired = cb.checked;
|
||||
var body = new URLSearchParams();
|
||||
body.set('_csrf', csrfEl ? csrfEl.value : '');
|
||||
body.set('labId', cb.getAttribute('data-lab') || '');
|
||||
body.set('labId', labId);
|
||||
body.set('studentId', cb.getAttribute('data-student') || '');
|
||||
body.set('published', desired ? 'true' : 'false');
|
||||
cb.disabled = true;
|
||||
@@ -277,9 +278,14 @@ function attachPublishToggles() {
|
||||
.then(function (res) {
|
||||
cb.disabled = false;
|
||||
if (res && res.ok) {
|
||||
showToast(desired
|
||||
? 'Оценка и комментарии опубликованы студенту.'
|
||||
: 'Оценка и комментарии скрыты от студента.');
|
||||
showToast((desired ? 'Опубликовано: ' : 'Скрыто: ') + res.count + ' из ' + res.total);
|
||||
var eye = document.querySelector('.mini-btn.js-pub-eye[data-lab="' + labId + '"]');
|
||||
if (eye) {
|
||||
eye.setAttribute('data-count', String(res.count || 0));
|
||||
eye.setAttribute('data-total', String(res.total || 0));
|
||||
eye.setAttribute('data-all', res.all ? 'true' : 'false');
|
||||
updateEyeCounts();
|
||||
}
|
||||
} else {
|
||||
cb.checked = !desired;
|
||||
showToast((res && res.error) || 'Не удалось изменить публикацию.', 'error');
|
||||
@@ -294,19 +300,171 @@ function attachPublishToggles() {
|
||||
});
|
||||
}
|
||||
|
||||
/* Перерисовка «глазика» публикации по атрибутам data-count/data-total/data-all. */
|
||||
function updateEyeCounts() {
|
||||
document.querySelectorAll('.mini-btn.js-pub-eye').forEach(function (eye) {
|
||||
var total = parseInt(eye.getAttribute('data-total') || '0', 10);
|
||||
var count = parseInt(eye.getAttribute('data-count') || '0', 10);
|
||||
var all = eye.getAttribute('data-all') === 'true';
|
||||
eye.classList.remove('ok', 'partial', 'warn');
|
||||
eye.classList.add(all ? 'ok' : (count > 0 ? 'partial' : 'warn'));
|
||||
var on = eye.querySelector('.eye-open');
|
||||
var off = eye.querySelector('.eye-closed');
|
||||
if (on) on.style.display = count > 0 ? '' : 'none';
|
||||
if (off) off.style.display = count > 0 ? 'none' : '';
|
||||
var c = eye.querySelector('.js-pub-count');
|
||||
if (c) {
|
||||
c.textContent = count;
|
||||
c.style.display = (count > 0 && !all) ? '' : 'none';
|
||||
}
|
||||
eye.title = all
|
||||
? 'Оценки опубликованы всей группе — нажмите, чтобы скрыть у всех'
|
||||
: (count > 0
|
||||
? 'Опубликовано ' + count + ' из ' + total + ' — нажмите, чтобы опубликовать всем'
|
||||
: 'Показать оценки и комментарии всей группе');
|
||||
var form = eye.closest('form');
|
||||
var hidden = form ? form.querySelector('.js-pub-published') : null;
|
||||
if (hidden) hidden.value = all ? 'false' : 'true';
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- За Protection: синхронизация таймеров со статусами с сервером (10 с) ---------- */
|
||||
|
||||
var DEFENSE_STATE = {};
|
||||
var DEFENSE_POLL_MS = 10000;
|
||||
var DEFENSE_TICK_MS = 1000;
|
||||
|
||||
function defenseBtn(sessionId) {
|
||||
if (!sessionId) return null;
|
||||
return document.querySelector('.cell-btn.js-defense-cell[data-session="' + sessionId + '"]');
|
||||
}
|
||||
|
||||
function applyDefenseToCell(s) {
|
||||
var btn = defenseBtn(s.sessionId);
|
||||
if (!btn) return;
|
||||
btn.setAttribute('data-status', s.status || '');
|
||||
if (s.status === 'started' && s.endsAt) btn.setAttribute('data-seconds', String(Math.max(0, Math.ceil((s.endsAt - Date.now()) / 1000))));
|
||||
if (s.code) btn.setAttribute('data-code', s.code); else btn.removeAttribute('data-code');
|
||||
btn.classList.remove('ok', 'warn', 'dim');
|
||||
btn.classList.add(s.status === 'ready' ? 'ok' : (s.status === 'waiting' || s.status === 'started' ? 'warn' : 'dim'));
|
||||
var label = btn.querySelector('.js-def-label');
|
||||
var timer = btn.querySelector('.js-def-timer');
|
||||
var started = s.status === 'started';
|
||||
if (!timer && started) {
|
||||
timer = document.createElement('span');
|
||||
timer.className = 'defense-cell-timer js-def-timer';
|
||||
btn.appendChild(timer);
|
||||
}
|
||||
if (timer) {
|
||||
timer.hidden = !started;
|
||||
if (started) timer.textContent = fmtSeconds(Math.max(0, Math.ceil((s.endsAt - Date.now()) / 1000)));
|
||||
}
|
||||
if (label) {
|
||||
label.textContent = started ? ''
|
||||
: (s.status == null ? 'нет'
|
||||
: (s.status === 'ready' ? 'готов'
|
||||
: (s.code ? 'код' : 'ожид.')));
|
||||
}
|
||||
if (s.status === 'ready') {
|
||||
btn.title = 'Защита завершена: ' + (s.readyReason === 'timeout' ? 'время вышло' : 'студент нажал «Готов»');
|
||||
if (label) label.textContent = 'готов';
|
||||
}
|
||||
}
|
||||
|
||||
function syncDefenseStatuses(sessions, initial) {
|
||||
(sessions || []).forEach(function (s) {
|
||||
var prev = DEFENSE_STATE[s.sessionId];
|
||||
var st = {
|
||||
sessionId: s.sessionId,
|
||||
status: s.status,
|
||||
code: s.code,
|
||||
secondsLeft: s.secondsLeft,
|
||||
readyReason: s.readyReason,
|
||||
endsAt: s.status === 'started' ? Date.now() + Math.max(0, parseInt(s.secondsLeft || 0, 10) * 1000) : null
|
||||
};
|
||||
DEFENSE_STATE[s.sessionId] = st;
|
||||
applyDefenseToCell(st);
|
||||
|
||||
// Уведомление о переходе в «готов» (не при первой загрузке).
|
||||
if (!initial && prev && prev.status === 'started' && s.status === 'ready') {
|
||||
var btn = defenseBtn(s.sessionId);
|
||||
var who = (btn && btn.getAttribute('data-student-name')) || 'студент';
|
||||
var lab = (btn && btn.getAttribute('data-lab-name')) || 'лабораторная';
|
||||
var reason = s.readyReason === 'timeout' ? 'время вышло' : 'нажал «Готов»';
|
||||
showToast(who + ' — ' + lab + ': завершил защиту (' + reason + ').');
|
||||
}
|
||||
|
||||
// Обновляем открытый диалог, если он про эту сессию.
|
||||
var dlg = document.getElementById('modal-defense');
|
||||
if (dlg && dlg.open && DEFENSE_OPEN_SESSION === s.sessionId) {
|
||||
var statusEl = $id('defense-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = defenseStatusText(s.status, s.code,
|
||||
s.status === 'started' ? Math.max(0, Math.ceil((st.endsAt - Date.now()) / 1000)) : 0,
|
||||
s.readyReason);
|
||||
}
|
||||
var resetForm = $id('defense-reset-form');
|
||||
if (resetForm) resetForm.hidden = s.status !== 'ready';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startDefenseWatch() {
|
||||
var meta = $id('defense-meta');
|
||||
if (!meta) return;
|
||||
var labs = meta.getAttribute('data-labs') || '';
|
||||
if (!labs) return;
|
||||
var url = '/admin/defense-watch?labs=' + encodeURIComponent(labs);
|
||||
var first = true;
|
||||
|
||||
function poll() {
|
||||
fetch(url)
|
||||
.then(function (r) { return r.ok ? r.json() : { ok: false }; })
|
||||
.then(function (res) {
|
||||
if (res && res.ok) {
|
||||
syncDefenseStatuses(res.sessions, first);
|
||||
first = false;
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
|
||||
poll();
|
||||
setInterval(poll, DEFENSE_POLL_MS);
|
||||
setInterval(function () {
|
||||
Object.keys(DEFENSE_STATE).forEach(function (id) {
|
||||
var s = DEFENSE_STATE[id];
|
||||
if (s.status === 'started') applyDefenseToCell(s);
|
||||
});
|
||||
}, DEFENSE_TICK_MS);
|
||||
}
|
||||
|
||||
|
||||
/* ---------- Индивидуальное задание (защита) ---------- */
|
||||
function defenseStatusText(status, code, seconds) {
|
||||
|
||||
function defenseReasonText(reason) {
|
||||
if (reason === 'timeout') return 'время вышло';
|
||||
if (reason === 'button') return 'студент нажал «Готов»';
|
||||
return '';
|
||||
}
|
||||
|
||||
function defenseStatusText(status, code, seconds, reason) {
|
||||
if (status === 'waiting') {
|
||||
return code
|
||||
? 'Задание назначено, код выдан — ждём входа студента.'
|
||||
: 'Задание назначено, код ещё не выдан.';
|
||||
}
|
||||
if (status === 'started') return 'Студент выполняет работу, осталось ' + fmtSeconds(seconds) + '.';
|
||||
if (status === 'ready') return 'Студент готов к сдаче. Можно начать заново для дополнительного задания.';
|
||||
if (status === 'ready') {
|
||||
var why = defenseReasonText(reason);
|
||||
return 'Студент готов к сдаче' + (why ? ' (' + why + ')' : '') + '. Можно начать заново для дополнительного задания.';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** Открывает диалог задания; кнопка может не нести data-* (тогда это «назначить новое»). */
|
||||
var DEFENSE_OPEN_SESSION = '';
|
||||
|
||||
function openDefense(btn) {
|
||||
var form = $id('defense-form');
|
||||
if (!form) return;
|
||||
@@ -321,15 +479,17 @@ function openDefense(btn) {
|
||||
var status = btn.getAttribute('data-status') || '';
|
||||
var code = btn.getAttribute('data-code') || '';
|
||||
var seconds = parseInt(btn.getAttribute('data-seconds') || '0', 10);
|
||||
var reason = btn.getAttribute('data-reason') || '';
|
||||
var actions = $id('defense-actions');
|
||||
var statusEl = $id('defense-status');
|
||||
DEFENSE_OPEN_SESSION = session;
|
||||
if (session) {
|
||||
$id('defense-issue-form').action = '/admin/defenses/' + session + '/issue-code';
|
||||
$id('defense-reset-form').action = '/admin/defenses/' + session + '/reset';
|
||||
$id('defense-action-lab').value = lab;
|
||||
$id('defense-reset-lab').value = lab;
|
||||
$id('defense-reset-form').hidden = status !== 'ready';
|
||||
if (statusEl) statusEl.textContent = defenseStatusText(status, code, seconds);
|
||||
if (statusEl) statusEl.textContent = defenseStatusText(status, code, seconds, reason);
|
||||
actions.hidden = false;
|
||||
} else {
|
||||
if (statusEl) statusEl.textContent = '';
|
||||
@@ -427,20 +587,6 @@ function showVariantTask() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Таймеры защиты в ячейках ---------- */
|
||||
|
||||
function tickDefenseCells() {
|
||||
document.querySelectorAll('.defense-cell-timer').forEach(function (el) {
|
||||
var left = parseInt(el.getAttribute('data-seconds') || '0', 10);
|
||||
var tick = setInterval(function () {
|
||||
left -= 1;
|
||||
if (left <= 0) { clearInterval(tick); el.textContent = '0:00'; return; }
|
||||
el.textContent = fmtSeconds(left);
|
||||
}, 1000);
|
||||
el.textContent = fmtSeconds(left);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- Матрица: фокус на лабораторной ---------- */
|
||||
|
||||
function focusLab() {
|
||||
@@ -458,6 +604,6 @@ document.addEventListener('submit', function (ev) {
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
focusLab();
|
||||
tickDefenseCells();
|
||||
startDefenseWatch();
|
||||
attachPublishToggles();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user