# Перенос TestingWebApp на стек HR_TG_Bot / tgFlaskForm **Тот же план простым языком (две базы, люди, этапы):** [migration-to-tgflaskform-plain.md](migration-to-tgflaskform-plain.md). **Назначение документа:** зафиксировать целевую архитектуру, **спринтовый план** доведения функциональности до паритета и **порядок миграции данных** из отдельного приложения (`Express` + `React` + БД `clinic_tests`) в кабинет **`tgFlaskForm`** (Flask, шаблоны, общая БД `hr_bot_test`, таблицы `testing_*`). **Связанные материалы:** [PROJECT_STATUS.md](PROJECT_STATUS.md), [README.md](../README.md), [TEST_TABLES_ANALYSIS.md](TEST_TABLES_ANALYSIS.md), код модуля в репозитории HR: `HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/`, модели: `HR_TG_Bot/tgFlaskForm/db/models.py`. **Каркас нового контура в этом репозитории:** [../flask_app/README.md](../flask_app/README.md). --- ## 0. Стратегия переходного периода (отдельное приложение, тот же стек) **Решение:** переписывание с Node/React на **тот же стек, что у мини-приложения и кабинета HR** — Python 3, **Flask**, шаблоны (Jinja2), статический JS, работа с PostgreSQL в духе `tgFlaskForm`. При этом сервис **пока живёт отдельно**: свой процесс, свой URL/порт, **не** обязан совпадать с деплоем полного `HR_TG_Bot/tgFlaskForm`. **Зачем так:** быстрее выйти на паритет по UX и данным, **без** риска «большого взрыва» в едином кабинете; позже либо встраиваете модуль в кабинет (общий `webApp`), либо оставляете отдельный вход — стек уже совпадает. **Обязательно зафиксировать продуктово:** | Вопрос | Рекомендация | |--------|----------------| | Где **пишут** тесты и попытки, пока два контура? | Один «канонический» контур на запись; второй read-only или только пилот — иначе разъедутся данные. | | База | Либо по-прежнему **`clinic_tests`** в новом Flask до ETL, либо сразу **`hr_bot_test`** + `testing_*` (как в кабинете) — одно из двух, не смешивать без миграции. | | ETL | Скрипт `HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py`: бэкап → `--dry-run` → проверка на копии → короткое окно → `--apply`. | **Технически:** в репозитории TestingWebApp заведён каталог **`flask_app/`** — минимальное приложение-заготовка; развитие переноса идёт там (или копированием готовых модулей из `HR_TG_Bot/tgFlaskForm`). --- ## 1. Зачем переносить | Аспект | Сейчас (TestingWebApp) | Цель (tgFlaskForm) | |--------|------------------------|---------------------| | Стек | Node.js (Express), React (Vite), отдельный деплой | Python 3, Flask, Jinja/PyPug, статический JS в шаблонах — **единый кабинет** с остальным HR | | База | PostgreSQL, схема `clinic_tests`, UUID-ключи, локальные `users` | Та же инфраструктура Postgres, БД **`hr_bot_test`**, целочисленные `id`, связь с **`staff_members`** | | Авторизация | Собственные логин/JWT + опция `HR_AUTH` | Сессии кабинета, RBAC через HR (`testing_head_positions`, флаги HR и т.д.) | | Модуль тестирования | Полный цикл в одном репозитории | В **`tgFlaskForm` уже есть** blueprint `/cabinet/testing`, запросы в `db/queries/testing_queries.py` — задача переноса = **паритет фич + данные + вывод из эксплуатации** старого UI/API | Итог после **полной** консолидации: один вход для сотрудника, одна БД «истины» по людям, меньше дублирования интеграций с HR. На переходном этапе допустим **отдельный** Flask-инстанс с тем же стеком (см. §0). --- ## 2. Исходный и целевой стек (кратко) **Исходный (TestingWebApp):** - Backend: `express`, `pg`, миграции SQL в `backend/src/db/migrations/`. - Frontend: `react`, `react-router-dom`, `vite`. - Данные: цепочки `tests` → `test_versions` → `questions` → `answer_options`; назначения с `test_assignment_targets` (отдел/пользователь); попытки `test_attempts`, ответы `user_answers` (массив UUID вариантов). **Целевой (`HR_TG_Bot/tgFlaskForm`) и отдельный контур в этом репозитории (`flask_app/`):** - Приложение: `Flask`, точка входа `web_run.py`, фабрика/приложение `webApp/__init__.py`. - Шаблоны: `webApp/templates/cabinet/testing/*.html`, клиентский JS в `templates/static/js/cabinet/testing_*`. - ORM/запросы: SQLAlchemy-модели `TestingTest`, `TestingTestVersion`, `TestingQuestion`, `TestingAnswer`, `TestingAssignment`, `TestingAttempt`, `TestingAttemptAnswer`, `TestingSetting`, `TestingHeadPosition` в `db/models.py`; бизнес-запросы — `db/queries/testing_queries.py`. - Сервер: dev `flask run`, prod типично `waitress` (см. `web_run.py`). - **Отдельный деплой в TestingWebApp:** каталог `flask_app/` — `run.py`, шаблоны в `flask_app/app/templates/` (см. §0). --- ## 3. Спринтовый план (переписывание = паритет + миграция + снятие стенда) Длительность спринта ориентировочно **2 календарные недели**; границы можно сжимать/растягивать под состав команды. Нумерация условная: **Спринт 0** — подготовка, далее функциональные слои. ### Спринт 0 — Инвентаризация и критерии готовности **Цель:** зафиксировать разрыв «TestingWebApp ↔ tgFlaskForm» и правила миграции. - Составить **матрицу сценариев** по [ТЗ.md](ТЗ.md) и [PROJECT_STATUS.md](PROJECT_STATUS.md): редактор теста, версии, назначения, прохождение, разбор, трекер, настройки модуля, AI. - Зафиксировать отличия схемы: UUID vs integer, модель назначений (цель: каждая строка `TestingAssignment` = один `staff_id`). - Решение по **импорту из PDF/DOCX** (в Node-версии есть извлечение текста для черновика): либо перенос в Python (`tgFlaskForm`), либо явный scope «после миграции». - **Критерий выхода:** подписанный чек-лист паритета + утверждённый порядок миграции (раздел 4 этого документа). ### Спринт 1 — Данные и идентификаторы **Цель:** подготовить перенос без потери смысла связей. - Убедиться, что у всех значимых пользователей `clinic_tests.users` есть сопоставление с **`staff_members.id`** (колонка `staff_id` и/или правила сопоставления по логину из HR). - Спроектировать **таблицы соответствия** для одноразового ETL (например временные таблицы или JSON-маппинги: `old_test_uuid → testing_tests.id`, `old_version_uuid → testing_test_versions.id`, и т.д.). - Реализовать **скрипт миграции** — в репозитории HR: [`HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py`](../../HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py) (Python, `psycopg2`, два URL). Режимы: `--dry-run` (только отчёт) и `--apply` (одна транзакция `COMMIT` в `hr_bot_test`). Переменные или флаги: `CLINIC_TESTS_URL`, `HR_BOT_URL`; опция `--skip-missing-staff` пропускает цепочки, у автора нет `users.staff_id`. - **Критерий выхода:** dry-run на копии прод-дампа `clinic_tests` + smoke-проверки количества строк (тесты, версии, вопросы, попытки). ### Спринт 2 — Паритет бизнес-логики в Flask **Цель:** закрыть расхождения поведения, а не только UI. - Версионирование: правила «первая правка без попыток / новая версия после попыток», активная версия — согласовать с уже реализованным в `testing_queries.py` и довести до полного соответствия ТЗ при необходимости. - Назначения: если в `clinic_tests` остались назначения **на отдел**, описать стратегию **разворачивания** в N строк `TestingAssignment` (по списку `staff_id` отдела на дату миграции) или доработать модель в HR (отдельное решение продукт-оунера). - Прохождение: таймер, лимит попыток, дедлайн, случайный порядок вопросов (`question_seed`) — сверка с ТЗ и доработка в Python при расхождении. - **Критерий выхода:** автоматические тесты на критичные запросы (где их ещё нет) + ручной прогон чек-листа из спринта 0. ### Спринт 3 — UI/UX кабинета и интеграция в меню **Цель:** пользователь не возвращается к старому хосту. - Пункты меню кабинета, бейджи «назначенные тесты», единый стиль с `cabinet/base.html`. - Довести страницы: список «мои тесты», редактор, назначение, прохождение, результат/разбор, трекер, настройки — по чек-листу. - Импорт документов (если включён в scope спринта 0): эндпоинт + UI в шаблоне, ключи API только на сервере (`TestingSetting` / env). - **Критерий выхода:** UX-приёмка на стенде, совпадающий с ТЗ сценарий для HR / руководителя / сотрудника. ### Спринт 4 — Миграция prod, cutover, архив TestingWebApp **Цель:** переключить реальных пользователей и зафиксировать артефакты. - Заморозка записи в TestingWebApp (режим только чтение или техническое окно). - Прогон ETL на прод-копии → валидация → прогон на боевой БД в согласованное окно. - Обновление ссылок (внутренние порталы, документация, docker-compose): вместо `:3107` / отдельного сервиса — URL кабинета HR с `/cabinet/testing/...`. - Репозиторий TestingWebApp: ветка **`legacy/clinic-tests-node`**, в README — ссылка на этот документ и дата end-of-life API/UI. - **Критерий выхода:** мониторинг ошибок (например Sentry уже в `webApp/__init__.py`), отсутствие P1 по тестам в первую неделю после cutover. --- ## 4. Как происходит миграция данных (пошагово) ### 4.1 Предпосылки 1. Доступ к **двум** базам с одной машины (или логическое копирование дампа): `clinic_tests` и `hr_bot_test`. 2. Маппинг **пользователь → сотрудник:** для каждой строки `users` в `clinic_tests` должен быть известен **`staff_members.id`**. Если `staff_id` пустой — заранее ручной/полуавтоматический справочник соответствий (логин, email, ФИО). 3. Зафиксированная **версия кода** `tgFlaskForm`, в которой пройдены регрессионные тесты модуля тестирования. ### 4.2 Порядок загрузки сущностей (чтобы не нарушить FK) Рекомендуемый порядок транзакций/батчей: 1. **`testing_tests`** — из цепочек `tests`: `title`, `description`, `created_by` ← `users.staff_id`, `is_active`, `created_at` (по политике: локальное время vs UTC). 2. **`testing_test_versions`** — из `test_versions`: связь `test_id` через маппинг; `version_number` ← `version`; `passing_score_percent` ← порог из версии/цепочки (в старой схеме часть полей была на `tests` — нормализовать в версию как в SQLAlchemy-модели); `time_limit_minutes`, `allow_back_navigation`, `is_active_version`, флаг единственной активной версии на цепочку. 3. **`testing_questions`** — из `questions`: текст, тип (`single`/`multiple` из `has_multiple_answers`), `sort_order` ← `question_order`. 4. **`testing_answers`** — из `answer_options`: текст, `is_correct`, порядок. 5. **`testing_assignments`** — из `test_assignments` + `test_assignment_targets`: - для целей типа **пользователь:** одна строка на пару (тест, `staff_id`); - для целей **отдел:** развернуть в множество строк по сотрудникам отдела на момент миграции (с явным логом «создано из department_id=…»); - `assigned_by` ← `staff_id` постановщика; `deadline`, `max_attempts`, `assigned_at`. 6. **`testing_attempts`** — из `test_attempts`: связь с новым `assignment_id` (если в старой модели попытка шла от `user_id` без отдельного assignment — потребуется **восстановление** или создание синтетических назначений; зафиксировать правило в спринте 0). 7. **`testing_attempt_answers`** — из `user_answers`: каждый выбранный UUID варианта → строка с новым `answer_id` (через маппинг `answer_options.id` → `testing_answers.id`). Везде, где в старой БД использовались **UUID**, скрипт хранит таблицу **`public._clinic_tests_migration_map`** (`entity`, `old_uuid` → `new_id`) в `hr_bot_test` для идемпотентного повторного прогона. **Замечание по назначениям:** в текущей версии скрипта строки `clinic_tests.test_assignments` / `test_assignment_targets` **не** переносятся пакетно; для каждой пары (тест HR, сотрудник) при переносе **попыток** создаётся или находится строка `testing_assignments` (синтетическое назначение, `max_attempts = 99`). Полный импорт истории назначений из clinic — отдельная доработка при необходимости. ### 4.3 Валидация после ETL - Сравнение **агрегатов:** число тестов, версий, вопросов, назначений, завершённых попыток, строк ответов. - Выборочная сверка: 5–10 последних попыток — ручной разбор «вопрос / выбранные варианты / балл» в старом и новом UI. - Проверка уникальности «одна активная версия на тест» и отсутствия «висячих» FK. ### 4.4 Cutover (переключение) 1. Объявить **окно**: остановка записи в TestingWebApp. 2. Инкрементальный дамп изменений с последней реплики (если делали пробный перенос ранее) или финальный полный перенос. 3. Прогон ETL в транзакции (или по крупным батчам с чекпоинтами) → `VACUUM ANALYZE` при необходимости. 4. Включить пользователям ссылку на **кабинет**; проверить права `can_create_tests` / HR. 5. Сохранить **бэкап** `clinic_tests` и лог миграции минимум на срок, определённый политикой клиники (типично 30–90 дней). ### 4.5 Откат - Если после cutover обнаружен блокирующий дефект: вернуть пользователей на временный старый стенд **только для чтения** при наличии бэкапа; новые данные в `hr_bot_test` после cutover при откате не синхронизируются автоматически — риск фиксируется заранее (короткое окно, «freeze» повторных действий). --- ## 5. Риски и как их снимать | Риск | Мера | |------|------| | Неполное сопоставление `users` ↔ `staff_members` | Закрыть в спринте 1; не начинать ETL без процента покрытия, согласованного с заказчиком | | Разная семантика назначений (отдел, версия) | Явные правила в спринте 0 + лог развёртки отделов | | Потеря истории попыток из-за смены модели assignment | Моделирование на копии БД в спринте 1–2 | | Дублирование разработки UI | Опираться на уже существующий модуль в `tgFlaskForm`, не переписывать с нуля параллельный SPA | --- ## 6. Итог Переписывание в данном контексте — это не «ещё один greenfield на Flask», а **консолидация** уже начатого модуля в `tgFlaskForm` с **одноразовой миграцией** из `clinic_tests` и выводом из эксплуатации связки React + Express. Спринты 0–4 дают сквозной маршрут от анализа до cutover; детали ETL должны быть закреплены в коде скрипта и журнале прогона к концу **спринта 1**. **См. также:** если пользователи жалуются на медленную загрузку страниц кабинета/Flask — пошаговый план измерений и правок: [performance-flask-mini-app.md](performance-flask-mini-app.md).