Lab assignment management UI and student variant view

- progress-service: POST /api/v1/assign (manual variant), GET /api/v1/assignments?labId=
  (enriched assignments), fixed enrich() to return real variant number/title/statement
- gateway: routes for /api/v1/assign and /api/v1/levels (exact paths)
- web: lab detail page shows students grouped with level and variant selects,
  group filter, distribute/redistribute buttons, source badge (auto/manual)
- web: student dashboard renders variant statement via libs/markdown (spoilers,
  code highlight, sanitized)
- academic-service: unquote YAML front-matter values (variant numbers were quoted)
- academic-service: LabLevelDetail.id for level references
This commit is contained in:
2026-09-10 23:25:36 +03:00
parent 84532adbff
commit 0de6d3f2d8
17 changed files with 408 additions and 67 deletions

View File

@@ -9,28 +9,23 @@
<div class="grid grid-2">
<form class="card form-card form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
<h2 class="section-title">Импорт лабораторной</h2>
<p class="text-sm text-muted">Принимаются .zip (lab.yaml + README.md + variants/*.md) или .json (старый формат).</p>
<p class="text-sm text-muted">Принимаются .zip (lab.yaml + README.md + variants/*.md) или .json.</p>
<div><label class="label" for="labFile">Файл</label><input class="input" id="labFile" name="file" type="file" accept=".zip,.json" required></div>
<button class="btn btn-primary" type="submit">Импортировать</button>
</form>
</div>
<div class="card card-flush mt-8">
<table>
<thead><tr><th>№</th><th>Название</th><th>Семестр</th><th>Вариантов</th><th>Статус</th><th></th></tr></thead>
<thead><tr><th>№</th><th>Название</th><th>Вариантов</th><th>Статус</th><th></th></tr></thead>
<tbody>
<tr th:each="lab : ${labs}">
<td th:text="${lab.number}">1</td>
<td th:text="${lab.title}">—</td>
<td class="text-muted" th:text="${lab.semesterTitle}">—</td>
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="${lab.title}">—</a></td>
<td th:text="${lab.variantCount}">0</td>
<td><span class="badge" th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'" th:text="${lab.status}">—</span></td>
<td class="text-center">
<form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post" onsubmit="return confirm('Удалить лабораторную ${lab.title}?')">
<button class="nav-link text-sm" type="submit">Удалить</button>
</form>
</td>
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post"><button class="nav-link text-sm" type="submit">Удалить</button></form></td>
</tr>
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="6" class="empty">Лабораторных пока нет.</td></tr>
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="5" class="empty">Лабораторных пока нет.</td></tr>
</tbody>
</table>
</div>