2026-04-01 14:18:52 +05:00
2026-04-01 14:18:52 +05:00
2026-02-27 15:37:15 +05:00
2026-04-01 14:18:52 +05:00
2026-04-01 14:18:52 +05:00
2026-03-31 13:25:14 +05:00
2026-02-03 17:15:58 +05:00
2026-03-31 13:25:14 +05:00
2026-03-31 13:25:14 +05:00
2026-03-31 13:25:14 +05:00
2026-03-01 21:40:09 +05:00
2026-03-31 13:25:14 +05:00
2026-04-01 14:18:52 +05:00
2026-03-23 21:28:10 +05:00
2026-04-01 14:18:52 +05:00
2026-03-23 21:28:10 +05:00
2026-03-23 21:28:10 +05:00
2026-03-23 21:28:10 +05:00

api-copp

Веб-приложение для создания, проведения и анализа психологических тестов. Включает API-сервер на FastAPI и пользовательский интерфейс на Flet. Поставляется в Docker и поддерживает готовый тест по методике профессиональной ориентации Голланда.


Содержание


Архитектура

┌─────────────────────────────────────────────┐
│  Браузер пользователя  :80                  │
│  (Flet Web App)                             │
└────────────────────┬────────────────────────┘
                     │ HTTP / REST
┌────────────────────▼────────────────────────┐
│  FastAPI API  :8000                         │
│  - аутентификация (JWT)                     │
│  - управление тестами                       │
│  - сбор ответов                             │
│  - расчёт радарных результатов              │
│  - генерация SVG-диаграмм                   │
└──────┬─────────────────────┬────────────────┘
       │                     │
┌──────▼──────┐     ┌────────▼────────┐
│ PostgreSQL  │     │     Redis       │
│   :5432     │     │     :6379       │
│ (основная   │     │ (сессии /       │
│  БД)        │     │  кэш)           │
└─────────────┘     └─────────────────┘

Сетевая топология Docker: все три сервиса (api, web, postgres, redis) работают в одной Docker-сети. Фронтенд (web) обращается к API по внутреннему имени http://api:8000. PostgreSQL и Redis запускаются отдельным compose-файлом docker-compose_db.yml.


Быстрый старт

1. Инфраструктура (PostgreSQL + Redis)

docker compose -f docker-compose_db.yml up -d

2. Приложение (API + Web)

docker compose up -d --build

После запуска:

Адрес Что
http://localhost Веб-интерфейс (Flet)
http://localhost:8000/docs Swagger UI (HTTP Basic Auth)
http://localhost:8000/redoc ReDoc

Доступ к документации — логин/пароль из переменных DOCS_USERNAME / DOCS_PASSWORD.

3. Инициализация БД

После первого запуска выполните:

# Создать таблицы
curl -X POST http://localhost:8000/db/create-tables \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Применить миграции (добавляет колонки, если БД уже была создана ранее)
curl -X POST http://localhost:8000/db/migrate \
  -H "X-Admin-Key: <ADMIN_KEY>"

4. Загрузка теста Голланда

# Создать тест (возвращает poll_id)
curl -X POST http://localhost:8000/polls/holland \
  -H "X-Admin-Key: <ADMIN_KEY>"

# Загрузить ключ расшифровки (42 пары → 6 типов × 14 баллов)
curl -X POST http://localhost:8000/polls/holland/<poll_id>/seed-scores \
  -H "X-Admin-Key: <ADMIN_KEY>"

Переменные окружения

Важно: перед деплоем в продакшн обязательно смените SECRET_KEY, ADMIN_KEY, DOCS_PASSWORD и пароли БД.

Переменная Описание Пример
APP_PATH Абсолютный путь к директории приложения внутри контейнера /home/user/api-copp
DB_HOST Хост PostgreSQL postgres
DB_PORT Порт PostgreSQL 5432
DB_USER Пользователь БД postgres
DB_PASS Пароль БД postgres
DB_NAME Имя базы данных profi
REDIS_HOST Хост Redis redis
REDIS_PORT Порт Redis 6379
REDIS_DB Номер БД Redis 0
REDIS_PASSWORD Пароль Redis
SECRET_KEY Секрет для подписи JWT-токенов (min 32 символа) python -c "import secrets; print(secrets.token_hex(32))"
ADMIN_KEY Ключ для защищённых admin-эндпоинтов
DOCS_USERNAME Логин для доступа к /docs и /redoc admin
DOCS_PASSWORD Пароль для доступа к /docs и /redoc
API_URL Внутренний URL API для фронтенда http://api:8000

