4 changed files with 238 additions and 0 deletions
@ -0,0 +1,14 @@ |
|||||||
|
-- Version chain: parent link + at most one active version per test chain |
||||||
|
-- Aligns with docs/revision_task/card1.md (V.1) |
||||||
|
|
||||||
|
ALTER TABLE test_versions |
||||||
|
ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES test_versions(id) ON DELETE RESTRICT; |
||||||
|
|
||||||
|
COMMENT ON COLUMN test_versions.parent_id IS 'Previous version in chain; NULL for first version'; |
||||||
|
|
||||||
|
-- Only one active version per tests.id (chain) |
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_test_versions_one_active_per_test |
||||||
|
ON test_versions (test_id) |
||||||
|
WHERE is_active = true; |
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_test_versions_parent_id ON test_versions (parent_id); |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
# Декомпозиция доработки (по ТЗ [revision_task/task.md](task.md)) |
||||||
|
|
||||||
|
**Стек (репозиторий [TestingWebApp](../..)):** **Node.js** (backend), **PostgreSQL**, **Docker**; фронт — desktop-first SPA. Экосистема клиники: см. [HR_TG_Bot README](../../../HR_TG_Bot/README.md); перенос на Python/FastAPI **не** считается обязательным для этого репо — **контракт** данных и [card1.md](revision_task/card1.md) важнее. |
||||||
|
|
||||||
|
**Карта больших кусков работ:** [card1.md](card1.md) (версии **V**, документ **D**, авторизация **HR A**). |
||||||
|
|
||||||
|
**Этап 1 (ТЗ §4)** — пять фич: 4.1–4.5 (части можно параллелить). |
||||||
|
**Этап 2 (ТЗ §5)** — дашборды. |
||||||
|
**Этапы 3–5** — интеграция в общий HR, MAX, уведомления. |
||||||
|
|
||||||
|
**Первые спринты:** [sprint-01.md](sprint-01.md), [sprint-02.md](sprint-02.md) (и при наличии `sprint-02-testing`). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## A. Подготовка и база (фундамент) |
||||||
|
|
||||||
|
| ID | Подзадача | |
||||||
|
| --- | --- | |
||||||
|
| A.1 | Репозиторий, Docker Compose (PostgreSQL), .env, health | |
||||||
|
| A.2 | Схема БД, миграции: пользователи, подразделения, тесты, версии, попытки (см. [card1 V.1](card1.md)) | |
||||||
|
| A.3 | Аутентификация: **локальная** **или** **через** [Postgres_TG_Bots](card1.md#часть-a--авторизация-по-паролю-бд-postgres_tg_bots) ([card1 A.x](card1.md)) | |
||||||
|
| A.4 | CRUD тест/назначение/прохождение (база шагов `docs/шаги/`) + затем **B** | |
||||||
|
|
||||||
|
*Если A.1–A.4 частично сданы — добить по [sprint-01](sprint-01.md) и [card1](card1.md).* |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## B. Фича 4.1 — Версионирование тестов |
||||||
|
|
||||||
|
См. полностью [card1.md — Часть V](card1.md#часть-v--версионирование-цель-корректная-история-при-правках-автора) (V.1–V.10). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## C. Фича 4.2 — AI-помощник (DeepSeek) |
||||||
|
|
||||||
|
| ID | Подзадача | |
||||||
|
| --- | --- | |
||||||
|
| C.1 | Ключ в БД; `/settings`; «Проверить»; ключ не на фронт | |
||||||
|
| C.2 | OpenAI-совместимый клиент, `json_object` | |
||||||
|
| C.3 | Сгенерировать/проверить/улучшить тест; модалки, было→стало | |
||||||
|
| C.4 | Вопрос: улучшить, дистракторы, подсказка (с 4.4) | |
||||||
|
|
||||||
|
*Импорт из документа ([card1 D](card1.md)) тянет C.1–C.3.* |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## D. Импорт из документа |
||||||
|
|
||||||
|
См. [card1 D.1–D.5](card1.md#часть-d--загрузка-документа--черновик-теста). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## E–F. Подсказки и режимы (§4.4–4.5) |
||||||
|
|
||||||
|
| ID | Кратко | |
||||||
|
| --- | --- | |
||||||
|
| E.x | Подсказка в вопросе, показ по режиму | |
||||||
|
| F.x | Таймер, мгновенная оценка, итог в конце | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## G. Этап 2 — Дашборды (§5) |
||||||
|
|
||||||
|
| ID | Подзадача | |
||||||
|
| --- | --- | |
||||||
|
| G.1 | Дашборд сотрудника | |
||||||
|
| G.2 | Руководитель подразделения | |
||||||
|
| G.3 | Директор / вся клиника | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## H. Спринт 1 — сопоставление |
||||||
|
|
||||||
|
| Спринт | Охват | |
||||||
|
| --- | --- | |
||||||
|
| **Спринт 1** | A (дозакрытие) + **B** = [card1 V](card1.md) + при согласовании [A](card1.md) (Postgres_TG_Bots) | |
||||||
|
| **Спринт 2** | **C** + начало D при наличии LLM; иначе D без генерации = только текст/ручной ввод | |
||||||
|
| Далее | E, F, G, интеграция MAX, уведомления | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Сопоставление с файлами |
||||||
|
|
||||||
|
| Документ | Содержание | |
||||||
|
| --- | --- | |
||||||
|
| [card1.md](card1.md) | Задачи Card 1: версии, документ, auth HR | |
||||||
|
| [sprint-01.md](sprint-01.md) | Спринт 1, кратко | |
||||||
|
| [task.md](task.md) | ТЗ 1.0 | |
||||||
@ -0,0 +1,81 @@ |
|||||||
|
# Карта задач: Card 1 — история прохождений, документ, авторизация HR |
||||||
|
|
||||||
|
**Связь со спринтами:** основная масса пунктов **V.x** — **Спринт 1** (версионирование + инфра); **D.x** — загрузка документа (может пересекаться со **Спринтом 2** / AI, если без LLM черновик не делается); **A.x** — авторизация **базой** `Postgres_TG_Bots` (выполнять после/параллельно с V.1, чтобы не плодить дубли пользователей на долгий срок). |
||||||
|
|
||||||
|
**Фактический стек репозитория [TestingWebApp](../../README.md):** Node.js (backend), PostgreSQL, Docker; фронт — SPA в `frontend/`. План доработок **не** привязывать к FastAPI, если в коде API на Express/Node. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Часть V — Версионирование (цель: корректная история при правках автора) |
||||||
|
|
||||||
|
**Правила (приёмка):** |
||||||
|
|
||||||
|
1. Пока по **цепочке теста** (`tests.id`) **не было ни одной** попытки в `test_attempts` (через любую `test_version_id` этой цепочки) — автор **редактирует на месте** текущую единственную строку `test_versions` и дочерние вопросы/ответы; поле `version` **не** увеличивается. |
||||||
|
2. Как только появилась **хотя бы одна** попытка — **каждое** сохранение с изменениями контента создаёт **новую** строку `test_versions`: `version = max+1`, `parent_id` → id предыдущей версии, прежняя версия `is_active = false`, новая `is_active = true`; старые вопросы/ответы **копируются** в новую версию. |
||||||
|
3. Все версии одной цепочки — общий `test_id`; цепочка линейна по `parent_id` (и/или по монотонному `version` при одном `test_id`). |
||||||
|
4. `test_attempts.test_version_id` **NOT NULL** — попытка всегда на снимок версии, разбор старых результатов не «плывёт» при новых правках. |
||||||
|
5. Списки тестов для **сотрудников** и **авторов**: только **активная** версия цепочки (`test_versions.is_active` и `tests` не скрыт деактивацией цепочки). |
||||||
|
6. **История версий** в UI: просмотр, **ручной** выбор активной версии; при выборе в транзакции: у всех версий данного `test_id` `is_active = false`, у выбранной `is_active = true`. Новые старты попыток — по **текущей** активной версии. |
||||||
|
7. **Деактивация теста целиком** — флаг на уровне `tests` (скрыть цепочку из списков, **без** удаления строк). |
||||||
|
|
||||||
|
**Задачи (детализация):** |
||||||
|
|
||||||
|
| ID | Задача | Критерий | |
||||||
|
| --- | --- | --- | |
||||||
|
| V.1 | Миграция БД: `test_versions.parent_id` (FK на `test_versions.id`, ON DELETE NO ACTION/RESTRICT), частичный уникальный индекс: не более **одной** `is_active = true` на `test_id` (если СУБД поддерживает; иначе — уникальный индекс + проверка в сервисе) | `migrate` отрабатывает пусто на повтор | |
||||||
|
| V.2 | Сервис `hasAnyAttemptForTest(testId)` + unit-тесты | Покрыты кейсы: 0 попыток / 1+ по любой версии цепочки | |
||||||
|
| V.3 | `saveTestDraft(author, testId, payload)`: ветвление in-place **vs** `forkNewVersion` (копия вопросов/опций) | Соответствие правилам 1–2 | |
||||||
|
| V.4 | Старт попытки: в `test_attempts` писать `test_version_id` **той** версии, что была **активна** в момент старта | Нет перезаписи `version_id` позже | |
||||||
|
| V.5 | API: `GET /tests` (роль) — только активные цепочки; `GET /tests/:id/versions`; `POST /tests/:id/versions/:vid/activate` | 403/404 по политике | |
||||||
|
| V.6 | API: `PATCH /tests/:id` (деактивация цепочки `tests.is_active` или отдельное поле) | Список пустеет, данные на месте | |
||||||
|
| V.7 | UI автора: номер/метка версии, предупреждение при «после первой попытки», экран **история версий**, кнопка **сменить активную** (с confirm) | Смоук `sprint-01-testing.md` | |
||||||
|
| V.8 | UI списки сотрудника/автора: **один** ряд на цепочку, без дублей версий | — | |
||||||
|
| V.9 | Интеграционные тесты API + регресс «разбор старой попытки» по старым `question_id` | — | |
||||||
|
| V.10 | *Продукт (зафиксировать в коде/доке):* при **новой** версии, что делаем с `test_assignments` — остаются на старом `test_version_id` / подтягиваем на новый / оба сценария | Решение в `task.md` или ADR one-liner | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Часть D — Загрузка документа → черновик теста |
||||||
|
|
||||||
|
**Цель:** загрузить файл (PDF, DOCX — перечислить лимиты), извлечь текст, **сгенерировать** структуру вопросов (логично тянуть **LLM** из ТЗ §4.2) или дать мастер «вставьте текст». |
||||||
|
|
||||||
|
| ID | Задача | Критерий | |
||||||
|
| --- | --- | --- | |
||||||
|
| D.1 | Эндпоинт `POST /tests/import/document` (multipart), валидация размера/типа, сохранение во временное хранилище | 413/400 при нарушении | |
||||||
|
| D.2 | Извлечение текста: PDF (библиотека на выбор), DOCX (zip/xml) | Юнит на фикстуре | |
||||||
|
| D.3 | Вызов слоя **генерации** (если **есть** ключ DeepSeek — → промпт; иначе — заглушка «только вручную») с ответом JSON по схеме вопросов/ответов | Согласовано с §4.2 | |
||||||
|
| D.4 | UI: кнопка «Из документа», превью, применение → дальше **тот же** поток сохранения, что и ручной редактор (в т.ч. V.1–V.3) | — | |
||||||
|
| D.5 | Удаление временных файлов после обработки / TTL | — | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Часть A — Авторизация по паролю, БД [Postgres_TG_Bots](../../../Postgres_TG_Bots) |
||||||
|
|
||||||
|
**Контекст:** в `Postgres_TG_Bots`/`hr_web_viewer` учёт `users` с полями `username`, `password_hash` (Werkzeug `pbkdf2:sha256` / `scrypt` через `werkzeug.security` — см. `hr_web_viewer/models/user_models.py`). |
||||||
|
|
||||||
|
| ID | Задача | Критерий | |
||||||
|
| --- | --- | --- | |
||||||
|
| A.1 | Режим `DATABASE_URL` (или `HR_DATABASE_URL`) на **ту же** БД, что и `Postgres_TG_Bots`, read-only **или** отдельный пользователь с `SELECT` на `users` (+ при необходимости `staff_members` для `department_id`) | Пример `.env` в `TestingWebApp` | |
||||||
|
| A.2 | Реализация `verifyPassword` в Node, **совместимой** с `check_password_hash` (использовать пакет, понимающий префиксы `pbkdf2:sha256:` и `scrypt:`) | Тест: тот же хеш, что сгенерировал Werkzeug | |
||||||
|
| A.3 | Логин: по `username` → взять `id`, `password_hash`; при успехе — JWT/сессия **TestingWebApp** с `user_id` (и маппинг на локальный UUID, если в приложении внутри свои `users` — **документировать** стратегию: single source = HR DB **или** sync) | Нет дублирования паролей в две таблицы в долгую | |
||||||
|
| A.4 | Отключить/не использовать регистрацию с локальным `password_hash` в прод-режиме, если включён `HR_AUTH=1` | Флаг в `.env` | |
||||||
|
| A.5 | Маппинг ролей: `users` не содержит `hr`/`manager` — связать `staff_members.web_login` = `users.username` и взять роль из существующей HR-модели **или** MVP: одна роль + админ-лист | Зафиксировать в README Card 1 | |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Порядок работ (рекомендация) |
||||||
|
|
||||||
|
1. **V.1** → **V.2** → **V.3** (ядро версий). |
||||||
|
2. **A.1**–**A.3** параллельно или сразу после **V.1** (нужен стабильный логин для стенда). |
||||||
|
3. **V.4**–**V.9** и UI **V.7**–**V.8**. |
||||||
|
4. **D.*** после появления клиента LLM (или D.1–D.2 + ручной встав текста без LLM). |
||||||
|
5. **V.10** — решение по назначениям до **V.5** если назначения уже в проде. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Ссылки |
||||||
|
|
||||||
|
- ТЗ: [task.md](task.md) §4.1, §4.2 |
||||||
|
- Спринт 1: [sprint-01.md](sprint-01.md) |
||||||
|
- Тестирование: [sprint-01-testing.md](sprint-01-testing.md) |
||||||
|
- Анализ таблиц (если ведёте): [TEST_TABLES_ANALYSIS.md](../TEST_TABLES_ANALYSIS.md) |
||||||
@ -0,0 +1,55 @@ |
|||||||
|
# Спринт 1 — Редактор тестов: версионирование (desktop web) |
||||||
|
|
||||||
|
**Формат:** отдельное веб desktop-приложение (согласно ТЗ `task.md`, этап 1; приёмка руководителями подразделений). |
||||||
|
**Граница спринта:** **до** готовой цепочки **версий** теста, **привязки попыток** к версии, **UI** списков/истории/смены активной версии и **деактивации** цепочки. |
||||||
|
**Детальная нарезка и правила приёмки:** [card1.md](card1.md) (части **V.x**, **A.x** по мере готовности). |
||||||
|
|
||||||
|
**Дизайн:** не отходить от визуального языка внутренних веб-экранов клиники (типографика, отступы, таблицы, модалки). |
||||||
|
|
||||||
|
**Стек (факт по репозиторию [TestingWebApp](../../README.md)):** **Node.js** (API в `backend/`), **PostgreSQL**, **Docker**; фронтенд **desktop-first** SPA. Интеграция с [HR_TG_Bot](../../../HR_TG_Bot/README.md) / экосистемой — по готовности API-контрактов, **не** обязана совпадать с FastAPI, если в этом репо бэкенд на Node. При переносе на Python — пересмотреть только слой API, **смысл** [card1.md](card1.md) не меняется. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Цель спринта |
||||||
|
|
||||||
|
Реализовать **§4.1 ТЗ** (версионирование) end-to-end: модель данных, бизнес-правила, API, UI автора, поведение для сотрудника и разбора **старых** результатов. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Функциональные критерии готовности (из ТЗ 4.1) |
||||||
|
|
||||||
|
1. Пока по тесту **не было попыток** — автор редактирует **на месте**, номер версии **не** инкрементируется. |
||||||
|
2. После **первой** попытки каждое сохранение изменений создаёт **новую** версию (`version + 1`, связь `parent_id`), предыдущая версия **неактивна**, данные **сохраняются**. |
||||||
|
3. Попытки привязаны к **конкретной** версии; разбор старых попыток **валиден** после правок. |
||||||
|
4. В списках — **только одна** активная версия цепочки. |
||||||
|
5. **История версий** + **ручная** смена активной версии; остальные — неактивны. |
||||||
|
6. Тест можно **деактивировать** целиком (скрыть из списков, **без** удаления данных). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Технические подзадачи (этапы внутри спринта) |
||||||
|
|
||||||
|
| # | Задача | См. в card1 | |
||||||
|
| --- | --- | --- | |
||||||
|
| 1 | Миграция: `parent_id`, инвариант «одна активная версия на `test_id`», флаг цепочки (деактивация) | V.1, V.6 | |
||||||
|
| 2 | Сервис: «0 попыток — in-place; иначе — fork + копия вопросов» | V.2, V.3 | |
||||||
|
| 3 | Попытка: `test_version_id` фиксируется **на старте**; не перезаписывается | V.4 | |
||||||
|
| 4 | API: CRUD с версиями; переключение активной; деактивация; списки без дублей | V.5, V.6, V.10 | |
||||||
|
| 5 | UI: версия, история, смена активной, списки | V.7, V.8 | |
||||||
|
| 6 | Тесты: unit + integration, регресс разбора | V.9 | |
||||||
|
|
||||||
|
**Авторизация на БД [Postgres_TG_Bots](../../../Postgres_TG_Bots):** [card1.md](card1.md) **A.1**–**A.5** — **в рамке спринта 1**, если согласовано (иначе — отдельной задачей сразу после V.1). |
||||||
|
|
||||||
|
**Загрузка документа → тест:** [card1.md](card1.md) **D.1**–**D.5** — **вне** замыкания **только** на версии; может пойти **после** V.3 или параллельно, зависит от **LLM** (§4.2). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Вне спринта 1 (как в ТЗ) |
||||||
|
|
||||||
|
- Полный **AI-**модуль (§4.2) сверх **импорта** из карточки, **медиа** (§4.3), **подсказки** (§4.4), **режимы** (§4.5), **дашборды** (этап 2). |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Документ тестирования |
||||||
|
|
||||||
|
Чек-лист: [sprint-01-testing.md](sprint-01-testing.md). |
||||||
Loading…
Reference in new issue