Files
LMS/web/src/main/resources/templates/admin/lab-edit.html
Vladislav Kopnin d04207302a Phase 7: deployment, data import, UI hardening and security fixes
Docker/prod deployment:
- multi-stage Dockerfile (JDK21 build, JRE21 run), docker-compose.prod.yml,
  nginx config for lms.our-place.ru, deploy.sh; DEPLOY/IMPORT guides (UI flow)
- Fix Docker 401: LMS_JWT_JWK_SET_URI for academic/progress; forward-headers

Variant distribution (lab page only):
- round-robin by student level, shared random queue across selected groups,
  least-used variants, manual pins always preserved; occupied/dup labels
- setup page: single form + one «Сохранить всё», batch level/variant for
  selected students (checkboxes)

Grades, defense, materials:
- publication gate + comment lists; defense CSV import (2/3 cols, quotes)
- additional task after defense ("начать заново"); enroll/unenroll students
  from group page; temporary-password lock (no navigation until changed) and
  persistent "Смена пароля"; forced change once
- chapter page /learn/{c}; header: LMS home, Войти, user -> dashboard,
  compact theme toggle; responsive mobile header
- markdown: single newlines are soft breaks; highlight.js with light/dark themes
- lab/content imports ZIP-only (lab.yaml+README+variants/assets,
  chapters tree+assets); duplicate lab numbers rejected; variant ordering
  and date defaults honored

Fixes:
- 500 on student create (@MapsId set before persist); broken dialogs/JS refs in group page
2026-09-15 00:09:28 +03:00

117 lines
7.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
<head><title th:text="|Настройка: ${lab.title} — LMS|">Настройка лабы</title></head>
<body>
<div id="content">
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" class="nav-link">← К лабораторной</a>
<h1 class="page-title mt-4">Настройка лабораторной</h1>
<div th:if="${notice}" class="alert alert-success" th:text="${notice}"></div>
<div th:if="${error}" class="alert alert-error" th:text="${error}"></div>
<form class="form-stack" method="post" th:action="@{/admin/labs/{id}/edit(id=${lab.id})}">
<input type="hidden" name="_csrf" th:value="${csrfToken}">
<h2 class="section-title">Параметры</h2>
<div class="card form-card form-stack">
<div><label class="label" for="title">Название</label>
<input class="input" id="title" name="title" th:value="${lab.title}" required></div>
<div class="grid grid-2">
<div><label class="label" for="deadlineAt">Срок сдачи (сейчас: <span th:text="${deadlineMsk}" th:if="${deadlineMsk}">—</span> МСК)</label>
<input class="input" id="deadlineAt" name="deadlineAt" type="datetime-local" th:value="${deadlineLocal}"></div>
<div><label class="label" for="defenseAt">Дата защиты (сейчас: <span th:text="${defenseMsk}" th:if="${defenseMsk}">—</span> МСК)</label>
<input class="input" id="defenseAt" name="defenseAt" type="datetime-local" th:value="${defenseLocal}"></div>
</div>
<div class="grid grid-2">
<div><label class="label" for="pointsMax">Максимум баллов</label>
<input class="input" id="pointsMax" name="pointsMax" type="number" step="0.1" min="0" th:value="${lab.pointsMax}" required></div>
<div><label class="label" for="status">Статус</label>
<select class="input" id="status" name="status">
<option value="draft" th:selected="${lab.status == 'draft'}">draft (черновик)</option>
<option value="published" th:selected="${lab.status == 'published'}">published (опубликована)</option>
<option value="archived" th:selected="${lab.status == 'archived'}">archived</option>
</select></div>
</div>
<div><label class="label" for="descriptionMd">Условие (markdown)</label>
<textarea class="input" id="descriptionMd" name="descriptionMd" rows="14" th:text="${lab.descriptionMd}"></textarea></div>
</div>
<h2 class="section-title">Уровни</h2>
<div class="card card-flush">
<table>
<thead><tr><th class="th-w-sm">Код</th><th>Название</th><th></th></tr></thead>
<tbody>
<tr th:each="level : ${lab.levels}">
<td class="font-mono" th:text="${level.code}">basic</td>
<td>
<input type="hidden" name="levelIds" th:value="${level.id}">
<input class="input" name="levelTitles" th:value="${level.title}" required>
</td>
<td class="text-right">
<button class="btn btn-danger btn-sm" type="submit" name="action"
th:value="|delete-level-${level.id}|"
onclick="return confirm('Удалить уровень? Варианты на нём должны быть пустыми.')">
удалить</button>
</td>
</tr>
<tr th:if="${#lists.isEmpty(lab.levels)}"><td colspan="3" class="empty">Уровней нет — добавьте ниже.</td></tr>
</tbody>
</table>
</div>
<details class="card card-body mb-4">
<summary class="acc-head text-sm">+ Новый уровень</summary>
<div class="grid grid-2 mt-4">
<div><label class="label" for="nlCode">Код (латиницей)</label>
<input class="input" id="nlCode" name="newLevelCode" placeholder="pro"></div>
<div><label class="label" for="nlTitle">Название</label>
<input class="input" id="nlTitle" name="newLevelTitle" placeholder="Профессиональный"></div>
</div>
</details>
<h2 class="section-title">Варианты</h2>
<div th:if="${#lists.isEmpty(lab.levels)}" class="card card-body text-muted">Сначала создайте уровень — затем варианты.</div>
<div th:each="level : ${lab.levels}" class="mb-4">
<h3 class="group-title" th:text="|${level.title} (${#lists.size(level.variants)})|">Уровень</h3>
<div th:each="v : ${level.variants}" class="card form-card mb-4">
<input type="hidden" name="variantIds" th:value="${v.id}">
<input type="hidden" name="variantLevelIds" th:value="${level.id}">
<div class="grid grid-2">
<div><label class="label">Номер</label>
<input class="input" name="variantNumbers" th:value="${v.number}" required></div>
<div><label class="label">Название</label>
<input class="input" name="variantTitles" th:value="${v.title}"></div>
</div>
<div><label class="label">Условие (markdown)</label>
<textarea class="input" name="variantStatements" rows="6" th:text="${v.statementMd}"></textarea></div>
<button class="btn btn-danger btn-sm" type="submit" name="action"
th:value="|delete-variant-${v.id}|"
onclick="return confirm('Удалить вариант?')">Удалить вариант</button>
</div>
</div>
<details class="card card-body mb-4" th:if="${!#lists.isEmpty(lab.levels)}">
<summary class="acc-head text-sm">+ Добавить вариант</summary>
<div class="grid grid-2 mt-4">
<div><label class="label" for="avLevel">Уровень</label>
<select class="input" id="avLevel" name="addLevelId">
<option th:each="level : ${lab.levels}" th:value="${level.id}" th:text="${level.title}">—</option>
</select></div>
<div><label class="label" for="avNumber">Номер</label>
<input class="input" id="avNumber" name="addNumber" placeholder="21"></div>
</div>
<div><label class="label" for="avTitle">Название</label>
<input class="input" id="avTitle" name="addTitle"></div>
<div><label class="label" for="avStatement">Условие (markdown)</label>
<textarea class="input" id="avStatement" name="addStatementMd" rows="6"></textarea></div>
</details>
<div class="flex gap-2 mb-8">
<button class="btn btn-primary" type="submit" name="action" value="save">Сохранить всё</button>
<button class="btn btn-secondary" type="submit" name="action" value="add-level">Добавить уровень</button>
<button class="btn btn-secondary" type="submit" name="action" value="add-variant">Добавить вариант</button>
</div>
</form>
</div>
</body>
</html>