Phase 3: individual defense assignments with invite codes
- progress V5 defense_sessions: per student+lab individual task (md), status machine waiting -> started -> ready, one-time 6-digit codes - 30-min timer with server-side lazy expiry (syncTimeouts: started->ready by timeout), countdown JS on student page - admin: assign task dialog (+prefill existing via data-md), CSV import (studentNo;task, \n for newlines) resolved via identity students, issue/reset code buttons, defense status column - student: code entry form (code hidden until start), assignment md, 'Готов к сдаче', waiting-for-teacher note - gateway: /api/v1/defenses,/api/v1/defenses/** routes - web api client + admin/student controllers wired
This commit is contained in:
@@ -28,7 +28,50 @@
|
||||
<p class="text-muted">Ваш вариант ещё не распределён. Дождитесь, пока преподаватель распределит варианты.</p>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Сдача работы</h2>
|
||||
|
||||
<h2 class="section-title">Защита лабораторной</h2>
|
||||
<div class="card card-body form-stack">
|
||||
<div th:if="${defense == null}" class="text-muted">
|
||||
Индивидуальное задание для защиты ещё не назначено. Преподаватель сообщит, когда можно приходить.
|
||||
</div>
|
||||
<th:block th:if="${defense != null and defense.status == 'waiting'}">
|
||||
<p class="text-sm">Подойдите к преподавателю: он выдаст вам одноразовый код для начала защиты.</p>
|
||||
<form class="inline-form" th:action="@{/student/defenses/start}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="labId" th:value="${lab.id}">
|
||||
<input class="input input-sm" name="code" placeholder="6-значный код" maxlength="6" pattern="[0-9]{6}" required>
|
||||
<button class="btn btn-primary" type="submit">Приступить к защите</button>
|
||||
</form>
|
||||
</th:block>
|
||||
<div th:if="${defense != null and defense.status == 'started'}">
|
||||
<p class="text-sm defense-timer" th:data-seconds="${defense.secondsLeft}" data-labid="${lab.id}"></p>
|
||||
<h3 class="group-title">Ваше индивидуальное задание</h3>
|
||||
<div class="md-body" th:utext="${defense.assignmentHtml}"></div>
|
||||
<form class="mt-4" th:action="'/student/defenses/' + ${defense.id} + '/finish?labId=' + ${lab.id}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-primary" type="submit">Готов к сдаче</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.querySelectorAll('.defense-timer').forEach(function (el) {
|
||||
var left = parseInt(el.getAttribute('data-seconds') || '0', 10);
|
||||
var tick = setInterval(function () {
|
||||
left -= 1;
|
||||
if (left <= 0) { clearInterval(tick); window.location.reload(); return; }
|
||||
var m = Math.floor(left / 60), s = left % 60;
|
||||
el.textContent = 'Осталось времени: ' + m + ':' + (s < 10 ? '0' : '') + s;
|
||||
}, 1000);
|
||||
var m = Math.floor(left / 60), s = left % 60;
|
||||
el.textContent = 'Осталось времени: ' + m + ':' + (s < 10 ? '0' : '') + s;
|
||||
});
|
||||
</script>
|
||||
<div th:if="${defense != null and defense.status == 'ready'}">
|
||||
<p class="badge badge-ok defense-note">Завершено (<span th:text="${defense.readyReason == 'timeout' ? 'истекло время' : 'по кнопке'}"></span>).</p>
|
||||
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Сдача работы</h2>
|
||||
<div class="card form-card form-stack mb-4">
|
||||
<form th:action="@{/student/labs/{id}/submit(id=${lab.id})}" method="post" enctype="multipart/form-data" class="form-stack">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
@@ -56,6 +99,5 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</div></body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user