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
This commit is contained in:
@@ -1,85 +1,116 @@
|
||||
<!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>
|
||||
<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>
|
||||
<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="card form-card form-stack" th:action="@{/admin/labs/{id}/edit(id=${lab.id})}" method="post">
|
||||
<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">Срок сдачи</label>
|
||||
<input class="input" id="deadlineAt" name="deadlineAt" type="datetime-local"
|
||||
th:value="${deadlineLocal}"></div>
|
||||
<div><label class="label" for="defenseAt">Дата защиты</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>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<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 mt-8">Варианты</h2>
|
||||
<div th:each="level : ${lab.levels}" class="mb-6">
|
||||
<h3 class="group-title" th:text="${level.title}">Уровень</h3>
|
||||
<div th:each="v : ${level.variants}" class="card form-card form-stack mb-4">
|
||||
<form th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" method="post">
|
||||
<input type="hidden" name="variantId" th:value="${v.id}">
|
||||
<input type="hidden" name="labLevelId" th:value="${level.id}">
|
||||
<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="number" th:value="${v.number}" required></div>
|
||||
<div><label class="label">Название</label><input class="input" name="title" th:value="${v.title}"></div>
|
||||
<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="statementMd" rows="6" th:text="${v.statementMd}"></textarea></div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</div>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<form th:action="@{/admin/labs/{labId}/variants/{vid}/delete(labId=${lab.id}, vid=${v.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить вариант?')">
|
||||
<button class="btn btn-secondary btn-sm" type="submit">Удалить вариант</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title mt-8">Добавить вариант</h2>
|
||||
<form class="card form-card form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" method="post">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="newLevel">Уровень</label>
|
||||
<select class="input" id="newLevel" name="labLevelId">
|
||||
<option th:each="level : ${lab.levels}" th:value="${level.id}" th:text="${level.title}">—</option>
|
||||
</select></div>
|
||||
<div><label class="label" for="newNumber">Номер</label>
|
||||
<input class="input" id="newNumber" name="number" required></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>
|
||||
<div><label class="label" for="newTitle">Название</label>
|
||||
<input class="input" id="newTitle" name="title"></div>
|
||||
<div><label class="label" for="newStatement">Условие (markdown)</label>
|
||||
<textarea class="input" id="newStatement" name="statementMd" rows="6"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Добавить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user