From 6f8add3736df1f9afd59f4af90299adccbf26694 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Tue, 27 Oct 2026 15:37:56 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=8D=D1=82=D0=B0=D0=BF=202=20?= =?UTF-8?q?=E2=80=94=20=D0=BA=D1=83=D1=80=D1=81=D1=8B,=20bootstrap=20?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=B0=D0=B1=D0=BE=D1=80=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++- deploy/IMPORT.md | 40 +++++++-- docs/07-groups-courses.md | 14 ++- docs/08-groups-courses-plan.md | 16 ++-- docs/10-stage0-report.md | 5 +- docs/12-stage2-report.md | 150 +++++++++++++++++++++++++++++++++ docs/README.md | 1 + 7 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 docs/12-stage2-report.md diff --git a/README.md b/README.md index 9225c24..8267943 100644 --- a/README.md +++ b/README.md @@ -84,9 +84,10 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD ## Наполнение данными -Порядок ввода данных (всё через админку): `/setup` → администратор; импорт групп -(`.xls`, логин = студенческий номер, временный пароль со сменой при первом входе); -импорт лабораторной ZIP'ом (`lab.yaml` + `README.md` + `variants/*.md`); «Распределить» +Порядок ввода данных (всё через админку): `/setup` → администратор; курс +(bootstrap или «+ Новый курс»); импорт групп (`.xls`, логин = студенческий номер, +временный пароль со сменой при первом входе); импорт лабораторной ZIP'ом с +выбором курса (`lab.yaml` + `README.md` + `variants/*.md`); «Распределить» — случайная раздача по кругам в рамках уровня; оценки/защита — на странице группы; учебные материалы — статьи или ZIP (`chapters/…`, доступны гостям). @@ -105,6 +106,11 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD ручное добавление студента (генерация пароля) и «Добавить существующего» (перевод из другой группы), «Отчислить из группы», выгрузка логинов XLSX/CSV, сброс пароля, история членства. +- **Курсы**: админ-страницы курсов (`/admin/courses`), номер `course_no` для URL, + назначение преподавателей и привязка групп с историей; bootstrap переносит + существующие лабораторные, главы и активные группы в курс «Промышленное + программирование на языке C». Лабораторные привязаны к курсу: создание + и импорт требуют выбора курса, лабу можно скопировать в другой курс. - **Лабораторные**: импорт ZIP (`lab.yaml` + `README.md` + `variants/*.md` + `assets/`); создание через UI с произвольным числом уровней; единая форма настроек с одной кнопкой «Сохранить всё»; статусы draft/published diff --git a/deploy/IMPORT.md b/deploy/IMPORT.md index 819b8b8..61967dd 100644 --- a/deploy/IMPORT.md +++ b/deploy/IMPORT.md @@ -1,7 +1,7 @@ # Порядок наполнения LMS данными (через интерфейс) -Всё делается в админке, порядок: администратор → группы и студенты → лабораторная → -раздача вариантов → защита → материалы. +Всё делается в админке, порядок: администратор → курс (bootstrap) → группы +и студенты → лабораторная → раздача вариантов → защита → материалы. ## Шаг 1. Деплой @@ -9,7 +9,25 @@ откройте домен — попадёте на `/setup`, создайте администратора (логин и пароль храните в секрете, в репозиторий их не писать). -## Шаг 2. Группы и студенты +## Шаг 2. Курс + +Откройте **Курсы** (`/admin/courses`). Все лабораторные принадлежат курсу, +поэтому курс должен существовать до импорта лабораторных. + +* **Существующая база:** если курсов ещё нет, нажмите «Создать курс и перенести + данные». Bootstrap создаст курс «Промышленное программирование на языке C», + привяжет к нему все активные группы и перенесёт существующие лабораторные + и главы. Повторный запуск безопасен и не создаёт дублей. +* **Новая база:** создайте курс кнопкой «+ Новый курс» (номер присвоится + автоматически) либо тем же bootstrap. +* После импорта групп (шаг 3) привяжите их к курсу на странице курса, если их + не было в момент bootstrap: блок «Привязать группу». Там же назначаются + преподаватели — по UUID аккаунта со страницы «Пользователи». + +Архивированный курс нельзя выбрать для новой лабораторной или копирования. +Без курса создание и импорт лабораторной недоступны: в форме нужно выбрать курс. + +## Шаг 3. Группы и студенты **Группы → Импорт** — загрузите `.xls`/`.xlsx`/`.csv` со списком группы. Обязательные столбцы: @@ -35,9 +53,10 @@ Выгрузить логины: **«Логины XLSX» / «CSV»**. -## Шаг 3. Лабораторная +## Шаг 4. Лабораторная -**Лабораторные → Импорт** — ZIP со структурой: +**Лабораторные → Импорт** — сначала выберите **курс** (обязательно), затем +загрузите ZIP со структурой: ```text lab.yaml # номер, название, points_max, уровни (code/title), сроки, статус @@ -49,9 +68,12 @@ variants/01.md # вариант: front-matter (level, number, title) + тек странице лабораторной: * «Настроить» — срок сдачи, дата защиты, максимум баллов, статус; -* уровни/варианты можно править и добавлять прямо там же. +* уровни/варианты можно править и добавлять прямо там же; +* «Копировать в курс» — создать копию лабораторной (с уровнями и вариантами) + в другом курсе; номер и название можно изменить. Прогресс студентов не + копируется, копия создаётся черновиком. -## Шаг 4. Раздача вариантов +## Шаг 5. Раздача вариантов На странице лабораторной — **«Распределить»**: выбрать группы, при необходимости «пересоздать заново». Логика: случайная очередь по всем вариантам выбранного @@ -59,7 +81,7 @@ variants/01.md # вариант: front-matter (level, number, title) + тек вручную не трогаются. Частично/точно — в списке назначений (уровень и вариант по каждому студенту) или массово чекбоксами. -## Шаг 5. Оценки и защита +## Шаг 6. Оценки и защита На странице группы (матрица): @@ -69,7 +91,7 @@ variants/01.md # вариант: front-matter (level, number, title) + тек (`студент;ЛР;задание` или `студент;задание` при выбранной ЛР), «выдать код», «изменить», «текст», после готовности — «доп. задание: начать заново». -## Шаг 6. Учебные материалы +## Шаг 7. Учебные материалы **Лабораторные → материалы (`/learn`)** доступны гостям. Для наполнения: создание глав/разделов/статей в админке или **импорт ZIP** со структурой: diff --git a/docs/07-groups-courses.md b/docs/07-groups-courses.md index bb6d2ae..069e59e 100644 --- a/docs/07-groups-courses.md +++ b/docs/07-groups-courses.md @@ -272,10 +272,18 @@ CREATE UNIQUE INDEX chapters_slug_course_uidx ON chapters (course_id, slug) WHER Признак видимости актив наследует от статьи→раздела→главы (course/audience). Требуется проверка доступа в маршруте выдачи активов (см. §6). Отдельных колонок достаточно опционально; минимум — корректная авторизация. -**Post-bootstrap шаг academic (условный «V10», не файл `db/migration` и вне обычной цепочки Flyway) — курс C: завершение привязки лаб** +**V10 — уникальное название курса (реальный файл `V10__course_title_unique.sql`)** ```sql --- Выполняется после bootstrap-сервиса, когда все лабы получили course_id. --- Это отдельный шаг/миграция: обычный Flyway-старт применяет миграции раньше +-- Защита от дублей курса при конкурентном bootstrap и повторном создании. +CREATE UNIQUE INDEX courses_title_lower_uidx ON courses (lower(title)); +``` +Сервис дополнительно проверяет название без учёта регистра и возвращает `409` +на дубликат; индекс закрывает гонку между проверкой и вставкой. + +**Post-bootstrap шаг academic (вне Flyway) — курс C: завершение привязки лаб** +```sql +-- Выполняется сервисом CourseBootstrapService после переноса данных, когда все +-- лабы получили course_id. Обычный Flyway-старт применяет миграции раньше -- бизнес-логики и не может дождаться создания курса и назначения владельца. ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL; ``` diff --git a/docs/08-groups-courses-plan.md b/docs/08-groups-courses-plan.md index 58cfe7d..089c69f 100644 --- a/docs/08-groups-courses-plan.md +++ b/docs/08-groups-courses-plan.md @@ -201,13 +201,13 @@ ## 4. Ориентировочные миграции Номера уточняются при реализации, чтобы не конфликтовать с максимумами master -**до этой ветки** (identity V3, academic V5, progress V12); максимумы этой ветки -больше. +(identity V3, **academic V5**, progress V12). Максимумы этой ветки: +identity V9, **academic V10**, progress V13 (+V14 при необходимости аудита). | Сервис | Миграции | Содержание | |---|---|---| | identity | V4–V9 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status` | -| academic | V6–V9 + post-bootstrap V10 | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `SET NOT NULL` — отдельный шаг после bootstrap | +| academic | V6–V10 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `SET NOT NULL` — post-bootstrap шаг вне Flyway | | progress | V13 (+ V14 при необходимости аудита) | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит скачиваний — этап 6 | ### Переход со старой feature-миграции V10 на V13 @@ -300,9 +300,13 @@ проверка на копии БД. Отчёт: [10-stage0-report.md](10-stage0-report.md). - **Этап 1 — выполнен.** Роли, группы-когорты, статусы, история, импорт v2, админ-UI. Отчёт: [11-stage1-report.md](11-stage1-report.md). -- **Этап 2 — следующий.** Курсы, преподаватели, привязка групп, лабораторные - и материалы курса C, активы, копирование лаб. -- Этапы 3–6 — по плану. +- **Этап 2 — выполнен.** Курсы и `course_no`, история преподавателей и групп, + bootstrap курса C, привязка лабораторных к курсу, копирование лаб, + админ-UI курсов и лабораторных. Отчёт: [12-stage2-report.md](12-stage2-report.md). +- **Этап 3 — следующий.** Доступ и профиль: `TEACHER` в security-конфигах, + внутренние access-эндпоинты, проверки курса/группы/студента, профиль студента + курса, материалы курса и аудитории, активы, кабинет преподавателя. +- Этапы 4–6 — по плану. ## 10. Definition of Done diff --git a/docs/10-stage0-report.md b/docs/10-stage0-report.md index 0c05f9a..6eeaa2b 100644 --- a/docs/10-stage0-report.md +++ b/docs/10-stage0-report.md @@ -11,8 +11,9 @@ - Миграции: - identity `V4–V9`: роли, группы-когорты, статусы студентов, повышение, история членства, статусы аккаунтов; - - academic `V6–V9`: курсы, преподаватели, группы курса, `labs.course_id`, - материалы курса/аудитории, индексы активов; + - academic `V6–V10`: курсы, преподаватели, группы курса, `labs.course_id`, + материалы курса/аудитории, индексы активов; `V10` (уникальный индекс + `lower(title)` по курсам) добавлена на этапе 2, после этого отчёта; - progress `V13`: автор комментария и последнего изменения оценки. - Preflight-скрипты: - `deploy/preflight/groups-courses.sql` — до миграций; diff --git a/docs/12-stage2-report.md b/docs/12-stage2-report.md new file mode 100644 index 0000000..f4cac54 --- /dev/null +++ b/docs/12-stage2-report.md @@ -0,0 +1,150 @@ +# 12. Отчёт этапа 2: academic (курсы и лабораторные) + +**Дата:** 2026-10-27. +**Статус:** этап 2 выполнен; этап 3 (доступ и профиль студента курса) — следующий. + +## 1. Что сделано + +### Курсы + +- Добавлены таблицы `courses`, `course_teachers`, `course_groups` (миграция `V6`). +- `courses.course_no` — `BIGINT GENERATED ALWAYS AS IDENTITY`, уникальный номер + для человекочитаемых URL; UUID остаётся первичным ключом. +- CRUD курсов: создание, просмотр, переименование, перевод в архив и возврат + в работу. Название нормализуется (trim), пустое заменяется на «Новый курс». +- Название курса уникально без учёта регистра (миграция `V10`, индекс по + `lower(title)`); повторное создание возвращает `409`. +- Назначение и снятие преподавателей, привязка и отвязка групп хранятся + с историей: снятие/отвязка проставляют `removed_at`, запись не удаляется. + Повторное назначение после снятия создаёт новую активную запись. +- Для архивированного курса запрещены новые назначения преподавателей + и привязка групп. + +### Bootstrap курса C + +- `CourseBootstrapService` идемпотентно создаёт (или находит) курс + «Промышленное программирование на языке C», назначает владельцем текущего + админа и переносит в него существующие данные. +- Конкурентные запуски сериализуются advisory-локом + `pg_advisory_xact_lock(hashtext('academic.course.bootstrap'))`. +- Перед переносом проверяются дубли номеров лабораторных без курса. При дублях + bootstrap завершается `409`, транзакция откатывается, курс не создаётся. +- Все активные группы, переданные из web, привязываются к курсу (повторный + запуск не создаёт дублей). +- Всем лабораторным и главам без курса проставляется `course_id = C`; + у курсовых глав `audience` сбрасывается в `NULL`. +- После переноса, если лабораторных без курса не осталось, сервис в рантайме + выполняет `ALTER TABLE academic.labs ALTER COLUMN course_id SET NOT NULL`. + Это **post-bootstrap шаг, а не Flyway-файл**: обычный старт Flyway не может + дождаться создания курса и назначения владельца. + +### Лабораторные + +- `labs.course_id` добавлен nullable-миграцией `V7`; уникальность номера — + в рамках курса: `(number, course_id)`. +- Создание и импорт лабораторной требуют курс; архивированный курс отклоняется + с `400`. +- Список лабораторных фильтруется по `courseId`; API и UI показывают номер + и название курса у каждой лабораторной. +- Копирование лабораторной в другой курс (`LabCopyService`): копируются + описание, уровни и варианты с новыми id, копия создаётся черновиком. + Оценки, сдачи, защиты и назначения вариантов не копируются. +- Копирование запрещено в тот же курс и в архивированный курс; занятый номер + в целевом курсе даёт `409`. + +### Админ-UI + +- `/admin/courses` — список курсов с номером, названием, статусом и счётчиками + преподавателей, групп и лабораторных; создание курса; блок первичного + bootstrap, пока курсов нет. +- `/admin/courses/{id}` — карточка курса: архивирование/возврат, привязка + и отвязка групп, назначение и снятие преподавателей по UUID аккаунта, + список лабораторных курса. +- `/admin/labs` — фильтр по курсу, колонка курса, выбор курса (обязательный) + в формах импорта и создания. +- `/admin/labs/{id}` — карточка лабораторной показывает курс; кнопка + «Копировать в курс» открывает форму выбора целевого курса, номера и названия. +- В навигацию админа добавлен пункт «Курсы». + +### Миграции + +- `V6__courses.sql` — курсы, преподаватели, группы курса. +- `V7__labs_course.sql` — `labs.course_id`, уникальность `(number, course_id)`. +- `V8__chapters_course.sql` — `chapters.course_id/audience`, частичные индексы slug. +- `V9__article_assets_indexes.sql` — индекс `article_assets(article_id)`. +- `V10__course_title_unique.sql` — уникальный индекс `lower(title)` по курсам. + +## 2. API + +Все эндпоинты курсов доступны только роли `ADMIN`. + +| Метод | Путь | Назначение | +|---|---|---| +| `GET` | `/api/v1/courses` | список курсов со счётчиками | +| `POST` | `/api/v1/courses` | создать курс, владелец — текущий админ | +| `GET` | `/api/v1/courses/{id}` | карточка курса с историей преподавателей и групп | +| `PUT` | `/api/v1/courses/{id}` | изменить название и/или статус | +| `POST` | `/api/v1/courses/{id}/teachers` | назначить преподавателя | +| `DELETE` | `/api/v1/courses/{id}/teachers/{accountId}` | снять преподавателя | +| `POST` | `/api/v1/courses/{id}/groups` | привязать группу | +| `DELETE` | `/api/v1/courses/{id}/groups/{groupId}` | отвязать группу | +| `POST` | `/api/v1/courses/bootstrap` | создать курс C и перенести данные | +| `GET` | `/api/v1/labs?courseId=…` | список лабораторных, опционально по курсу | +| `POST` | `/api/v1/labs` | создать лабораторную с обязательным `courseId` | +| `POST` | `/api/v1/labs/import?courseId=…` | импорт ZIP в указанный курс | +| `POST` | `/api/v1/labs/{id}/copy` | копировать лабораторную в другой курс | + +`LabSummary` и `LabDetail` дополнены полями `courseId`, `courseNo`, +`courseTitle`. В браузерных URL курса используется `course_no`; API работает +с UUID. + +## 3. Порядок bootstrap + +1. Админ открывает `/admin/courses`. Пока курсов нет, показывается блок + первичной настройки. +2. Web передаёт в `POST /api/v1/courses/bootstrap` название (необязательно) + и id всех активных групп. +3. Сервис берёт advisory-лок, проверяет UUID групп, находит или создаёт курс + по названию без учёта регистра (владелец — текущий админ). +4. Привязывает активные группы, проверяет дубли номеров лабораторных без курса. +5. Проставляет `course_id` лабораторным и главам. +6. Если лабораторных без курса не осталось, выполняет `SET NOT NULL` для + `labs.course_id`. +7. Возвращает `courseId`, `courseNo`, название и счётчики: сколько групп + привязано, лабораторных и глав перенесено, сколько осталось без курса. +8. Повторный запуск безопасен: существующий курс переиспользуется, уже + привязанные группы и перенесённые данные не дублируются. + +## 4. Тесты + +Добавлены интеграционные тесты на PostgreSQL (Testcontainers): + +- `CourseServiceIntegrationTest` (6 тестов): отказ bootstrap при дублях + номеров без создания курса, создание курса и перенос групп/лабораторных/глав, + идемпотентность bootstrap, генерация уникальных `course_no`, запрет + дубликата названия без учёта регистра, история назначений преподавателей + и групп, отказ при некорректном UUID группы. +- `LabCopyIntegrationTest` (5 тестов): копирование с уровнями и вариантами, + обязательный курс при создании, запрет копирования в тот же курс, в + архивированный курс и создание лабораторной в архивированном курсе. + +Прогон 2026-10-27: `./gradlew :services:academic-service:test :web:test` — +успешно, в новых наборах 0 failures / 0 errors. `git diff --check` — успешно. + +## 5. Известные пробелы (этап 3) + +- **Материалы курса и аудитории.** Схема `chapters.course_id/audience` готова + (`V8`), но URL `/learn/course/{course_no}/...` и проверка аудитории ещё не + реализованы: читалка материалов пока работает по прежним глобальным правилам. +- **Активы.** `V9` добавляет только индекс; наследуемая видимость активов + от статьи → раздела → главы и проверка доступа при выдаче файлов — этап 3. +- **Проверка аккаунтов преподавателей.** Academic принимает UUID без обращения + к identity: не проверяются существование аккаунта, роль `TEACHER` и статус + `ACTIVE`. Нужна внутренняя проверка identity при назначении. +- **Кабинет преподавателя и скоупинг.** API курсов пока admin-only; кабинет + `/teacher/...`, доступ преподавателя только к своим курсам и студентам, + профиль студента курса и negative-тесты «чужой курс/студент/сдача» — + этап 3. +- **Повторный bootstrap из UI.** Кнопка bootstrap показывается только когда + курсов нет; повторный запуск возможен через API. Группы, появившиеся позже, + привязываются вручную на странице курса. diff --git a/docs/README.md b/docs/README.md index a34c8b8..359a584 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid). | [09-teacher-student-profile.md](09-teacher-student-profile.md) | Контракт API профиля студента курса для преподавателя | | [10-stage0-report.md](10-stage0-report.md) | Отчёт этапа 0: миграции, preflight и проверка на копии БД | | [11-stage1-report.md](11-stage1-report.md) | Отчёт этапа 1: роли, группы-когорты, статусы, история, импорт v2 | +| [12-stage2-report.md](12-stage2-report.md) | Отчёт этапа 2: курсы, bootstrap курса C, привязка и копирование лабораторных, миграция V10 | | [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение | ## Кратко о системе