Файлы docker-compose.yml содержат демо-значения — не используйте их в продакшн.


Структура проекта

api-copp/
├── main.py                  # точка входа FastAPI, регистрация роутеров
├── requirements.txt         # зависимости API
├── pyproject.toml           # метаданные проекта
├── Dockerfile.api           # образ API (python:3.14-slim)
├── Dockerfile.web           # образ фронтенда (python:3.11-slim + flet)
├── docker-compose.yml       # API + Web
├── docker-compose_db.yml    # PostgreSQL + Redis
├── docker-compose_web.yml   # отдельный запуск только Web
│
├── bd/                      # слой доступа к данным
│   ├── __init__.py          # Settings (pydantic), make_engine()
│   └── tables/
│       ├── simple_base.py   # общий Base для SQLAlchemy
│       ├── users.py         # User
│       ├── organization.py  # Organization
│       ├── group.py         # Group
│       ├── poll.py          # Poll
│       ├── question.py      # Question
│       ├── choice.py        # Choice
│       ├── response.py      # Response, Answer
│       ├── scale.py         # ScaleDimension, ChoiceScore
│       └── radar_result.py  # RadarResult, RadarResultItem
│
├── route/                   # FastAPI-роутеры
│   ├── auth_utils.py        # JWT, bcrypt, require_admin_key
│   ├── auth.py              # /auth/register, /auth/login
│   ├── base.py              # /health, /version
│   ├── init_data_base.py    # /db/create-tables, /db/migrate
│   ├── users_crud.py        # CRUD пользователей
│   ├── groups_crud.py       # CRUD групп
│   ├── organizations_crud.py# CRUD организаций
│   ├── poll_crud.py         # CRUD опросов
│   ├── question_crud.py     # CRUD вопросов
│   ├── choice_crud.py       # CRUD вариантов ответов
│   ├── response_crud.py     # приём ответов + авто-расчёт радара
│   ├── holland_crud.py      # генерация теста Голланда + seed-scores
│   ├── scale_crud.py        # CRUD осей и расшифровок
│   ├── radar_crud.py        # расчёт и чтение радарных результатов
│   └── radar_svg_gen.py     # серверная генерация SVG-диаграмм
│
├── web/                     # Flet-фронтенд
│   ├── main.py              # точка входа Flet ASGI
│   ├── router.py            # маршрутизация страниц
│   ├── api_client.py        # httpx-обёртки над API
│   ├── designer.py          # компоненты дизайн-системы
│   ├── radar_chart.py       # виджет RadarChart (flet.canvas)
│   ├── radar_svg.py         # генератор SVG-data-URI (на клиенте)
│   └── views/
│       └── content_users/
│           ├── poll_page.py       # прохождение теста + диаграмма сразу после
│           ├── response_page.py   # страница результата с диаграммой
│           ├── my_tests_page.py   # список пройденных тестов
│           └── ...
│
└── data/                    # смонтированный том Docker (./data:/app/data)
    ├── postgres/            # данные PostgreSQL
    └── radar/               # сохранённые SVG-файлы диаграмм (создаётся автоматически)

База данных

Схема таблиц

organizations ──< groups ──< users
                              │
polls ──< questions ──< choices ──< choice_scores
  │                                     │
  └──< responses ──< answers            │
         │                              │
         └──< radar_results ──< radar_result_items
              (image_path)         └── (dimension_id FK → scale_dimensions)
                                               ↑
polls ──< scale_dimensions ──────────────────-─┘

Ключевые таблицы

Таблица Описание
users Испытуемые: username, hashed_password, first_name, last_name, FK на groups и organizations
polls Тест/опрос: title, description
questions Вопросы теста с type (single / multiple)
choices Варианты ответов к вопросам
responses Один факт прохождения теста (user + poll + timestamp)
answers Конкретные ответы внутри прохождения
scale_dimensions Оси радарной диаграммы: name, color, position, FK на polls
choice_scores Расшифровка: какой вариант ответа сколько баллов вносит в какую ось
radar_results Итоговый результат по осям + image_path (путь к SVG-файлу)
radar_result_items Снимок значений по каждой оси на момент расчёта

Миграции

Таблицы создаются через POST /db/create-tables. Для уже существующих БД применяются дополнительные ALTER-миграции через POST /db/migrate. Текущие миграции:

  • Добавление колонок username / hashed_password в users
  • Добавление колонки image_path в radar_results

