Phase 6: accordion in /learn; README feature notes; tests green
- collapse chapters/sections via styled <details> with chevrons - identity testcontainers smoke passes on JDK 21 - README: student UX, comments, JDK note (logback ThrowableProxy in fat-jars → restart recovery), data dirs
This commit is contained in:
24
README.md
24
README.md
@@ -59,10 +59,28 @@ progress `:8083`, web `:8090`. Интерфейс: `http://localhost:8090`.
|
||||
markdown (код, спойлеры, картинки; санитизация OWASP); импорт ZIP
|
||||
(`chapters/N-глава/N-раздел/N-статья.md` + `assets/`); создание/переименование/
|
||||
удаление глав, разделов, статей; перенос статей между разделами.
|
||||
- **Студент**: видит опубликованные лабы, свой вариант с условием (markdown),
|
||||
сроки сдачи/защиты и оценки.
|
||||
- **Студент**: дашборд успеваемости (сдано, средний балл, лучшая/слабая оценка),
|
||||
карточки опубликованных лаб, страница лабы: условие, свой вариант, сдача
|
||||
файлов (zip/.c, переотправка до дедлайна, история, скачивание последней),
|
||||
комментарий преподавателя к оценке, защита по одноразовому коду (30 мин).
|
||||
- **Комментарии к оценкам**: тональность (pos/neu/neg), хранятся в БД,
|
||||
автоподсказки при вводе ранжируются по частоте использования; видны студенту чипсами.
|
||||
- UI: светлая/тёмная тема (сохраняется в localStorage), модальные окна вместо
|
||||
отдельных страниц, фавиконка, адаптивная вёрстка без inline-стилей.
|
||||
отдельных страниц, аккордеоны глав/разделов в `/learn`, фавиконка,
|
||||
адаптивная вёрстка без inline-стилей.
|
||||
|
||||
## Примечания
|
||||
|
||||
- **JDK 21 обязателен для runtime**. В связке Spring Boot 4.0.x + fat-jar logback
|
||||
не находит `ThrowableProxy`, и это падает при любом исключении в потоке ответа.
|
||||
Если после деплоя сервис вдруг «висит» на страницах (а `/actuator/health` даёт 200) —
|
||||
лечится перезапуском (`./run.sh restart`).
|
||||
- Даты хранятся в UTC (instant), а в UI конвертируются в Europe/Moscow
|
||||
(`DateUtils`); формы datetime-local отправляются в московском времени.
|
||||
- Капча/rate-limit на входе отсутствует — для учебного стендома оставлено;
|
||||
пароли студентов показываются один раз при выдаче.
|
||||
- `lms.submissions.dir` (default ./data/submissions) и `lms.content.assets-dir`
|
||||
держите на постоянном томе при деплое.
|
||||
|
||||
## Сборка и тесты
|
||||
|
||||
|
||||
@@ -231,3 +231,16 @@ code.code { padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-family: monos
|
||||
.chip-neg { background: #b3283b; color: #fff; }
|
||||
|
||||
/* Defense timer */
|
||||
/* Accordion (главы/разделы в /learn) */
|
||||
details.accordion > summary.acc-head { list-style: none; cursor: pointer; display: flex; align-items: center; justify-content: space-between; }
|
||||
details.accordion > summary::-webkit-details-marker { display: none; }
|
||||
details.accordion[open] > .acc-head, details.accordion > summary { margin-bottom: 0; }
|
||||
.acc-arrow { width: 10px; height: 10px; border-right: 2px solid var(--c-text-muted); border-bottom: 2px solid var(--c-text-muted); transform: rotate(45deg); transition: transform 0.2s; margin-left: .75rem;}
|
||||
details.accordion[open] > summary > .acc-arrow { transform: rotate(-135deg); }
|
||||
.acc-title { margin: 0; font-size: 1.0625rem; }
|
||||
.acc-body { border-top: 1px solid var(--c-border); }
|
||||
details.sub-acc > summary.sub-head { list-style: none; cursor: pointer; }
|
||||
details.sub-acc > summary::-webkit-details-marker { display: none; }
|
||||
.sub-arrow { display: inline-block; width: 8px; height: 8px; border-right: 2px solid var(--c-text-muted); border-bottom: 2px solid var(--c-text-muted); transform: rotate(45deg); transition: transform 0.2s; margin-right: 6px; vertical-align: 2px;}
|
||||
details.sub-acc[open] > summary .sub-arrow { transform: rotate(-135deg); }
|
||||
.sub-actions { margin: 0 0 2px 18px; }
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
Материалы ещё не добавлены.
|
||||
</div>
|
||||
|
||||
<div th:each="chapter : ${tree?.chapters}" class="card mb-4">
|
||||
<div class="card-body">
|
||||
<details th:each="chapter : ${tree?.chapters}" class="card mb-4 accordion" open>
|
||||
<summary class="card-body acc-head">
|
||||
<h2 class="section-title acc-title" th:text="${chapter.title}">Глава</h2>
|
||||
<span class="acc-arrow" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="card-body acc-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||
<h2 class="section-title" th:text="${chapter.title}">Глава</h2>
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2">
|
||||
@@ -35,20 +39,24 @@
|
||||
|
||||
<p th:if="${#lists.isEmpty(chapter.sections)}" class="text-sm text-muted">Разделов пока нет</p>
|
||||
|
||||
<div th:each="section : ${chapter.sections}" class="mt-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h3 class="group-title">
|
||||
<details th:each="section : ${chapter.sections}" class="mt-4 sub-acc" open>
|
||||
<summary class="sub-head">
|
||||
<h3 class="group-title m-0">
|
||||
<span class="sub-arrow" aria-hidden="true"></span>
|
||||
<a th:href="@{/learn/{c}/{s}(c=${chapter.slug}, s=${section.slug})}" th:text="${section.title}">Раздел</a>
|
||||
</h3>
|
||||
</summary>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 sub-actions">
|
||||
<div class="spacer"></div>
|
||||
<div th:if="${currentUser != null and currentUser.admin}" class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:data-modal="|modal-rename-section-${section.id}|" onclick="openModalData(this)">Переименовать</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:data-modal="|modal-move-${section.id}|" onclick="openModalData(this)">Перенести статьи</button>
|
||||
<form th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
|
||||
<form class="inline-form" th:action="@{/admin/content/sections/{id}/delete(id=${section.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить раздел со всеми статьями?')">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-danger btn-sm" type="submit">Удалить</button>
|
||||
<button class="btn btn-danger btn-sm" type="submit">Удалить раздел</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,8 +113,7 @@
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
|
||||
</details>
|
||||
<th:block th:if="${currentUser != null and currentUser.admin}">
|
||||
<dialog th:id="|modal-rename-chapter-${chapter.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
@@ -125,7 +132,7 @@
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<dialog id="modal-import" class="modal">
|
||||
<div class="modal-header">
|
||||
|
||||
Reference in New Issue
Block a user