feat(flask): E1.0–E1.3, E1.8 — миграция на Python/Flask + AI v2
Этап 1 миграции TestingWebApp на целевой стек (Python/Flask/Jinja),
БД остаётся clinic_tests.
E1.0 — База Flask-приложения: SQLAlchemy/psycopg2 пул, Flask sessions,
фабрика create_app, blueprint main с / и /health, base.html в стиле
кабинета HR (Tailwind CDN + Manrope + Material Symbols), 404/500.
E1.1 — Auth + /api/me: Flask sessions (signed cookie) вместо JWT,
bcrypt + Werkzeug, опц. HR_AUTH=1 с UPSERT в clinic_tests.users по
staff_id. UI /login, JSON /api/auth/{login,logout,me}, декораторы
@login_required / @require_role.
E1.2 — Тесты: список + редактор. 10 эндпоинтов, сервисы test_draft,
test_access, test_chain, ai_editor, llm_client, draft_validator,
editor_content. UI /tests (каталог + создание) и /tests/<id>/edit
(редактор с AI). Полный мобильный UX (аккордеоны/drag-n-drop) — в E1.7.
E1.3 — Импорт документов: pypdf + python-docx, эндпоинт
POST /api/tests/import/document, кнопка «Импорт документа» в
AI-панели редактора, лимит 16 МБ.
E1.8 — AI v2: страница /settings (статус ENV-ключа + ping),
ai/generate-by-title (без сетки), ai/check (рецензия), ai/improve
(массовое было→стало с чекбоксами). Унифицированный ответ AI-ошибок:
{ error, code, settingsUrl }.
Docker:
- docker-compose.dev.yml: добавлены DATABASE_URL, HR_AUTH/HR_DATABASE_URL,
DEEPSEEK_API_KEY/OPENAI_API_KEY/LLM_BASE_URL/LLM_MODEL и сеть postgres
для testing-flask.
Документация:
- docs/migration-final.md — двух-этапный план (Этап 1: унификация
стека внутри TestingWebApp; Этап 2: слияние с tgFlaskForm).
- docs/migration-final-inventory.md — карта 22 эндпоинтов Express.
Made-with: Cursor
This commit is contained in:
@@ -23,6 +23,8 @@
|
||||
- **Активная версия** — та, с которой сейчас стартуют новые попытки. Автор может **вручную** переключить активную версию в таблице истории (с подтверждением), если бизнесу так нужно.
|
||||
- **Публикация / видимость:** в кабинете (аккордеон **«Показ в каталоге»**, подсекция **«Видимость»**) тест можно **скрыть из общего списка** (цепочка остаётся в базе) или **снова показать**; **назначения** (подсекция **«Кому выдать»**) — при включённой фиче, см. раздел «Назначения» ниже.
|
||||
- **Мобильный UI** кабинета (колонка списка на узком экране, фикс-футер, группировка разделов, копи): [СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md) · [РУКОВОДСТВО_КАБИНЕТ_ТЕСТОВ.md](РУКОВОДСТВО_КАБИНЕТ_ТЕСТОВ.md) (тезисы для врачей/кураторов).
|
||||
- **Унификация стека (Этап 1, текущий)**: Express → Flask + React → Jinja **внутри TestingWebApp** (`flask_app/`). БД остаётся `clinic_tests`, схема не меняется. План и журнал — [migration-final.md](migration-final.md).
|
||||
- **Слияние с HR-кабинетом (Этап 2, на будущее, без сроков)**: перенос в `HR_TG_Bot/tgFlaskForm` как blueprint `cabinet/testing`, ETL `clinic_tests → hr_bot_test`. План — [migration-to-tgflaskform.md](migration-to-tgflaskform.md). Карта Express-функционала и справочный gap-analysis с уже существующим модулем HR-кабинета — [migration-final-inventory.md](migration-final-inventory.md).
|
||||
|
||||
### Список тестов и доступ
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Иллюстрации к [РУКОВОДСТВО_КАБИНЕТ_ТЕСТОВ.md](../РУКОВОДСТВО_КАБИНЕТ_ТЕСТОВ.md)
|
||||
|
||||
Сейчас здесь **SVG-схемы** (placeholder). Их **подключает** `РУКОВОДСТВО_КАБИНЕТ_ТЕСТОВ.md` в корне `docs/`.
|
||||
|
||||
Чтобы подставить **скриншоты** с экрана кабинета / мини-приложения:
|
||||
|
||||
1. Сделайте 2–3 снимка (список тестов, низ с **«Сохранить»**, **«Показ в каталоге»**).
|
||||
2. Сохраните в эту папку, например `spisok.png`, `chernovik.png`, `pokaz.png`.
|
||||
3. В **`РУКОВОДСТВО_КАБИНЕТ_ТЕСТОВ.md`** замените в строках `` пути `placeholder-*.svg` на ваши `*.png` (или оставьте SVG).
|
||||
|
||||
Текущие имена в ссылках: `placeholder-spisok.svg`, `placeholder-chernovik.svg`, `placeholder-pokaz.svg`.
|
||||
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200" role="img" aria-label="!E5<0: A>E@0=5=85">
|
||||
<rect width="100%" height="100%" fill="#f8faf9" stroke="#cfe8e3"/>
|
||||
<text x="180" y="36" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" font-weight="700" fill="#0d5c54">=87C «!>E@0=8BL G5@=>28:»</text>
|
||||
<text x="180" y="58" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#5c6b69">A>E@0=O5B 2AQ, GB> =0?8A0;8 2 2>?@>A0E</text>
|
||||
<rect x="32" y="100" width="296" height="40" rx="8" fill="#0d5c54"/>
|
||||
<text x="180" y="125" text-anchor="middle" font-family="system-ui,sans-serif" font-size="12" font-weight="600" fill="#fff">!>E@0=8BL G5@=>28:</text>
|
||||
<rect x="32" y="150" width="296" height="32" rx="8" fill="#fff" stroke="#0d5c54"/>
|
||||
<text x="180" y="170" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#0d5c54"> A?8A:C</text>
|
||||
</svg>
|
||||
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="180" viewBox="0 0 360 180" role="img" aria-label="!E5<0: 2848<>ABL 8 =07=0G5=8O">
|
||||
<rect width="100%" height="100%" fill="#f8faf9" stroke="#cfe8e3"/>
|
||||
<text x="180" y="32" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" font-weight="700" fill="#0d5c54">>:07 2 :0B0;>35</text>
|
||||
<text x="32" y="64" font-family="system-ui,sans-serif" font-size="11" fill="#1a1a1a">· 848<>ABL 2 >1I5< A?8A:5 8;8 A:@KB</text>
|
||||
<text x="32" y="88" font-family="system-ui,sans-serif" font-size="11" fill="#1a1a1a">· ><C 2K40BL A>B@C4=8:8 (5A;8 2:;NG5=>)</text>
|
||||
<text x="32" y="118" font-family="system-ui,sans-serif" font-size="10" fill="#5c6b69">07=0G5=85 = 2K40BL B5AB 2 @01>BC, =5 ?CB09B5 A «A>E@0=8BL»</text>
|
||||
<text x="32" y="148" font-family="system-ui,sans-serif" font-size="10" fill="#5c6b69">«K1@0BL 2A5E» = B>;L:> 2 B5:CI5< >BD8;LB@>20==>< A?8A:5</text>
|
||||
</svg>
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="200" viewBox="0 0 360 200" role="img" aria-label="!E5<0: A?8A>: B5AB>2">
|
||||
<rect width="100%" height="100%" fill="#f8faf9" stroke="#cfe8e3"/>
|
||||
<text x="180" y="32" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" font-weight="700" fill="#0d5c54">!?8A>: B5AB>2</text>
|
||||
<text x="180" y="54" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#5c6b69">:0@B>G:0 ’ =06<8B5 =0 =0720=85 8;8 «@>9B8»</text>
|
||||
<rect x="24" y="72" width="312" height="44" rx="10" fill="#fff" stroke="#b8d4cf"/>
|
||||
<rect x="24" y="124" width="312" height="44" rx="10" fill="#fff" stroke="#b8d4cf"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,291 @@
|
||||
# Инвентаризация backend и справочный gap-analysis с `tgFlaskForm`
|
||||
|
||||
**Назначение.** §1–§9 — полная карта того, что **сейчас живёт** в `backend/` (Node.js / Express). Используется в [Этапе 1](migration-final.md#этап-1-текущий--единый-стек-express--flask-react--jinja-внутри-testingwebapp) как чек-лист «что переписать на Flask внутри `flask_app/`». §10 — **справочный gap-analysis** между этим и уже готовым модулем `cabinet/testing` в `HR_TG_Bot/tgFlaskForm` — пригодится в [Этапе 2](migration-to-tgflaskform.md) при слиянии.
|
||||
|
||||
**Связано:** [migration-final.md](migration-final.md) (главный трекер двух этапов), [migration-to-tgflaskform.md](migration-to-tgflaskform.md) (план Этапа 2 — слияние с tgFlaskForm), [PROJECT_STATUS.md](PROJECT_STATUS.md), [СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md).
|
||||
|
||||
> **Прочитайте сначала [migration-final.md](migration-final.md)** — там зафиксировано разделение на Этап 1 (унификация стека внутри TestingWebApp, БД остаётся `clinic_tests`) и Этап 2 (слияние с HR-кабинетом, БД мигрирует на `hr_bot_test`). Содержимое §10 этого документа — материал для Этапа 2.
|
||||
|
||||
---
|
||||
|
||||
## 1. Карта HTTP-эндпоинтов (всё, что зовёт фронтенд)
|
||||
|
||||
Источник: `backend/src/app.js`, `backend/src/routes/*.js`. Перепроверено по фактическим вызовам из `frontend/src/**` (`frontend/src/api.js` — общий `fetch` с `credentials: 'include'`).
|
||||
|
||||
| # | Метод | Путь | Зовёт из фронта | Auth | Сервис(ы) | Особенности |
|
||||
|---|------|------|----------------|------|----------|-------------|
|
||||
| 1 | GET | `/api/health` | — | нет | inline | smoke-проверка |
|
||||
| 2 | POST | `/api/auth/login` | `Login.jsx` | нет | `utils/auth`, `db/db`, `db/hrPool`, `utils/werkzeugPassword`, `utils/hrRoleMap`, `config/authConstants` | bcrypt (dev) **+** Werkzeug (HR) ветки; UPSERT `users` по `staff_id`; кладёт JWT в HTTP-only cookie |
|
||||
| 3 | POST | `/api/auth/logout` | `CabinetLayout.jsx` | нет | inline | очистить cookie |
|
||||
| 4 | GET | `/api/auth/me` | `CabinetLayout.jsx` | cookie JWT | `middleware/auth` + `featureFlags` | возвращает пользователя + флаги UI (`devUi`, `assignmentUi`) |
|
||||
| 5 | GET | `/api/auth/dev/assignment-directory` | `TestDetail.jsx` (assign-блок) | cookie JWT, feature-flag | `services/assignmentDirectoryService` | сливает HR (`staff_members`, `employees_departments`) + `clinic_tests.users`; query: `q`, `department`, `clinic` |
|
||||
| 6 | POST | `/api/tests/import/document` | `TestDetail.jsx` («Документ в вопросы») | cookie JWT | `services/documentExtractService` (PDF/DOCX/TXT), `services/documentGenService` → `services/llmClient` | `multer` (10 МБ, OS tmpdir); удаляет файл после извлечения |
|
||||
| 7 | GET | `/api/tests` | `TestsList.jsx` | cookie JWT | `services/testAccessService.queryTestsVisibleToUser` + inline (hiddenByYou) | каталог + список «скрытые мной» |
|
||||
| 8 | POST | `/api/tests` | `TestsList.jsx` | cookie JWT | `services/testDraftService.createTestWithVersion` | создаёт цепочку + версию 1 |
|
||||
| 9 | GET | `/api/tests/:id/summary` | `TestDetail.jsx` | cookie JWT | inline + `testAccessService.userHasTestAccess` | карточка цепочки (одна строка) |
|
||||
| 10 | GET | `/api/tests/:id/versions` | `TestDetail.jsx` | cookie JWT, **только автор** | inline + `testChainService.hasAnyAttemptForTest` | список версий + флаг `hasAttempts` |
|
||||
| 11 | GET | `/api/tests/:id/editor` | `TestDetail.jsx` | cookie JWT, **только автор** | `testAttemptService.getEditorContent` | вопросы активной версии **с правильными ответами** |
|
||||
| 12 | POST | `/api/tests/:id/ai/generate-test` | `TestDetail.jsx` (ИИ — целиком) | cookie JWT, **только автор** | `aiEditorService.generateFullTestByShape` → `llmClient` | строгая сетка `shape: [{optionsCount, hasMultipleAnswers}]`; до 40 вопросов |
|
||||
| 13 | POST | `/api/tests/:id/ai/generate-question` | `TestDetail.jsx` (ИИ — один вопрос) | cookie JWT, **только автор** | `aiEditorService.generateOrRephraseQuestion` → `llmClient` | пустой текст → новый вопрос; непустой → переформулировка |
|
||||
| 14 | POST | `/api/tests/:id/versions/:vid/activate` | `TestDetail.jsx` | cookie JWT, **только автор** | inline | транзакция; снимает `is_active` со всех версий, потом ставит на `:vid` |
|
||||
| 15 | PATCH | `/api/tests/:id` | `TestDetail.jsx` | cookie JWT, **только автор** | inline | `chainActive` (true/false) — публикация в каталоге |
|
||||
| 16 | POST | `/api/tests/:id/assign` | `TestDetail.jsx` (назначение) | cookie JWT, **только автор**, feature-flag | `assignmentUserService.ensureClinicUserIdForStaff` | принимает `userIds[]`/`staffIds[]`/legacy `userId`/`staffId`; одна строка `test_assignments` + N строк `test_assignment_targets` |
|
||||
| 17 | POST | `/api/tests/:id/draft` | `TestDetail.jsx` («Сохранить») | cookie JWT, **только автор** | `testDraftService.saveTestDraft` | если есть попытки и переданы `questions` — fork новой версии (V.3) |
|
||||
| 18 | POST | `/api/tests/:id/attempts/start` | `TestsList.jsx` | cookie JWT (доступ через `userHasTestAccess`) | `testAccessService.userHasTestAccess` + inline | новая попытка по активной версии |
|
||||
| 19 | GET | `/api/tests/:id/attempts` | `TestDetail.jsx` («Прохождения») | cookie JWT, **только автор** | `testAttemptService.listTestAttemptsForAuthor` | до 200 попыток по всем версиям |
|
||||
| 20 | GET | `/api/tests/:id/attempts/:aid/review` | `TestAttemptReview.jsx` | cookie JWT (владелец **или** автор) | `testAttemptService.getAttemptReviewForUser` → `buildReviewFromDb` | разбор попытки |
|
||||
| 21 | GET | `/api/tests/:id/attempts/:aid/play` | `TestAttempt.jsx` | cookie JWT (владелец) | `testAttemptService.getPlayContent` | вопросы **без** правильных ответов |
|
||||
| 22 | POST | `/api/tests/:id/attempts/:aid/submit` | `TestAttempt.jsx` | cookie JWT (владелец) | `testAttemptService.submitAttempt` | `FOR UPDATE`, проверка ответов, перезапись `user_answers`, статус `completed` |
|
||||
| 23 | GET | `/api/tests/:id/chain-info` | `TestDetail.jsx` | cookie JWT (через `userHasTestAccess`) | `testAccessService` + `testChainService` | флаг `hasAnyAttempt` |
|
||||
|
||||
> **Итого: 22 функциональных эндпоинта** (без `/api/health`). Все ответы — JSON. Все входы — JSON или `multipart/form-data` (только `/import/document`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Сервисный уровень (что должно появиться в Flask)
|
||||
|
||||
| Файл (Node) | Что делает | Что нужно в Python |
|
||||
|------------|-----------|---------------------|
|
||||
| `services/testAccessService.js` | Запросы каталога; `userHasTestAccess(testId, userId)` | SQLAlchemy/`psycopg2` версия двух запросов |
|
||||
| `services/testDraftService.js` | `createTestWithVersion`, `saveTestDraft`, `forkNewVersion`, `replaceVersionContent`, `copyQuestionTree` | Транзакции на `psycopg2`/SQLAlchemy; следить за частичным уникальным индексом `uq_test_versions_one_active_per_test` |
|
||||
| `services/testAttemptService.js` | `getEditorContent`, `getPlayContent`, `submitAttempt`, `buildReviewFromDb`, `getAttemptReviewForUser`, `listTestAttemptsForAuthor`; вычисление баллов | Самый объёмный модуль; внимательно с массивами UUID (`user_answers.selected_options uuid[]`) |
|
||||
| `services/testChainService.js` | `hasAnyAttemptForTest` | Один SQL `EXISTS` |
|
||||
| `services/aiEditorService.js` | `parseAndValidateShape`, `generateFullTestByShape`, `generateOrRephraseQuestion`, `assertDraftMatchesShape` | Чистый Python; зависит от `llmClient` и `documentGenService.validate*` |
|
||||
| `services/documentGenService.js` | `parseJsonFromLlmText`, `validateAndNormalizeDraft`, `generationForImportDocument` | Чистый Python (`json.loads` + валидация формы) |
|
||||
| `services/documentExtractService.js` | `resolveDocumentKind`, `extractTextFromFile`, `extractTextFromBuffer` | **Замены пакетов:** `mammoth` → `python-docx` или `mammoth.py`; `pdf-parse` → `pypdf` (или `pdfminer.six`) |
|
||||
| `services/llmClient.js` | OpenAI-совместимый Chat Completions, JSON-mode, таймаут 120 с | `httpx` / `requests` + явный `timeout` |
|
||||
| `services/assignmentDirectoryService.js` | Слияние `clinic_tests.users` ↔ HR (`staff_members`, `employees_departments`); фильтры `q/department/clinic` | Два пула; `psycopg2` достаточно |
|
||||
| `services/assignmentUserService.js` | `ensureClinicUserIdForStaff` (UPSERT по `staff_id`) | UPSERT с `ON CONFLICT (staff_id)` |
|
||||
| `utils/auth.js` | bcrypt + JWT + (Werkzeug fallback) | `passlib[bcrypt]` или `bcrypt`; **`flask-jwt-extended`** или ручной `pyjwt` |
|
||||
| `utils/werkzeugPassword.js` | `scrypt:$N:$r:$p$salt$hex`, `pbkdf2:sha256:iter$salt$hex` | Уже **родной** Python: `werkzeug.security.check_password_hash` |
|
||||
| `utils/hrRoleMap.js` | строка HR-роли → `'hr'/'manager'/'employee'` | Один `def map_hr_role()` |
|
||||
| `middleware/auth.js` | `authenticate`, `requireRole`, `requireDepartment`, `optionalAuth` | Flask-декоратор `@login_required`/`@roles_required` (или `before_request`) |
|
||||
| `config/featureFlags.js` | `isAssignmentFeatureEnabled` | Простая функция от env |
|
||||
| `config/devAuthor.js` | `isTestAuthor(createdBy, userId)` | Один сравнительный helper |
|
||||
| `config/authConstants.js` | `HR_MANAGED_PASSWORD_PLACEHOLDER`, `isHrAuthEnabled` | Без изменений |
|
||||
| `messages/ru.js` | Текстовые сообщения API | `app/messages/ru.py` (dict) |
|
||||
|
||||
> **Особое внимание:** `testAttemptService.submitAttempt` использует `FOR UPDATE` и выгружает все `answer_options` версии разом. Простую построчную проверку «правильно/нет» делает Python-функция `same_selection(set, set)`. На больших тестах помогает индекс `idx_answer_options_question_id` — он уже есть.
|
||||
|
||||
---
|
||||
|
||||
## 3. Базы данных и схемы
|
||||
|
||||
### 3.1 Основная — `clinic_tests` (`DATABASE_URL`)
|
||||
|
||||
Из `backend/src/db/migrations/001_initial.sql`:
|
||||
|
||||
- `departments` (UUID, name)
|
||||
- `users` (UUID, login UNIQUE, password_hash, full_name, role `user_role`, department_id FK, is_active, **`staff_id`** — миграция 003, **UNIQUE**)
|
||||
- `tests` (UUID, title, description, passing_threshold, time_limit, allow_back, is_active, is_versioned, created_by FK)
|
||||
- `test_versions` (UUID, test_id FK, version, is_active; миграция 002 добавляет `parent_id` + частичный уникальный индекс «одна активная версия на цепочку»)
|
||||
- `questions` (UUID, test_version_id FK, text, question_order, has_multiple_answers)
|
||||
- `answer_options` (UUID, question_id FK, text, is_correct, option_order)
|
||||
- `test_assignments` (UUID, test_version_id FK, assigned_by FK, deadline DATE, max_attempts)
|
||||
- `test_assignment_targets` (UUID, assignment_id FK, target_type `'department'|'user'`, target_id UUID)
|
||||
- `test_attempts` (UUID, test_version_id FK, user_id FK, attempt_number, status `attempt_status`, started_at, completed_at, correct_count, total_questions, passed; UNIQUE(test_version_id, user_id, attempt_number))
|
||||
- `user_answers` (UUID, attempt_id FK, question_id FK, selected_options **UUID[]**)
|
||||
- `migrations` (служебная, имена применённых SQL)
|
||||
|
||||
**Расширения:** `uuid-ossp`. **Кастомные типы:** `user_role`, `target_type`, `attempt_status`.
|
||||
|
||||
### 3.2 Дополнительная — `hr_bot_test` (`HR_DATABASE_URL`, опционально)
|
||||
|
||||
Используется только на чтение через `db/hrPool.js`:
|
||||
|
||||
- `users` — для входа (HR_AUTH=1): `id`, `username`, `password_hash` (Werkzeug), `role`
|
||||
- `staff_members` — `id`, `fio`, `web_login`
|
||||
- `employees_departments` — `staff_id`, `department`
|
||||
|
||||
### 3.3 Миграции
|
||||
|
||||
3 SQL-файла в `backend/src/db/migrations/`. Простой `npm run migrate` пишет в служебную таблицу `migrations`. В Flask эквивалент: **Alembic** или ручной runner на `psycopg2`. Файлы `.sql` можно переиспользовать **как есть** — никакого Node-специфичного синтаксиса в них нет.
|
||||
|
||||
---
|
||||
|
||||
## 4. Зависимости Node → Python (план замены)
|
||||
|
||||
| Node-пакет | Python-замена | Комментарий |
|
||||
|-----------|----------------|-------------|
|
||||
| `express`, `cors`, `cookie-parser` | **Flask** + `flask-cors` | сессии встроены |
|
||||
| `multer` | `Flask` (`request.files`) + `werkzeug.utils.secure_filename` + `tempfile` | лимит 10 МБ — `MAX_CONTENT_LENGTH` |
|
||||
| `dotenv` | `python-dotenv` (уже есть в `flask_app/run.py`) | — |
|
||||
| `pg` | `psycopg2-binary` (или `psycopg[binary]` v3) | — |
|
||||
| `bcryptjs` | `bcrypt` (`pip install bcrypt`) | формат `$2b$…` совместим |
|
||||
| `jsonwebtoken` | `pyjwt` или `flask-jwt-extended` | важен **тот же** алгоритм/секрет, чтобы старые cookie работали в переходный период |
|
||||
| `mammoth` | `mammoth` (PyPI: `pip install mammoth`) или `python-docx` | API почти идентичен |
|
||||
| `pdf-parse` | `pypdf` (`pip install pypdf`) или `pdfminer.six` | `pypdf.PdfReader().pages[].extract_text()` |
|
||||
| `fetch` (LLM) | `httpx` (рекомендую — есть `timeout`, async) или `requests` | сохранить `Authorization: Bearer …`, `response_format: json_object` |
|
||||
|
||||
**Уже в `tgFlaskForm` есть готовое** — `werkzeug.security.check_password_hash` (бесплатно), Sentry, Jinja2-шаблоны, обмен с `staff_members`. Переиспользовать, если выберем сценарий «общий кабинет».
|
||||
|
||||
---
|
||||
|
||||
## 5. Переменные окружения (полный список)
|
||||
|
||||
| Переменная | Где читается | Назначение | Обязательная |
|
||||
|-----------|--------------|-----------|--------------|
|
||||
| `NODE_ENV` | `app.js`, `auth.js`, `featureFlags.js`, `db.js` | dev vs production | да (нет → dev) |
|
||||
| `PORT` | `server.js` | Express, по умолчанию 3001 | нет |
|
||||
| `FRONTEND_URL` | `app.js` (CORS) | разрешённый origin в prod | в prod — да |
|
||||
| `DATABASE_URL` **или** `DB_HOST`/`DB_PORT`/`DB_NAME`/`DB_USER`/`DB_PASSWORD` | `db/poolConfig.js` | основная БД `clinic_tests` | да |
|
||||
| `DB_POOL_MAX`, `DB_IDLE_TIMEOUT`, `DB_CONNECTION_TIMEOUT` | `db/poolConfig.js` | пул | нет |
|
||||
| `HR_DATABASE_URL` | `db/poolConfig.js` (`getHrPoolConfig`) | hr_bot_test (read-only) | при HR_AUTH=1 — да |
|
||||
| `HR_DB_POOL_MAX` | то же | пул HR | нет |
|
||||
| `HR_AUTH` (1/true) | `config/authConstants.js` | вход по HR-логину | нет |
|
||||
| `JWT_SECRET` | `utils/auth.js` | подпись JWT | **да** |
|
||||
| `JWT_EXPIRES_IN` | `utils/auth.js` | дефолт `7d` | нет |
|
||||
| `DEEPSEEK_API_KEY` | `services/llmClient.js` | LLM, приоритет №1 | для AI/импорта — да |
|
||||
| `OPENAI_API_KEY` | то же | LLM, приоритет №2 | альтернатива |
|
||||
| `LLM_BASE_URL` | то же | сменить хост (proxy / vLLM) | нет |
|
||||
| `LLM_MODEL` | то же | по умолчанию `deepseek-chat` или `gpt-4o-mini` | нет |
|
||||
| `LLM_NO_JSON` (1) | то же | отключить `response_format: json_object` (для моделей без поддержки) | нет |
|
||||
| `CLINIC_ASSIGNMENT_ENABLED` (1) | `config/featureFlags.js` | прод: включить assign | в prod — да |
|
||||
|
||||
В `flask_app/.env` нужно перенести **те же ключи** (имена можно сохранить, чтобы не плодить вариации).
|
||||
|
||||
---
|
||||
|
||||
## 6. Что вызывает фронтенд (карта зависимостей React → API)
|
||||
|
||||
Используется один тонкий клиент `frontend/src/api.js`: `fetch` с `credentials: 'include'`, базовый путь — пустой (т.е. **`/api/...` относительно текущего origin**, что в dev резолвит Vite-proxy, а в prod — Nginx). Это значит:
|
||||
|
||||
- **Менять фронтенд при смене бэкенда не нужно**, если новый сервис отвечает по тем же путям.
|
||||
- В dev сейчас `vite.config.js` проксирует `/api` на Express (`localhost:3001`). После переноса — заменить адрес/порт на Flask (см. `flask_app/run.py`, по умолчанию `3108`).
|
||||
|
||||
Список путей (отсортирован по убыванию использований):
|
||||
|
||||
```
|
||||
/api/auth/login
|
||||
/api/auth/logout
|
||||
/api/auth/me
|
||||
/api/auth/dev/assignment-directory?q&department&clinic
|
||||
/api/tests
|
||||
/api/tests/:id/summary
|
||||
/api/tests/:id/versions
|
||||
/api/tests/:id/editor
|
||||
/api/tests/:id/chain-info
|
||||
/api/tests/:id/draft
|
||||
/api/tests/:id/assign
|
||||
/api/tests/:id/ai/generate-test
|
||||
/api/tests/:id/ai/generate-question
|
||||
/api/tests/:id (PATCH)
|
||||
/api/tests/:id/versions/:vid/activate
|
||||
/api/tests/:id/attempts
|
||||
/api/tests/:id/attempts/start
|
||||
/api/tests/:id/attempts/:aid/play
|
||||
/api/tests/:id/attempts/:aid/review
|
||||
/api/tests/:id/attempts/:aid/submit
|
||||
/api/tests/import/document
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Тесты, которые тянутся за собой
|
||||
|
||||
В `backend/src/**/*.test.js` (Node test runner):
|
||||
|
||||
- `apiSmoke.test.js` — smoke-проверки HTTP.
|
||||
- `services/testChainService.test.js`
|
||||
- `services/aiEditorService.test.js`
|
||||
- `services/documentGenService.test.js`
|
||||
- `services/documentExtractService.test.js`
|
||||
- `utils/werkzeugPassword.test.js`
|
||||
- `integration/v9card1.test.js` (требует `CLINIC_TESTS_INTEGRATION=1`)
|
||||
|
||||
После переноса нужны их Python-аналоги (`pytest`). Часть (валидация LLM-формы, разбор Werkzeug) тривиально превращается в юниты.
|
||||
|
||||
---
|
||||
|
||||
## 8. Чего сейчас в `flask_app/` НЕТ (чтобы не повторяться в Спринте 2)
|
||||
|
||||
`flask_app/` содержит только: `run.py`, `app/__init__.py` (с `/health` и `/`), `app/templates/`, `app/static/`. Не реализовано **ничего** из перечня §1–§4. Это значит, что в Спринте 2 нужно с нуля поднять:
|
||||
|
||||
1. Подключение к БД (минимум — основной пул `clinic_tests`).
|
||||
2. Декоратор аутентификации (cookie JWT) и эндпоинты `/api/auth/*`.
|
||||
3. Роуты `/api/tests/*` поэтапно — начать с **read-only** (list, summary, versions, editor, attempts, review), потом write (draft, activate, patch, assign, attempts/start/submit), потом AI/import.
|
||||
4. CORS (в dev совпадает с тем, что у Express).
|
||||
5. Запуск под Vite/Nginx — обновить proxy/upstream.
|
||||
|
||||
---
|
||||
|
||||
## 9. Критерии «можно удалять `backend/`» (Спринт 4)
|
||||
|
||||
- ETL-скрипт `HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py` прогнан **на копии прод**, агрегаты совпадают (тесты, версии, вопросы, попытки) — затем прогон на проде в окне.
|
||||
- Все сценарии §1 (TestingWebApp) либо имеют **рабочий аналог** в `cabinet/testing/`, либо явно **признаны не-MVP** (см. §10).
|
||||
- Внутренние ссылки и закладки сотрудников переключены на URL общего кабинета (`/cabinet/testing/...`).
|
||||
- Документация (`README.md`, `DEV_CONTOUR_USER_GUIDE.md`, `шаги/*`) перестала ссылаться на `backend/` и `frontend/` TestingWebApp.
|
||||
- Резерв: ветка `legacy/clinic-tests-node` (или `legacy/express-backend`) зафиксирована перед удалением.
|
||||
|
||||
---
|
||||
|
||||
## 10. Gap-analysis: `tgFlaskForm/cabinet/testing` vs Express
|
||||
|
||||
Сверка проведена по фактическому коду `HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/*` и `HR_TG_Bot/tgFlaskForm/db/queries/testing_queries.py` на 2026-04-27. Колонка «Что нужно сделать» — это и есть остаток Спринта 2 из [migration-to-tgflaskform.md](migration-to-tgflaskform.md).
|
||||
|
||||
### 10.1 Маппинг сущностей и ID
|
||||
|
||||
| TestingWebApp (`clinic_tests`) | tgFlaskForm (`hr_bot_test`, схема `testing_*`) | Замечание |
|
||||
|---|---|---|
|
||||
| `tests` (UUID) | `testing_tests` (Integer PK) | UUID → integer; маппинг хранится в `_clinic_tests_migration_map` |
|
||||
| `test_versions` (UUID, `version`, `is_active`, `parent_id`) | `testing_test_versions` (Integer, `version_number`, `is_active_version`) — **+** `passing_score_percent`, `time_limit_minutes`, `allow_back_navigation` (нормализовано на версию) | В Express часть полей лежала на `tests` (`passing_threshold`, `time_limit`, `allow_back`); ETL поднимает их на версию |
|
||||
| `questions` (`text`, `has_multiple_answers`, `question_order`) | `testing_questions` (`question_text`, `question_type` `'single'\|'multiple'`, `sort_order`) | `has_multiple_answers: true` ↔ `question_type='multiple'` |
|
||||
| `answer_options` (`text`, `is_correct`, `option_order`) | `testing_answers` (`answer_text`, `is_correct`, `sort_order`) | прямой маппинг |
|
||||
| `users` + `users.staff_id` | `staff_members.id` напрямую | Express держит «свою» строку `users` со ссылкой на `staff_id`; в HR — только `staff_members` |
|
||||
| `test_assignments` + `test_assignment_targets` (target_type='user', target_id=UUID) | `testing_assignments` (одна строка = одна пара тест×сотрудник, `assigned_to=staff_id`) | **Существенное расхождение модели** — см. §10.3 |
|
||||
| `test_attempts` (привязан к `test_version_id`, `user_id`) | `testing_attempts` (привязан к `assignment_id` + `test_version_id`, попытка от **сотрудника**) | Если в clinic попытка не имеет связанного assignment — ETL создаёт синтетическое назначение (`max_attempts=99`) |
|
||||
| `user_answers` (`selected_options uuid[]`) | `testing_attempt_answers` (одна строка на каждый выбранный `answer_id`) | Развёртка массива в N строк |
|
||||
| (нет аналога) | `testing_settings` (key-value), `testing_head_positions` (право назначения по должности) | в Express не было — RBAC проще |
|
||||
|
||||
### 10.2 Эндпоинты Express → роуты `tgFlaskForm`
|
||||
|
||||
| # | Express | Соответствие в `tgFlaskForm` | Статус | Что сделать |
|
||||
|---|---------|------------------------------|--------|-------------|
|
||||
| 1 | `POST /api/auth/login` | `webApp/auth/*` (общая сессия HR) | ✅ есть аналог | Express-сессии не переносим; пользователи входят как обычно в HR-кабинет |
|
||||
| 2 | `POST /api/auth/logout` | `webApp/auth/*` | ✅ — | — |
|
||||
| 3 | `GET /api/auth/me` | session + `helpers._get_staff_id` | ✅ — | — |
|
||||
| 4 | `GET /api/auth/dev/assignment-directory` | `routes_assignments.assign_form` (страница) + `testing_get_employees_for_assignment` | ✅ — | На UI: добавить «Выбрать всех» (см. Спринт 3 React-кабинета), мульти-фильтры |
|
||||
| 5 | `GET /api/health` | `webApp/__init__.py` | ✅ — | — |
|
||||
| 6 | `GET /api/tests` (каталог + `hiddenByYou`) | `routes_tests.test_list` (по автору) + `routes_passing.my_tests` (по назначениям) | ⚠️ нет «скрытые мной» отдельным разделом | Добавить вкладку/фильтр «скрытые автором» в `cabinet/testing/test_list.html` |
|
||||
| 7 | `POST /api/tests` (создать пустой) | `routes_tests.test_create` (требует full payload + ≥7 вопросов) | ⚠️ другой контракт | Решить продуктово: оставить ограничение `≥7` либо разрешить «пустой тест» как в Express. **Рекомендую** разрешить пустой и валидировать только при публикации |
|
||||
| 8 | `GET /api/tests/:id/summary` | вшито в `test_edit` | ⚠️ нет отдельной summary | Добавить функцию `testing_get_test_summary(test_id)` если нужен лёгкий вариант для списка |
|
||||
| 9 | `GET /api/tests/:id/versions` | `testing_get_test_for_edit` отдаёт активную; история — внутри `test_edit` | ⚠️ — | Вынести список версий в API/шаблон («История» в UX) |
|
||||
| 10 | `GET /api/tests/:id/editor` | `routes_tests.test_edit` + `testing_get_test_for_edit` | ✅ — | сверить набор полей, отдаваемых шаблону, с тем что показывает React-редактор |
|
||||
| 11 | `POST /api/tests/:id/ai/generate-test` (строгая `shape: [{optionsCount, hasMultipleAnswers}]`, до 40 вопросов) | `routes_ai.ai_generate` принимает `topic, question_count` (фикс. форма) | ❌ **gap** | Добавить вариант с явной формой (передавать массив `optionsCount/hasMultipleAnswers`); либо принять, что HR-кабинет генерирует «обобщённо» |
|
||||
| 12 | `POST /api/tests/:id/ai/generate-question` (один вопрос: пусто=новый, текст=переформулировать) | `routes_ai.ai_improve` (улучшить) + `ai_distractors` (варианты) | ❌ **gap** | Добавить ручку «один вопрос со строгой сеткой» (объединить `improve` + `distractors` под единый сценарий) |
|
||||
| 13 | `POST /api/tests/:id/versions/:vid/activate` | `routes_tests.test_version_activate` | ✅ — | — |
|
||||
| 14 | `PATCH /api/tests/:id` (`chainActive`) | `routes_tests.test_toggle` | ✅ — | сверить семантику (toggle vs явный флаг) |
|
||||
| 15 | `POST /api/tests/:id/assign` (массивы `userIds`/`staffIds`) | `routes_assignments.assign_submit` (`employee_ids = staff_id[]`) | ✅ совместимо | — |
|
||||
| 16 | `POST /api/tests/:id/draft` (с авто-fork версии) | `routes_tests.test_update` (тоже фаркает при `has_attempts`) | ⚠️ контракт другой | Сверить, что в HR-кабинете тоже работает «правка без попыток = на месте, после попыток = новая версия». В коде есть, но нужна проверка |
|
||||
| 17 | `POST /api/tests/:id/attempts/start` | `routes_passing.start_attempt(assignment_id)` | ⚠️ другой ключ | Привести к виду «start by test_id» **или** оставить как есть и в UI стартовать по `assignment_id` (логичнее) |
|
||||
| 18 | `GET /api/tests/:id/attempts` | `routes_results.tracker` (общая лента) — без фильтра по тесту | ⚠️ нет per-test ленты | Добавить фильтр `test_id` или отдельный URL `/cabinet/testing/tests/:id/attempts` |
|
||||
| 19 | `GET /api/tests/:id/attempts/:aid/review` | `routes_results.result(attempt_id)` | ✅ — | сверить, что разбор показывает выбранные/верные варианты по каждому вопросу |
|
||||
| 20 | `GET /api/tests/:id/attempts/:aid/play` | `routes_passing.take_test(attempt_id)` | ✅ — | — |
|
||||
| 21 | `POST /api/tests/:id/attempts/:aid/submit` | `routes_passing.finish_attempt(attempt_id)` (+ `save_answer`) | ⚠️ контракт другой | Express отправляет все ответы пакетом; HR — пошагово (`save_answer` × N + `finish`). Это **OK для UX** (продолжить позже), но фронт работает иначе |
|
||||
| 22 | `POST /api/tests/import/document` | `routes_import.import_document` | ✅ — | сверить mime-типы и сообщения об ошибках |
|
||||
| 23 | `GET /api/tests/:id/chain-info` (`hasAnyAttempt`) | вшито в `testing_get_test_for_edit['has_attempts']` | ✅ — | — |
|
||||
|
||||
> **Итог по gap-analysis:** 13 из 22 эндпоинтов закрыты «как есть» или близко; 4 требуют добавления (AI «один вопрос», AI «строгая сетка», per-test attempts, summary); 5 — расхождение контракта, надо принять решение или подровнять.
|
||||
|
||||
### 10.3 Расхождения, которые ETL уже обходит
|
||||
|
||||
- **Назначения на отдел:** в `clinic_tests.test_assignment_targets` могут быть строки `target_type='department'`. ETL разворачивает их в N строк `testing_assignments` по списку сотрудников отдела на момент миграции (см. шапку `tools/migrate_clinic_tests_to_hr.py`).
|
||||
- **Попытки без явного назначения:** в Express `test_attempts.user_id` напрямую идёт от пользователя; в HR попытка обязательно имеет `assignment_id`. ETL для каждой пары (тест, сотрудник) создаёт **синтетическое** назначение `max_attempts=99` и привязывает попытку к нему.
|
||||
- **UUID → Integer:** идемпотентный маппинг лежит в `public._clinic_tests_migration_map` (entity, old_uuid → new_id) — повторный прогон ETL безопасен.
|
||||
|
||||
### 10.4 Чего точно нет в `tgFlaskForm` и потребует доработки
|
||||
|
||||
1. **AI «один вопрос со строгой сеткой»** (`generate_or_rephrase_question` из `aiEditorService.js`).
|
||||
2. **AI «целый тест по shape»** с проверкой `optionsCount` и `hasMultipleAnswers` для каждой строки.
|
||||
3. **«Скрытые автором» цепочки** как отдельный список на UI.
|
||||
4. **`/tests/:id/summary`** — лёгкая ручка для карточки в списке (если потребуется в новом UI).
|
||||
5. **Per-test лента попыток** (UI «Прохождения» в аккордеоне «История»).
|
||||
6. **Мобильный UX из [Спринта 3](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md):** аккордеоны «О тесте / Вопросы / История / Показ в каталоге», фикс-футер, икон-кнопка удаления варианта, «Выбрать всех» в назначении и т.д. В Этапе 1 это переносится в Jinja-шаблоны `flask_app/app/templates/`. В Этапе 2 уже готовые шаблоны переезжают в `tgFlaskForm/webApp/templates/cabinet/testing/`.
|
||||
|
||||
### 10.5 Что точно НЕ переносим
|
||||
|
||||
- **Свой JWT/cookie/bcrypt** из `backend/src/utils/auth.js` и `middleware/auth.js`. Авторизация — через сессии общего кабинета.
|
||||
- **Werkzeug-полифилл в Node** (`utils/werkzeugPassword.js`). В Python это родная функция `werkzeug.security.check_password_hash`.
|
||||
- **Свои миграции SQL** из `backend/src/db/migrations/`. Целевая схема живёт в `tgFlaskForm/db/models.py` и сопутствующих DDL/Alembic.
|
||||
- **`flask_app/`** в этом репозитории — каркас уже **не нужен**, в Спринте 4 решим: удалить или оставить как историческую заготовку.
|
||||
- **`backend/src/**/*.test.js`** (Node test runner). Их предметная нагрузка покрывается **либо** уже существующими тестами `tgFlaskForm`, **либо** новыми pytest-юнитами.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Унификация стека TestingWebApp с `tgFlaskForm` — план и журнал
|
||||
|
||||
> **Корректировка курса от 2026-04-27.**
|
||||
> Ранее в этом документе фигурировал «полный переезд в HR-кабинет (`tgFlaskForm`) с cutover'ом и удалением React/Express». Это было забеганием вперёд. Текущая фаза — **только унификация стека**, без слияния репозиториев и без миграции данных.
|
||||
|
||||
---
|
||||
|
||||
## Назначение документа
|
||||
|
||||
Зафиксировать разделение работы на **два этапа** и текущий статус каждого. Этот файл — **трекер решения и журнал**, основной план Этапа 1 — здесь же; план Этапа 2 (на будущее) — в [migration-to-tgflaskform.md](migration-to-tgflaskform.md).
|
||||
|
||||
**Связано:** [migration-final-inventory.md](migration-final-inventory.md) (карта 22 эндпоинтов Express, БД, env, зависимости, плюс справочный gap-analysis с уже существующим модулем в `tgFlaskForm` — пригодится в Этапе 2), [PROJECT_STATUS.md](PROJECT_STATUS.md), [README](../README.md), [`flask_app/README.md`](../flask_app/README.md), [СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md).
|
||||
|
||||
---
|
||||
|
||||
## Этап 1 (текущий) — единый стек: Express → Flask, React → Jinja, **внутри TestingWebApp**
|
||||
|
||||
### Цель
|
||||
|
||||
Привести TestingWebApp к **тому же стеку**, что у `HR_TG_Bot/tgFlaskForm`:
|
||||
|
||||
- **Бэкенд:** Python 3 + Flask, точечный SQL/SQLAlchemy в стиле `tgFlaskForm`. Развивается в каталоге [`flask_app/`](../flask_app/) этого репозитория (сейчас — минимальный каркас).
|
||||
- **Фронтенд:** Jinja-шаблоны в `flask_app/app/templates/`, мобильный UX из [Спринта 3](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md) переносится один в один. React (`frontend/`) уходит **после** того, как Jinja-версия закроет все экраны.
|
||||
- **БД:** **остаётся `clinic_tests`** (со своими UUID-ключами). Никаких изменений схемы.
|
||||
- **Авторизация:** JWT/bcrypt + опциональный `HR_AUTH=1` (как в Express) — переносим как есть.
|
||||
|
||||
### Что **не** делаем в Этапе 1
|
||||
|
||||
- **Не** трогаем `HR_TG_Bot/tgFlaskForm/`. Его модуль `cabinet/testing` живёт своей жизнью.
|
||||
- **Не** мигрируем данные `clinic_tests → hr_bot_test`. ETL-скрипт `migrate_clinic_tests_to_hr.py` есть, но он — для Этапа 2.
|
||||
- **Не** удаляем `backend/` и `frontend/` сразу. Они работают параллельно с `flask_app/` до полного паритета. Удаление — последним PR этапа.
|
||||
|
||||
### Стартовая точка `flask_app/`
|
||||
|
||||
| Что есть | Файл / артефакт |
|
||||
|---|---|
|
||||
| Flask-приложение (`create_app`) | `flask_app/app/__init__.py` |
|
||||
| Точка входа (dev / waitress) | `flask_app/run.py` |
|
||||
| `/health` | `flask_app/app/__init__.py` |
|
||||
| Пустой `index.html` | `flask_app/app/templates/index.html` |
|
||||
| Зависимости | `flask_app/requirements.txt` (Flask, python-dotenv, waitress) |
|
||||
| Docker-сервис на порту 3108 | `flask_app/Dockerfile`, корневой `docker-compose.dev.yml` (сервис `testing-flask`) |
|
||||
|
||||
Всё остальное — **писать**.
|
||||
|
||||
### План Этапа 1 (по спринтам)
|
||||
|
||||
| Спринт | Цель | Артефакты |
|
||||
|---|---|---|
|
||||
| **E1.0 — База Flask-приложения** ✅ | БД-пул (SQLAlchemy + psycopg2), Flask sessions через `SECRET_KEY`, конфиг через `.env`, структура blueprint'ов, шаблон `base.html` в стиле кабинета, обработчики 404/500, `/health` с проверкой БД. **Без бизнес-логики.** | `flask_app/app/db.py`, `flask_app/app/__init__.py`, `flask_app/app/blueprints/main.py`, `flask_app/app/templates/{base,index,404,500}.html`, `flask_app/app/static/css/app.css`, обновлённые `requirements.txt` и `.env.example` |
|
||||
| **E1.1 — Auth и `/api/me`** ✅ | Flask sessions (signed cookie), bcrypt + Werkzeug (`werkzeug.security.check_password_hash`), опц. `HR_AUTH=1` с UPSERT в `clinic_tests.users` по `staff_id`. UI-страница `/login`, JSON-API `/api/auth/{login,logout,me}`, декораторы `login_required`/`require_role`, `current_user` доступен в шаблонах. | `flask_app/app/auth/{routes,services,decorators,hr_role}.py`, `flask_app/app/{config,messages}.py`, `flask_app/app/templates/auth/login.html`, обновлены `base.html`, `__init__.py`, `requirements.txt` (+`bcrypt`) |
|
||||
| **E1.2 — Тесты: список и редактор** ✅ | Перенесены 10 эндпоинтов из Express: `GET/POST /api/tests`, `GET /api/tests/:id/{summary,versions,editor}`, `POST /api/tests/:id/draft`, `POST /api/tests/:id/versions/:vid/activate`, `PATCH /api/tests/:id`, `POST /api/tests/:id/ai/{generate-test,generate-question}`. UI: `/tests` (каталог + создание), `/tests/:id/edit` (рабочий редактор с AI). Полная мобильная отполировка UX (4 аккордеона + fixed footer + drag-n-drop) — в **E1.7**. | `flask_app/app/services/{llm_client,draft_validator,ai_editor,test_access,test_chain,test_draft,editor_content}.py`, `flask_app/app/tests/{__init__,routes}.py`, `flask_app/app/templates/tests/{list,editor}.html`, `flask_app/app/static/js/editor.js`, обновлены `base.html`, `index.html`, `__init__.py` |
|
||||
| **E1.3 — Импорт документов** ✅ | `POST /api/tests/import/document` (PDF/DOCX/TXT/MD извлечение текста через `pypdf` и `python-docx`), интеграция с AI-генерацией черновика (`generation_for_import_document`), кнопка «Импорт документа» в AI-панели редактора, лимит 16 МБ. | `flask_app/app/services/{document_extract,document_gen}.py`, эндпоинт в `flask_app/app/tests/routes.py`, кнопка в `editor.html` + `editor.js`, `requirements.txt` (+`pypdf`, `python-docx`) |
|
||||
| **E1.4 — Назначение и прохождение** | Эндпоинты `assign`, `attempts/start`, `attempts/:id/play`, `attempts/:id/submit`, `attempts/:id/review`. Шаблоны: `assign.html`, `take_test.html`, `test_result.html`. | `flask_app/app/assignments/`, `flask_app/app/attempts/`, шаблоны |
|
||||
| **E1.5 — Трекер и настройки** | Трекер прохождений, настройки модуля (ключи AI и т.д.), цепочки тестов. Шаблоны `tracker.html`, `settings.html`. | `flask_app/app/tracker/`, `flask_app/app/settings/` |
|
||||
| **E1.6 — Cutover внутри репозитория** | `docker-compose.dev.yml` указывает на `flask_app/` как основной сервис; Nginx маршрутизирует `/api` и UI на новый Flask. Удаление `backend/` и `frontend/` отдельным PR. README → актуальные команды. | `docker-compose.dev.yml`, корневой `README.md`, `frontend/` и `backend/` удаляются |
|
||||
| **E1.7 — UX-полировка редактора** | Перевод базового редактора (E1.2) на мобильный UX из [Спринта 3](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md): 4 аккордеона (Шапка / AI-помощник / Вопросы / Действия), sticky footer, drag-n-drop вопросов, импорт документа в подразделе AI-блока (после E1.3). | `flask_app/app/templates/tests/editor.html`, `flask_app/app/static/js/editor.js`, новый `static/css/testing.css` |
|
||||
| **E1.8 — AI-функции v2** ✅ | `/settings` (статус ключа из ENV + ping), `POST /api/llm/ping`, на тесте — `ai/generate-by-title` (без сетки), `ai/check` (рецензия), `ai/improve` (массовое «было → стало» с чекбоксами). На уровне вопроса — уже есть `ai/generate-question` из E1.2 (создаёт вопрос или переформулирует). Все AI-эндпоинты унифицированы: при отсутствии ключа — `{ error, code, settingsUrl: '/settings' }`. | `flask_app/app/services/{ai_editor,llm_client}.py`, `flask_app/app/blueprints/settings.py`, `flask_app/app/templates/settings.html`, ссылка «Настройки» в `base.html`, обновлены `tests/routes.py`, `editor.html`, `editor.js` |
|
||||
|
||||
### Критерии готовности Этапа 1
|
||||
|
||||
- Все 22 эндпоинта Express (см. [migration-final-inventory.md](migration-final-inventory.md)) реализованы в `flask_app/` и проходят smoke-тесты.
|
||||
- Все экраны мобильного UX из [Спринта 3](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md) воспроизведены в Jinja.
|
||||
- В `docker-compose.dev.yml` остался **один** сервис приложения (Flask). `backend/` и `frontend/` удалены или перенесены в ветку `legacy/clinic-tests-node`.
|
||||
- БД — по-прежнему `clinic_tests`, схема не менялась.
|
||||
|
||||
---
|
||||
|
||||
## Этап 2 (на будущее, без сроков) — слияние с `tgFlaskForm`
|
||||
|
||||
Когда заказчик решит «вот теперь объединяем» — **вся** разработанная Flask-логика и шаблоны легко переносятся в `HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/` и `templates/cabinet/testing/`, потому что **стек уже совпадает**. Это и есть смысл Этапа 1.
|
||||
|
||||
Что нужно сделать в Этапе 2 (план — в [migration-to-tgflaskform.md](migration-to-tgflaskform.md)):
|
||||
|
||||
1. Перенести код Flask-приложения как blueprint в `tgFlaskForm`.
|
||||
2. Адаптировать модели под существующие `Testing*` таблицы (`hr_bot_test.testing_*`).
|
||||
3. Перевести авторизацию на сессии общего HR-кабинета.
|
||||
4. Прогнать ETL `clinic_tests → hr_bot_test` (скрипт `HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py` уже готов: 437 строк, режимы `--dry-run`/`--apply`, идемпотентность через `_clinic_tests_migration_map`).
|
||||
5. Cutover (если к тому моменту появятся реальные пользователи; сейчас TestingWebApp — песочница для тестировщиков).
|
||||
|
||||
**Решения, которые относятся к Этапу 2** (зафиксированы заранее, чтобы потом не переоткрывать):
|
||||
|
||||
- **`test_assignments`:** переносим 1:1, дописывая отдельный блок в ETL (сейчас скрипт переносит только пары через попытки).
|
||||
- **Пользователи без `staff_id`:** игнорируем с WARN; по договорённости настоящие пользователи всегда привязаны к `staff_members`.
|
||||
- **Cutover / окно простоя:** не нужны, пока TestingWebApp остаётся песочницей.
|
||||
|
||||
---
|
||||
|
||||
## Журнал
|
||||
|
||||
| Дата | Что сделано |
|
||||
|------|-------------|
|
||||
| 2026-04-27 | Спринт 0 («инвентаризация» в старой нумерации) закрыт: артефакт [migration-final-inventory.md](migration-final-inventory.md) — карта 22 эндпоинтов Express, БД, env, зависимости. |
|
||||
| 2026-04-27 | Принято решение: **сценарий B + b1** (полный переезд в HR-кабинет). |
|
||||
| 2026-04-27 | **Курс скорректирован:** Этап 1 = унификация стека внутри TestingWebApp (Express → Flask + React → Jinja, БД остаётся `clinic_tests`). Этап 2 = слияние с `tgFlaskForm` — на будущее. ETL и удаление React переходят в Этап 2. Документы переписаны под двух-этапную картину. Эксперимент с правкой `tgFlaskForm/cabinet/testing/test_editor.html` (ветка `feat/testing-editor-jinja-redesign`) откачен и не оставил следов в HR-репо. |
|
||||
| 2026-04-27 | **E1.8 закрыт.** AI v2: страница `/settings` (статус ключа из ENV, `Проверить подключение` → `POST /api/llm/ping`). Три новых эндпоинта на тесте: `POST /api/tests/<id>/ai/generate-by-title` (генерация только по названию + опции «сколько вопросов / сколько вариантов»), `POST /api/tests/<id>/ai/check` (рецензия: вердикт + разделы рекомендаций), `POST /api/tests/<id>/ai/improve` (массовое «было → стало» с проверкой неизменности сетки). UI редактора: кнопки «Сгенерировать по названию», «Проверить тест», «Улучшить тест»; общий `<dialog>` для модалок check/improve; чекбоксы в improve позволяют применять изменения по выбранным вопросам. Все AI-эндпоинты унифицированы: при отсутствии ключа возвращают `{ error, code, settingsUrl: '/settings' }` 502 — фронт предлагает открыть Настройки. |
|
||||
| 2026-04-27 | **E1.3 закрыт.** Импорт документов: `app/services/document_extract.py` (PDF через `pypdf`, DOCX через `python-docx`, TXT/MD), `app/services/document_gen.py` (`generation_for_import_document` — извлекает текст, при наличии LLM-ключа просит модель собрать draft через `validate_and_normalize_draft`), эндпоинт `POST /api/tests/import/document` под `@login_required` с лимитом 16 МБ. UI редактора: кнопка «Импорт документа» в AI-панели, после загрузки — confirm с предложением применить черновик; если ключа нет — алерт с превью текста. В `requirements.txt` добавлены `pypdf>=4` и `python-docx>=1.1`. |
|
||||
| 2026-04-27 | **E1.2 закрыт.** Перенесены `backend/src/routes/tests.js` (только E1.2-эндпоинты — без `import/document`/`assign`/`attempts`/`chain-info`, они уйдут в E1.3-E1.5) + сервисы `testDraftService.js`, `testAccessService.js`, `testChainService.js`, `aiEditorService.js`, `documentGenService.js` (только парсер JSON и валидатор draft), `llmClient.js`, `getEditorContent` из `testAttemptService.js`. Эндпоинты регистрируются в blueprint `tests`. AI-генерация: `parseAndValidateShape` 1:1, ошибки LLM (`llm_*`-коды) пробрасываются как 502 с кодом в JSON. UI: каталог тестов с кнопкой создания (модалка `<dialog>`) и рабочий редактор (inline-поля, AI-кнопки «весь тест» / «один вопрос», добавление/удаление/перемещение вопросов и вариантов, сохранение черновика, переключатель «Цепочка активна»). Полный мобильный UX редактора (аккордеоны+fixed footer+drag-n-drop из Спринта 3) вынесен в новый спринт **E1.7** — этот PR закрывает функциональность, не дизайн. |
|
||||
| 2026-04-27 | **E1.1 закрыт.** Перенесены `backend/src/routes/auth.js` + `middleware/auth.js` + `utils/{auth,werkzeugPassword,hrRoleMap}.js` в `flask_app/app/auth/`. Решение: Flask sessions (signed cookie) вместо JWT, как договорились (вариант A). Поддерживаются bcrypt-хеши (`$2*`) и Werkzeug-хеши (`scrypt:`/`pbkdf2:`). Эндпоинты — те же пути, что в Express: `POST /api/auth/login`, `POST /api/auth/logout`, `GET /api/auth/me` (отдаёт `user`, `devUi`, `assignmentUi`). Дополнительно — HTML-страница `/login` (форма) и `POST /logout`. Декораторы: `@login_required`, `@require_role(...)`. В шаблонах доступны `current_user`, `hr_auth_enabled`, `dev_ui`, `assignment_ui`. Защита от open-redirect в параметре `?next=`. Главная (`/`) теперь требует логин. |
|
||||
| 2026-04-27 | **E1.0 закрыт.** В `flask_app/`: SQLAlchemy/psycopg2-пул в стиле `tgFlaskForm/db/session.py` (`app/db.py`, основная БД `clinic_tests` + опциональная HR-БД при `HR_AUTH=1`), фабрика `create_app` с регистрацией blueprint'ов, обработчиками 404/500 и Flask sessions, главный blueprint `main` с `/` и `/health` (smoke-проверка БД), `base.html` в стиле кабинета HR (Tailwind CDN + Manrope + Material Symbols, без зависимостей от HR-репо), шаблоны `index/404/500`, минимальный `static/css/app.css`. Бизнес-логика **не** добавлялась. |
|
||||
@@ -1,175 +1,102 @@
|
||||
# Перенос TestingWebApp на стек HR_TG_Bot / tgFlaskForm
|
||||
# Этап 2 (на будущее) — слияние TestingWebApp с `HR_TG_Bot/tgFlaskForm`
|
||||
|
||||
**Тот же план простым языком (две базы, люди, этапы):** [migration-to-tgflaskform-plain.md](migration-to-tgflaskform-plain.md).
|
||||
> **Не текущая фаза.** Текущая работа — **Этап 1**: унификация стека внутри TestingWebApp (Express → Flask + React → Jinja). См. [migration-final.md](migration-final.md). Этот документ — план **последующего** слияния, когда заказчик решит объединять.
|
||||
|
||||
**Назначение документа:** зафиксировать целевую архитектуру, **спринтовый план** доведения функциональности до паритета и **порядок миграции данных** из отдельного приложения (`Express` + `React` + БД `clinic_tests`) в кабинет **`tgFlaskForm`** (Flask, шаблоны, общая БД `hr_bot_test`, таблицы `testing_*`).
|
||||
**Простое объяснение тех же шагов:** [migration-to-tgflaskform-plain.md](migration-to-tgflaskform-plain.md).
|
||||
|
||||
**Связанные материалы:** [PROJECT_STATUS.md](PROJECT_STATUS.md), [СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md](СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.md) (актуальный UI React-кабинета), [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).
|
||||
**Связано:** [migration-final.md](migration-final.md) (главный трекер двух этапов), [migration-final-inventory.md](migration-final-inventory.md) (карта Express + gap-analysis с уже существующим модулем `tgFlaskForm`), [PROJECT_STATUS.md](PROJECT_STATUS.md), [README](../README.md), код HR-кабинета: `HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/`, модели: `HR_TG_Bot/tgFlaskForm/db/models.py`.
|
||||
|
||||
---
|
||||
|
||||
## 0. Стратегия переходного периода (отдельное приложение, тот же стек)
|
||||
## 0. Предусловие — Этап 1 закрыт
|
||||
|
||||
**Решение:** переписывание с Node/React на **тот же стек, что у мини-приложения и кабинета HR** — Python 3, **Flask**, шаблоны (Jinja2), статический JS, работа с PostgreSQL в духе `tgFlaskForm`. При этом сервис **пока живёт отдельно**: свой процесс, свой URL/порт, **не** обязан совпадать с деплоем полного `HR_TG_Bot/tgFlaskForm`.
|
||||
К моменту, когда этот документ берётся в работу, в TestingWebApp **уже** должно быть:
|
||||
|
||||
**Зачем так:** быстрее выйти на паритет по UX и данным, **без** риска «большого взрыва» в едином кабинете; позже либо встраиваете модуль в кабинет (общий `webApp`), либо оставляете отдельный вход — стек уже совпадает.
|
||||
- Бэкенд переписан с Express на Flask внутри [`flask_app/`](../flask_app/), все 22 эндпоинта работают.
|
||||
- Фронтенд переписан с React на Jinja-шаблоны в `flask_app/app/templates/`.
|
||||
- БД — по-прежнему `clinic_tests`, схема не менялась.
|
||||
- В репозитории остался один сервис приложения.
|
||||
|
||||
**Обязательно зафиксировать продуктово:**
|
||||
|
||||
| Вопрос | Рекомендация |
|
||||
|--------|----------------|
|
||||
| Где **пишут** тесты и попытки, пока два контура? | Один «канонический» контур на запись; второй 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`).
|
||||
Если что-то из этого ещё не готово — Этап 2 не начинается.
|
||||
|
||||
---
|
||||
|
||||
## 1. Зачем переносить
|
||||
## 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).
|
||||
| Аспект | После Этапа 1 (отдельный сервис) | После Этапа 2 (часть HR-кабинета) |
|
||||
|---|---|---|
|
||||
| Репозиторий | `TestingWebApp/flask_app/` | `HR_TG_Bot/tgFlaskForm/webApp/interfaces/testing/` |
|
||||
| Деплой | Свой Docker-сервис, свой URL/порт | Часть основного `tgFlaskForm`, общий URL `/cabinet/testing/...` |
|
||||
| БД | `clinic_tests`, UUID | `hr_bot_test`, integer ID, схема `testing_*` |
|
||||
| Авторизация | JWT/bcrypt + опциональный `HR_AUTH` | Сессии общего HR-кабинета, привязка к `staff_members` |
|
||||
| Модели | Свои (как в Express, но на Python) | Существующие `TestingTest`, `TestingTestVersion`, `TestingQuestion`, `TestingAnswer`, `TestingAssignment`, `TestingAttempt`, `TestingAttemptAnswer`, `TestingSetting`, `TestingHeadPosition` в `db/models.py` |
|
||||
| UI | Jinja-шаблоны в `flask_app/app/templates/` | Jinja-шаблоны в `tgFlaskForm/webApp/templates/cabinet/testing/` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходный и целевой стек (кратко)
|
||||
## 2. План Этапа 2 (по спринтам)
|
||||
|
||||
**Исходный (TestingWebApp):**
|
||||
### E2.0 — Сверка кода и моделей
|
||||
|
||||
- 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 вариантов).
|
||||
- Сравнить структуру `flask_app/` (после Этапа 1) с уже существующим модулем `tgFlaskForm/webApp/interfaces/testing/`. Где функции называются иначе — выбрать одно имя.
|
||||
- Сверить модели: `clinic_tests` UUID-схема vs `hr_bot_test` `testing_*` integer-схема. Зафиксировать поле-в-поле соответствия (большая часть уже сделана в [migration-final-inventory.md §10](migration-final-inventory.md#10-gap-analysis-tgflaskformcabinettesting-vs-express)).
|
||||
- **Критерий выхода:** документ соответствий + решение по спорным точкам (например, кто прав — `is_active` на цепочке или на версии).
|
||||
|
||||
**Целевой (`HR_TG_Bot/tgFlaskForm`) и отдельный контур в этом репозитории (`flask_app/`):**
|
||||
### E2.1 — Перенос кода как blueprint
|
||||
|
||||
- Приложение: `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).
|
||||
- Скопировать роуты, сервисы, шаблоны из `flask_app/` в `tgFlaskForm`. **Адаптировать**:
|
||||
- Импорты — на `db/models.py` и `db/queries/testing_queries.py` HR-кабинета.
|
||||
- Авторизация — сменить с JWT/bcrypt на сессии и `werkzeug.security.check_password_hash`.
|
||||
- URL-prefix с корневого на `/cabinet/testing/`.
|
||||
- Шаблоны — наследование от `cabinet/base.html` (хедер, нижний нав-бар).
|
||||
- **Критерий выхода:** все экраны открываются через HR-кабинет, локальный smoke-тест зелёный.
|
||||
|
||||
### E2.2 — Миграция данных (ETL)
|
||||
|
||||
Скрипт уже готов: [`HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py`](../../HR_TG_Bot/tgFlaskForm/tools/migrate_clinic_tests_to_hr.py) (437 строк, режимы `--dry-run`/`--apply`, идемпотентность через `public._clinic_tests_migration_map`).
|
||||
|
||||
Перед прогоном **на актуальных данных** дописать:
|
||||
|
||||
- **Перенос `test_assignments` 1:1** — сейчас скрипт переносит только пары «тест-сотрудник» через попытки; нужны и «висящие» назначения без попыток. (Решение Этапа 2.)
|
||||
- **Логирование пользователей без `staff_id`:** автор → WARN, попытка → WARN; никаких хардовых ошибок. (Решение Этапа 2.)
|
||||
|
||||
**Порядок:**
|
||||
|
||||
1. Бэкап `clinic_tests` и `hr_bot_test`.
|
||||
2. `--dry-run` на копии прод-БД, разбор лога.
|
||||
3. `--apply` на той же копии, ручная сверка через UI HR-кабинета.
|
||||
4. После приёмки — `--dry-run` + `--apply` на боевой БД.
|
||||
|
||||
### E2.3 — Cutover
|
||||
|
||||
Если к этому моменту у TestingWebApp всё ещё «песочница для тестировщиков» (как сейчас) — простое переключение, без окна простоя и баннеров. Если появятся реальные пользователи — добавить пункт E2.3.1: коммуникация и redirect.
|
||||
|
||||
- Заморозка записи в `flask_app/` старой инсталляции (read-only).
|
||||
- Прогон ETL на боевом.
|
||||
- Маршрутизация: внешние ссылки `clinic-tests.example.com/*` → `hr-cabinet.example.com/cabinet/testing/*`.
|
||||
- В корневом репозитории TestingWebApp — ветка `legacy/clinic-tests-flask`, в README — ссылка на этот документ и дату EOL.
|
||||
|
||||
**Критерий выхода:** мониторинг ошибок (например Sentry, уже подключён в `webApp/__init__.py`), отсутствие P1 в первую неделю.
|
||||
|
||||
---
|
||||
|
||||
## 3. Спринтовый план (переписывание = паритет + миграция + снятие стенда)
|
||||
## 3. Что трогаем в HR-кабинете до Этапа 2
|
||||
|
||||
Длительность спринта ориентировочно **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.
|
||||
**Ничего.** Существующий модуль `tgFlaskForm/webApp/interfaces/testing/` развивается своим темпом командой HR-кабинета и **не** должен подстраиваться под TestingWebApp до момента слияния. Если в нём появляются полезные правки — переносим в `flask_app/` обратным потоком.
|
||||
|
||||
---
|
||||
|
||||
## 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. Риски и как их снимать
|
||||
## 4. Риски Этапа 2 и как их снимать
|
||||
|
||||
| Риск | Мера |
|
||||
|------|------|
|
||||
| Неполное сопоставление `users` ↔ `staff_members` | Закрыть в спринте 1; не начинать ETL без процента покрытия, согласованного с заказчиком |
|
||||
| Разная семантика назначений (отдел, версия) | Явные правила в спринте 0 + лог развёртки отделов |
|
||||
| Потеря истории попыток из-за смены модели assignment | Моделирование на копии БД в спринте 1–2 |
|
||||
| Дублирование разработки UI | Опираться на уже существующий модуль в `tgFlaskForm`, не переписывать с нуля параллельный SPA |
|
||||
| Несовпадение `users.staff_id` ↔ `staff_members.id` | Проверка перед `--apply`; пользователей без `staff_id` пропускаем по решению. |
|
||||
| Расхождение моделей (UUID vs integer, поля «на цепочке» vs «на версии») | Закрыть в E2.0; подкрепить unit-тестами на конвертацию. |
|
||||
| Назначения «отдел → N сотрудников» | Логировать развёртку с пометкой `created_from_department=...`. |
|
||||
| Двойное развитие модуля HR-кабинета | До Этапа 2 — не править `tgFlaskForm/cabinet/testing` под нужды TestingWebApp. |
|
||||
|
||||
---
|
||||
|
||||
## 6. Итог
|
||||
## 5. Производительность кабинета (общее)
|
||||
|
||||
Переписывание в данном контексте — это не «ещё один greenfield на Flask», а **консолидация** уже начатого модуля в `tgFlaskForm` с **одноразовой миграцией** из `clinic_tests` и выводом из эксплуатации связки React + Express. Спринты 0–4 дают сквозной маршрут от анализа до cutover; детали ETL должны быть закреплены в коде скрипта и журнале прогона к концу **спринта 1**.
|
||||
|
||||
**См. также:** если пользователи жалуются на медленную загрузку страниц кабинета/Flask — пошаговый план измерений и правок: [performance-flask-mini-app.md](performance-flask-mini-app.md).
|
||||
Если после переноса страницы кабинета или мини-приложения работают медленно — пошаговый план измерений и правок: [performance-flask-mini-app.md](performance-flask-mini-app.md). Это вспомогательный документ, не часть этапов миграции.
|
||||
|
||||
Reference in New Issue
Block a user