API

Аутентификация

POST /auth/register      — регистрация (username ≥ 3 симв., password ≥ 8 симв.)
POST /auth/login         — получить JWT-токен (OAuth2 password flow)

JWT-токен передаётся заголовком Authorization: Bearer <token>. Admin-операции требуют заголовок X-Admin-Key.

Основные эндпоинты

# Тесты
GET    /polls                    — список тестов
POST   /polls                    — создать тест
GET    /polls/{id}               — тест с вопросами и вариантами
DELETE /polls/{id}               — удалить тест

# Вопросы / Варианты ответов
POST   /polls/{id}/questions
POST   /questions/{id}/choices

# Прохождение
POST   /polls/{poll_id}/responses           — сдать тест (автоматически считает радар)
GET    /polls/users/{user_id}/responses     — история пользователя
GET    /responses/{id}                      — конкретный результат

# Результаты (радар)
POST   /responses/{id}/radar/compute        — (пере)считать радарный результат
GET    /responses/{id}/radar                — данные для диаграммы
GET    /responses/{id}/radar/image          — SVG-файл диаграммы
GET    /users/{user_id}/radar-results       — история радарных результатов

# Оси и расшифровки
POST   /polls/{poll_id}/dimensions          — создать ось
GET    /polls/{poll_id}/dimensions          — список осей
PUT    /dimensions/{id}
DELETE /dimensions/{id}
POST   /dimensions/{id}/scores              — задать расшифровку варианта ответа
GET    /choices/{choice_id}/scores

# Тест Голланда
POST   /polls/holland                       — создать тест с 42 парами
POST   /polls/holland/{poll_id}/seed-scores — загрузить ключ расшифровки

# База данных (admin)
POST   /db/create-tables
POST   /db/migrate

# Служебные
GET    /health
GET    /version

Тест Голланда

Реализована модификация Г.В. Резапкиной теста профессиональной ориентации Дж. Голланда. 42 пары профессий, испытуемый выбирает предпочтительную в каждой паре.

Типы личности

Код Название Цвет
Р Реалистичный #FF8C00
И Интеллектуальный #4169E1
С Социальный #32CD32
К Конвенциональный #9370DB
П Предприимчивый #DC143C
А Артистический #FF69B4

Каждый тип получает ровно 14 вопросов (= 14 максимально возможных баллов). Результат — паутинная диаграмма с 6 осями.

Процедура загрузки

  1. POST /polls/holland — создаёт тест с 42 вопросами-парами
  2. POST /polls/holland/{poll_id}/seed-scores — создаёт 6 ScaleDimension и 84 ChoiceScore (по 2 на каждую пару: вариант A → тип X, вариант B → тип Y)

Паутинная диаграмма

Как работает

При сдаче теста (POST /polls/{id}/responses) сервер автоматически:

  1. Суммирует баллы по каждой оси (ChoiceScore.score) для выбранных вариантов
  2. Сохраняет результат в radar_results + radar_result_items
  3. Генерирует SVG-файл и сохраняет в {APP_PATH}/data/radar/{result_id}.svg
  4. Записывает относительный путь data/radar/{result_id}.svg в колонку image_path

Скачивание SVG

Фронтенд скачивает SVG с API через внутреннюю сеть (GET /responses/{id}/radar/image), кодирует в base64 data URI и открывает в браузере как data:image/svg+xml;base64,... — файл сохраняется на устройство пользователя без необходимости публичного URL к серверу.

Генерация SVG

SVG генерируется чистым Python без зависимостей (route/radar_svg_gen.py). Серверный SVG идентичен клиентскому (web/radar_svg.py) по разметке, но предназначен для долгосрочного хранения. Параметры:

  • Фон #2F184B, полигон rgba(155,114,207,0.55)
  • 5 концентрических сеток
  • Подписи с именем типа и баллом
  • Размер холста: 520×520 пикселей

Фронтенд

Написан на Flet — Python-фреймворке поверх Flutter. Работает как ASGI-приложение через uvicorn, отдаётся браузеру как веб-приложение.

Маршруты

Путь Страница
/login Вход
/reg Регистрация
/take_test/{url_key} Список тестов для прохождения
/poll/{poll_id} Прохождение теста
/response/{response_id} Результат с диаграммой
/my_tests Мои результаты
/profile Профиль пользователя

Диаграмма после теста

