docs: миграция на tgFlaskForm и производительность Flask; контур flask_app; UI без лишних описаний

Made-with: Cursor
This commit is contained in:
Константин Лебединский
2026-04-27 18:43:51 +05:00
parent 47279c72e3
commit b3e3757a92
20 changed files with 680 additions and 115 deletions
+175
View File
@@ -0,0 +1,175 @@
# Перенос 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).