33 KiB
Инвентаризация backend и справочный gap-analysis с tgFlaskForm
Назначение. §1–§9 — полная карта того, что сейчас живёт в backend/ (Node.js / Express). Используется в Этапе 1 как чек-лист «что переписать на Flask внутри flask_app/». §10 — справочный gap-analysis между этим и уже готовым модулем cabinet/testing в HR_TG_Bot/tgFlaskForm — пригодится в Этапе 2 при слиянии.
Связано: migration-final.md (главный трекер двух этапов), migration-to-tgflaskform.md (план Этапа 2 — слияние с tgFlaskForm), PROJECT_STATUS.md, СПРИНТЫ_МОБИЛЬНЫЙ_ДИЗАЙН.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, roleuser_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, statusattempt_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),rolestaff_members—id,fio,web_loginemployees_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.jsservices/aiEditorService.test.jsservices/documentGenService.test.jsservices/documentExtractService.test.jsutils/werkzeugPassword.test.jsintegration/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 нужно с нуля поднять:
- Подключение к БД (минимум — основной пул
clinic_tests). - Декоратор аутентификации (cookie JWT) и эндпоинты
/api/auth/*. - Роуты
/api/tests/*поэтапно — начать с read-only (list, summary, versions, editor, attempts, review), потом write (draft, activate, patch, assign, attempts/start/submit), потом AI/import. - CORS (в dev совпадает с тем, что у Express).
- Запуск под 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.
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 и потребует доработки
- AI «один вопрос со строгой сеткой» (
generate_or_rephrase_questionизaiEditorService.js). - AI «целый тест по shape» с проверкой
optionsCountиhasMultipleAnswersдля каждой строки. - «Скрытые автором» цепочки как отдельный список на UI.
/tests/:id/summary— лёгкая ручка для карточки в списке (если потребуется в новом UI).- Per-test лента попыток (UI «Прохождения» в аккордеоне «История»).
- Мобильный UX из Спринта 3: аккордеоны «О тесте / Вопросы / История / Показ в каталоге», фикс-футер, икон-кнопка удаления варианта, «Выбрать всех» в назначении и т.д. В Этапе 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-юнитами.