Files
LMS/docs/12-stage2-report.md
Vladislav Kopnin 65ed08f5c2 feat(web): перевести браузерные URL курсов на courseNo
- teacher/admin course pages, content, gradebook, profile, downloads
- courseNo -> UUID только на границе REST API
- обновлены e2e, шаблоны, тесты и docs
- LDD-UI-002; baseline 59 claims / 385 тестов
2026-10-29 19:51:03 +03:00

12 KiB
Raw Blame History

12. Отчёт этапа 2: academic (курсы и лабораторные)

Историческая справка: в этом отчёте браузерные URL показаны с UUID курса. После перехода 2026-10-29 текущие URL используют courseNo; REST API по-прежнему принимает UUID.

Дата: 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) по курсам.
  • V11__global_audience_all.sql — backfill audience='ALL' для глобальных глав.
  • V12__asset_links_backfill.sql — backfill связей article_assets из markdown-ссылок для видимости активов.

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}/... и проверка аудитории ещё не реализованы: читалка материалов пока работает по прежним глобальным правилам.
  • Активы — закрыто после этапа 5. V9 добавил индекс; V12__asset_links_backfill.sql восстановил связи article_assets из markdown-ссылок, а /api/v1/content/assets/{name} проверяет наследуемую видимость статьи → раздела → главы. Остаточный случай — файлы без ссылок (см. 15).
  • Проверка аккаунтов преподавателей. Academic принимает UUID без обращения к identity: не проверяются существование аккаунта, роль TEACHER и статус ACTIVE. Нужна внутренняя проверка identity при назначении.
  • Кабинет преподавателя и скоупинг. API курсов пока admin-only; кабинет /teacher/..., доступ преподавателя только к своим курсам и студентам, профиль студента курса и negative-тесты «чужой курс/студент/сдача» — этап 3.
  • Повторный bootstrap из UI. Кнопка bootstrap показывается только когда курсов нет; повторный запуск возможен через API. Группы, появившиеся позже, привязываются вручную на странице курса.