После сдачи теста пользователь сразу видит паутинную диаграмму (виджет RadarChart) и кнопку «Сохранить диаграмму», которая загружает SVG с сервера и открывает его в новой вкладке для сохранения.


Известные ошибки и просчёты

1. Лишнее дублирование генерации SVG

Проблема: SVG-генератор написан дважды — route/radar_svg_gen.py (сервер) и web/radar_svg.py (клиент). Кнопка «Сохранить» изначально создавала SVG на клиенте заново вместо использования уже сохранённого серверного файла.

Решение: Кнопка «Сохранить» теперь скачивает SVG через api_client.get_radar_svg_bytes(), а не регенерирует его. Серверный файл — источник истины.

Оставшийся долг: web/radar_svg.py больше не используется кнопками сохранения, но файл остался. Можно удалить при следующем рефакторинге.


2. Несовместимость Python 3.14 и psycopg

Проблема: pyproject.toml указывает requires-python = ">=3.14" и зависимость psycopg>=3.3.3, однако Dockerfile.api использует python:3.14-slim. psycopg (асинхронный адаптер) на 3.14 может не иметь собранных wheels. В requirements.txt вместо psycopg используется pg8000==1.29.0 — синхронный pure-Python драйвер.

Решение: Все запросы к БД синхронные (make_engine() возвращает синхронный create_engine с postgresql+pg8000://...). Фактически psycopg не используется. pyproject.toml содержит ошибочную зависимость psycopg, которую следует убрать.


3. Нет транзакции при сохранении SVG

Проблема: После основного session.commit() (сохранение RadarResult и RadarResultItem) идёт отдельное обновление result.image_path. Если процесс упадёт между commit и вторым commit — в БД окажется запись без image_path, но файл будет создан на диске.

Последствие: При повторном прохождении теста пересчёт (compute_radar) удаляет старый RadarResult и создаёт новый, что восстанавливает корректность. Но исторический файл .svg осиротеет на диске.

Рекомендация: Объединить записи + генерацию SVG в одну транзакцию с rollback-обработчиком, либо хранить SVG в БД как blob/base64.


4. Перенос подписей в диаграмме по буквам (устранено)

Проблема: В виджете RadarChart (web/radar_chart.py) подписи осей переносились по буквам из-за слишком малой ширины контейнера (_W_LBL=90).

Решение: Увеличено до _W_LBL=130, добавлен max_lines=2, no_wrap=False — перенос только по словам.


5. Первоначально неверные пары профессий

Проблема: В первой версии holland_crud.py использовались произвольные пары профессий, не соответствующие оригинальной методике. Ключ расшифровки (CHOICE_KEY) не был реализован — загрузка в БД не работала.

Решение: Заменены на 42 официальные пары из модификации Резапкиной с корректным CHOICE_KEY — каждый из 6 типов (Р/И/С/К/П/А) получает ровно 14 баллов-вопросов.


6. Браузер не может напрямую обратиться к http://api:8000

Проблема: Внутреннее Docker-имя api недоступно из браузера пользователя. Если бы кнопка «Сохранить» формировала URL вида http://api:8000/responses/.../radar/image и передавала его в page.launch_url() — браузер получал бы ошибку DNS.

Решение: Фронтенд скачивает SVG-байты с API через httpx (внутренняя сеть), конвертирует в base64 data URI и передаёт его в page.launch_url(). Браузер открывает data URI напрямую, без обращения к серверу.


7. image_path добавлен после создания таблиц (требует миграции)

Проблема: Колонка image_path была добавлена в RadarResult после того, как таблицы уже создавались через POST /db/create-tables. create_all не изменяет существующие таблицы.

Решение: Добавлена явная миграция в route/init_data_base.py:

ALTER TABLE radar_results ADD COLUMN IF NOT EXISTS image_path VARCHAR(512);

Применяется вызовом POST /db/migrate.


8. Документация API доступна без авторизации по умолчанию у FastAPI (устранено)

Проблема: FastAPI по умолчанию открывает /docs и /openapi.json без каких-либо ограничений.

Решение: docs_url=None, openapi_url=None, redoc_url=None — стандартные маршруты отключены. Вместо них зарегистрированы собственные /docs, /redoc, /openapi.json с HTTP Basic Auth через secrets.compare_digest (защита от timing-атак).

Description
No description provided
Readme 1.1 MiB
Languages
Python 59.5%
TypeScript 33.9%
CSS 6.1%
Dockerfile 0.3%
HTML 0.2%