Phase 2+5: lab submissions and student lab flow
- progress: submissions table+V4; SubmissionService (deadline-gated submit,
unlimited resubmits, per-student history, zip download); files on disk
data/submissions/{student}/{lab}/{seq}/
- student API: POST submit (multipart), GET me submissions, download; admin
GET lab submissions + admin download (zip pack)
- progress enrich(): labId in MeAssignment; web DTO aligned
- web: /student dashboard (avg/best/worst cards), /student/labs cards,
/student/labs/{id}: condition md, variant block, submit form, history;
'Лабораторные' link in nav for students
- admin: submissions column on lab assignment table + download link
- fixed long-standing template bug: broken @{} expression (missing }) in
admin/lab and student/lab
This commit is contained in:
@@ -10,6 +10,10 @@ server:
|
||||
spring:
|
||||
application:
|
||||
name: web
|
||||
servlet:
|
||||
multipart:
|
||||
max-file-size: 30MB
|
||||
max-request-size: 120MB
|
||||
thymeleaf:
|
||||
cache: false
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<h3 class="group-title" th:text="${g.groupName}">Группа</h3>
|
||||
<div class="card card-flush mb-6">
|
||||
<table>
|
||||
<thead><tr><th>Студент</th><th>Логин</th><th>Уровень</th><th>Вариант</th><th>Источник</th></tr></thead>
|
||||
<thead><tr><th>Студент</th><th>Логин</th><th>Уровень</th><th>Вариант</th><th>Источник</th><th>Сдача</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="s : ${g.students}">
|
||||
<td th:text="${s.fullName}">—</td>
|
||||
@@ -130,8 +130,12 @@
|
||||
</td>
|
||||
<td><span th:if="${s.variantNumber != null}" class="font-mono" th:text="${s.variantNumber}">—</span></td>
|
||||
<td><span class="badge" th:classappend="${s.source == 'вручную'} ? 'badge-warn' : 'badge-ok'" th:text="${s.source}">—</span></td>
|
||||
<td><a th:if="${s.submissionId}" class="badge badge-ok"
|
||||
th:href="'/admin/submissions/' + ${s.submissionId} + '/download'"
|
||||
th:text="|попытка ${s.submissionSeq}|">↓</a>
|
||||
<span th:unless="${s.submissionId}" class="text-muted">не сдано</span></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(g.students)}"><td colspan="6" class="empty">В группе нет студентов.</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(g.students)}"><td colspan="7" class="empty">В группе нет студентов.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<th:block th:if="${currentUser != null}">
|
||||
<a th:if="${currentUser.admin}" th:href="@{/admin/groups}" class="nav-link">Группы</a>
|
||||
<a th:if="${currentUser.admin}" th:href="@{/admin/labs}" class="nav-link">Лабораторные</a>
|
||||
<a th:unless="${currentUser.admin}" th:href="@{/student/labs}" class="nav-link">Лабораторные</a>
|
||||
<span class="nav-sep" th:text="${currentUser.name}">—</span>
|
||||
</th:block>
|
||||
<button onclick="toggleTheme()" class="btn btn-secondary btn-sm">☀/🌙</button>
|
||||
|
||||
@@ -4,27 +4,44 @@
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1 class="page-title">Здравствуйте, <span th:text="${currentUser.name}">студент</span></h1>
|
||||
<p class="text-sm text-muted mb-6">Ваши лабораторные работы</p>
|
||||
|
||||
<div th:if="${#lists.isEmpty(assignments)}" class="card card-body text-muted">
|
||||
Пока нет назначенных лабораторных. Ожидайте, пока преподаватель распределит варианты.
|
||||
<div class="grid grid-stats">
|
||||
<div class="card card-body"><p class="stat-title">Сдано работ</p>
|
||||
<p class="stat-value" th:text="${submittedCount}">0</p></div>
|
||||
<div class="card card-body"><p class="stat-title">Средний балл</p>
|
||||
<p class="stat-value" th:if="${average != null}" th:text="${average}">—</p>
|
||||
<p class="stat-value text-muted" th:if="${average == null}">—</p></div>
|
||||
<div class="card card-body"><p class="stat-title">Лучшая оценка</p>
|
||||
<p class="stat-value" th:if="${best != null}" th:text="${best.toPlainString()}">—</p>
|
||||
<p class="stat-value text-muted" th:if="${best == null}">—</p></div>
|
||||
<div class="card card-body"><p class="stat-title">Слабое место</p>
|
||||
<p class="stat-value" th:if="${worst != null}" th:text="${worst.toPlainString()}">—</p>
|
||||
<p class="stat-value text-muted" th:if="${worst == null}">—</p></div>
|
||||
</div>
|
||||
|
||||
<div th:each="a : ${assignments}" class="card mb-6">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h2 class="section-title" th:text="|ЛР ${a.labNumber}. ${a.labTitle}|">ЛР</h2>
|
||||
<div class="flex gap-2 items-center">
|
||||
<span class="badge badge-ok">Вариант <span class="font-mono" th:text="${a.variantNumber}">—</span></span>
|
||||
<span th:if="${a.levelTitle}" class="badge badge-warn" th:text="${a.levelTitle}">—</span>
|
||||
<div class="flex flex-wrap gap-2 mt-8">
|
||||
<a class="btn btn-primary" th:href="@{/student/labs}">Лабораторные</a>
|
||||
<a class="btn btn-secondary" th:href="@{/learn}">Учебные материалы</a>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title mt-8">Мои лабораторные</h2>
|
||||
<div class="grid grid-2">
|
||||
<div th:each="c : ${cards}" class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="section-title">
|
||||
<a th:href="@{/student/labs/{id}(id=${c.id})}" th:text="|Лабораторная работа ${c.number}|">ЛР</a>
|
||||
</h3>
|
||||
<p class="text-sm" th:text="${c.title}">—</p>
|
||||
<div class="flex flex-wrap gap-2 mt-2 text-sm">
|
||||
<span th:if="${c.variantNumber}" class="badge badge-warn">вариант <span class="font-mono" th:text="${c.variantNumber}"></span></span>
|
||||
<span th:if="${c.submissionSeq}" class="badge badge-ok">сдано (попытка <span th:text="${c.submissionSeq}">1</span>)</span>
|
||||
<span th:unless="${c.submissionSeq}" class="badge badge-warn">не сдано</span>
|
||||
<span th:if="${c.grade}" class="badge badge-ok">оценка <span th:text="${c.grade}"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted" th:if="${a.variantTitle}" th:text="${a.variantTitle}">—</p>
|
||||
<p class="text-sm text-muted" th:if="${a.deadlineMsk}" th:text="|Срок сдачи: ${a.deadlineMsk} (МСК)|">—</p>
|
||||
<p class="text-sm text-muted" th:if="${a.defenseMsk}" th:text="|Защита: ${a.defenseMsk} (МСК)|">—</p>
|
||||
<div class="md-body mt-4" th:utext="${a.statementHtml}"></div>
|
||||
</div>
|
||||
<div th:if="${#lists.isEmpty(cards)}" class="card card-body text-muted">Лабораторных пока нет.</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
61
web/src/main/resources/templates/student/lab.html
Normal file
61
web/src/main/resources/templates/student/lab.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
|
||||
<head><title th:text="|ЛР ${lab.number} — LMS|">Лабораторная</title></head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<a th:href="@{/student/labs}" class="nav-link">← Лабораторные</a>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mt-4 mb-2">
|
||||
<h1 class="page-title" th:text="|Лабораторная работа ${lab.number}. ${lab.title}|">ЛР</h1>
|
||||
<div class="flex gap-2 text-sm">
|
||||
<span th:if="${grade != null}" class="badge badge-ok">Оценка: <span th:text="${grade}"></span></span>
|
||||
<span th:if="${assignment != null}" class="badge badge-warn">Вариант <span class="font-mono" th:text="${assignment.variantNumber}"></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted mb-6" th:if="${deadlineMsk}">Срок сдачи: <span th:text="${deadlineMsk}"></span> МСК</p>
|
||||
|
||||
<div th:if="${notice}" class="alert alert-success" th:text="${notice}"></div>
|
||||
<div th:if="${error}" class="alert alert-error" th:text="${error}"></div>
|
||||
|
||||
<h2 class="section-title">Условие лабораторной</h2>
|
||||
<div class="card card-body md-body mb-6" th:utext="${descriptionHtml}"></div>
|
||||
|
||||
<th:block th:if="${assignment != null}">
|
||||
<h2 class="section-title">Ваш вариант <span class="font-mono" th:text="${assignment.variantNumber}"></span>
|
||||
<span class="text-muted text-sm" th:if="${assignment.levelTitle}" th:text="|(${assignment.levelTitle})|"></span></h2>
|
||||
<div class="card card-body md-body mb-6" th:utext="${statementHtml}"></div>
|
||||
</th:block>
|
||||
<div th:if="${assignment == null}" class="card card-body mb-6">
|
||||
<p class="text-muted">Ваш вариант ещё не распределён. Дождитесь, пока преподаватель распределит варианты.</p>
|
||||
</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}">
|
||||
<p class="text-sm text-muted">zip‑архив или исходники (.c/.h). Пока не истёк срок, можно пересывать столько раз, сколько нужно — засчитывается последняя попытка.</p>
|
||||
<div><input class="input" name="files" type="file" multiple required></div>
|
||||
<div th:if="${canSubmit}">
|
||||
<button class="btn btn-primary" type="submit">Отправить решение</button>
|
||||
</div>
|
||||
<div th:unless="${canSubmit}" class="alert alert-warning">Срок сдачи истёк — отправка закрыта.</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3 class="group-title">История сдач</h3>
|
||||
<div class="card card-flush">
|
||||
<table>
|
||||
<thead><tr><th>Попытка</th><th>Когда (МСК)</th><th>Файлы</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="s : ${submissions}">
|
||||
<td th:text="${s.seq}">1</td>
|
||||
<td class="text-muted" th:text="${#strings.replace(s.createdAt, 'T', ' ')}"></td>
|
||||
<td class="font-mono text-sm"><span th:each="f : ${s.files}" th:text="${f}"></span></td>
|
||||
<td><a class="btn btn-secondary btn-sm" th:href="'/student/submissions/' + ${s.id} + '/download'">Скачать</a></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(submissions)}"><td colspan="4" class="empty">Вы ещё ничего не сдавали.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
web/src/main/resources/templates/student/labs.html
Normal file
26
web/src/main/resources/templates/student/labs.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
|
||||
<head><title>Лабораторные — LMS</title></head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1 class="page-title">Лабораторные работы</h1>
|
||||
<div class="grid grid-2">
|
||||
<div th:each="c : ${cards}" class="card">
|
||||
<div class="card-body">
|
||||
<h2 class="section-title">
|
||||
<a th:href="@{/student/labs/{id}(id=${c.id})}" th:text="|Лабораторная работа ${c.number}. ${c.title}|">ЛР</a>
|
||||
</h2>
|
||||
<div class="flex flex-wrap gap-2 text-sm mt-2">
|
||||
<span th:if="${c.variantNumber}" class="badge badge-warn">Вариант <span class="font-mono" th:text="${c.variantNumber}"></span></span>
|
||||
<span th:if="${c.submissionSeq}" class="badge badge-ok">сдано (попытка <span th:text="${c.submissionSeq}"></span>)</span>
|
||||
<span th:unless="${c.submissionSeq}" class="badge badge-warn">не сдано</span>
|
||||
<span th:if="${c.grade}" class="badge badge-ok">оценка <span th:text="${c.grade}"></span></span>
|
||||
</div>
|
||||
<p class="text-sm text-muted mt-2" th:if="${c.deadlineMsk}" th:text="|Срок сдачи: ${c.deadlineMsk} МСК|">—</p>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${#lists.isEmpty(cards)}" class="card card-body text-muted">Опубликованных лабораторных пока нет.</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user