24 KiB
api-copp
Веб-приложение для создания, проведения и анализа психологических тестов. Включает API-сервер на FastAPI и пользовательский интерфейс на Flet. Поставляется в Docker и поддерживает готовый тест по методике профессиональной ориентации Голланда.
Содержание
- Архитектура
- Быстрый старт
- Переменные окружения
- Структура проекта
- База данных
- API
- Тест Голланда
- Паутинная диаграмма
- Фронтенд
- Известные ошибки и просчёты
Архитектура
┌─────────────────────────────────────────────┐
│ Браузер пользователя :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 осями.
Процедура загрузки
POST /polls/holland— создаёт тест с 42 вопросами-парамиPOST /polls/holland/{poll_id}/seed-scores— создаёт 6ScaleDimensionи 84ChoiceScore(по 2 на каждую пару: вариант A → тип X, вариант B → тип Y)
Паутинная диаграмма
Как работает
При сдаче теста (POST /polls/{id}/responses) сервер автоматически:
- Суммирует баллы по каждой оси (
ChoiceScore.score) для выбранных вариантов - Сохраняет результат в
radar_results+radar_result_items - Генерирует SVG-файл и сохраняет в
{APP_PATH}/data/radar/{result_id}.svg - Записывает относительный путь
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-атак).