- bd/__init__.py: добавлены get_engine() и get_session() — один SQLAlchemy engine на весь процесс - make_engine(): pool_size=5, max_overflow=5, pool_timeout=30, pool_recycle=1800 - Все 17 route-файлов: убраны локальные get_session()/make_engine(), импорт из bd - transfer_crud.py: _get_session() → get_session() из bd - init_data_base.py: get_engine() вместо make_engine() - Результат: 78 idle соединений → 1
291 lines
17 KiB
Python
291 lines
17 KiB
Python
"""
|
||
Эндпоинты для работы с тестом ДДО Е.А. Климова.
|
||
|
||
POST /polls/klimov — создать опрос в БД (42 пары → нет, 20 пар)
|
||
POST /polls/klimov/{id}/seed-scores — загрузить ключ расшифровки (5 типов)
|
||
GET /polls/klimov — список тестов Климова
|
||
|
||
После прохождения (POST /polls/{id}/responses) диаграмма строится автоматически
|
||
через response_crud → compute_radar и сохраняется в data/radar/<poll_title>/<user_id>/<result_id>.svg.
|
||
Путь к файлу хранится в таблице radar_results.image_path.
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException
|
||
from typing import Optional
|
||
import uuid
|
||
|
||
from bd import get_session
|
||
from bd.tables.poll import Poll
|
||
from bd.tables.question import Question
|
||
from bd.tables.choice import Choice
|
||
from bd.tables.scale import ScaleDimension, ChoiceScore
|
||
|
||
router = APIRouter(tags=["polls"], prefix="/polls")
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 20 пар вопросов ДДО Е.А. Климова
|
||
# Каждая пара: (вариант А, вариант Б)
|
||
# ---------------------------------------------------------------------------
|
||
PAIRS: list[tuple[str, str]] = [
|
||
("Ухаживать за животными.", # 1а
|
||
"Обслуживать машины, приборы (следить, регулировать)."), # 1б
|
||
("Помогать больным людям, лечить их.", # 2а
|
||
"Составлять таблицы, схемы, программы для вычислительных машин."), # 2б
|
||
("Следить за качеством книжных иллюстраций, плакатов, художественных открыток, грампластинок.", # 3а
|
||
"Следить за состоянием, развитием растений."), # 3б
|
||
("Обрабатывать материалы (дерево, ткань, пластмассу, металл и т.п.).", # 4а
|
||
"Доводить товары до потребителя (рекламировать, продавать)."), # 4б
|
||
("Обсуждать научно-популярные книги, статьи.", # 5а
|
||
"Обсуждать художественные книги (или пьесы, концерты)."), # 5б
|
||
("Выращивать молодняк (животных какой-либо породы).", # 6а
|
||
"Тренировать товарищей (или младших) в выполнении каких-либо действий (трудовых, учебных, спортивных)."), # 6б
|
||
("Копировать рисунки, изображения (или настраивать музыкальные инструменты).", # 7а
|
||
"Управлять каким-либо грузовым (подъёмным или транспортным) средством."), # 7б
|
||
("Сообщать, разъяснять людям нужные им сведения (в справочном бюро, на экскурсии и т.д.).", # 8а
|
||
"Художественно оформлять выставки, витрины (или участвовать в подготовке пьес, концертов)."), # 8б
|
||
("Ремонтировать вещи, изделия (одежду, технику), жилище.", # 9а
|
||
"Искать и исправлять ошибки в текстах, таблицах, рисунках."), # 9б
|
||
("Лечить животных.", # 10а
|
||
"Выполнять вычисления, расчёты."), # 10б
|
||
("Выводить новые сорта растений.", # 11а
|
||
"Конструировать, проектировать новые виды промышленных изделий (машины или одежду, дома, продукты питания и т.п.)."), # 11б
|
||
("Разбирать споры, ссоры между людьми, убеждать, разъяснять, поощрять, наказывать.", # 12а
|
||
"Разбираться в чертежах, схемах, таблицах (проверять, уточнять, приводить в порядок)."), # 12б
|
||
("Наблюдать, изучать работу кружков художественной самодеятельности.", # 13а
|
||
"Наблюдать, изучать жизнь микробов."), # 13б
|
||
("Обсуждать, налаживать медицинские приборы, аппараты.", # 14а
|
||
"Оказывать людям медицинскую помощь при ранениях, ушибах, ожогах и т.п."), # 14б
|
||
("Составлять точные описания, отчёты о наблюдаемых явлениях, событиях, измеряемых объектах.", # 15а
|
||
"Художественно описывать, изображать события (наблюдаемые или представляемые)."), # 15б
|
||
("Делать лабораторные анализы в больнице.", # 16а
|
||
"Принимать, осматривать больных, беседовать с ними, назначать лечение."), # 16б
|
||
("Красить или расписывать стены помещений, поверхность изделий.", # 17а
|
||
"Осуществлять монтаж зданий или сборку машин, приборов."), # 17б
|
||
("Организовывать культпоходы сверстников или младших в театры, музеи, экскурсии, туристические походы и т.п.", # 18а
|
||
"Играть на сцене, принимать участие в концертах."), # 18б
|
||
("Изготовлять по чертежам детали, изделия (машины, одежду), строить здания.", # 19а
|
||
"Заниматься черчением, копированием чертежей, карт."), # 19б
|
||
("Вести борьбу с болезнями растений, с вредителями леса, сада.", # 20а
|
||
"Работать на клавишных машинах."), # 20б
|
||
]
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Ключ расшифровки: номер пары → (тип варианта А, тип варианта Б)
|
||
# Типы: П — природа, Т — техника, Ч — человек, Х — худ. образ, З — знаки
|
||
# Источник: лист ответов ДДО Е.А. Климова
|
||
# ---------------------------------------------------------------------------
|
||
CHOICE_KEY: dict[int, tuple[str, str]] = {
|
||
1: ("П", "Т"),
|
||
2: ("Ч", "З"),
|
||
3: ("З", "П"), # 3а — знаковая, 3б — природа
|
||
4: ("Т", "Ч"),
|
||
5: ("З", "Х"),
|
||
6: ("П", "Ч"),
|
||
7: ("Х", "Т"),
|
||
8: ("Ч", "Х"),
|
||
9: ("Т", "З"),
|
||
10: ("П", "З"),
|
||
11: ("П", "Т"),
|
||
12: ("Ч", "З"),
|
||
13: ("Х", "П"),
|
||
14: ("Т", "Ч"),
|
||
15: ("З", "Х"),
|
||
16: ("П", "Ч"),
|
||
17: ("Х", "Т"),
|
||
18: ("Ч", "Х"),
|
||
19: ("Т", "З"),
|
||
20: ("П", "З"),
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 5 типов профессий по Климову
|
||
# ---------------------------------------------------------------------------
|
||
DIMENSIONS = [
|
||
{
|
||
"code": "П",
|
||
"name": "Человек — природа",
|
||
"position": 1,
|
||
"color": "#6B8E5E",
|
||
"description": (
|
||
"Представители этих профессий связаны с живой природой: сельское хозяйство, "
|
||
"биология, охрана окружающей среды. Требуются наблюдательность, заботливость, "
|
||
"терпение, самостоятельность. "
|
||
"Примеры: агроном, зоотехник, ветеринар, биолог, геолог, лесник, пчеловод."
|
||
),
|
||
},
|
||
{
|
||
"code": "Т",
|
||
"name": "Человек — техника",
|
||
"position": 2,
|
||
"color": "#4A7FA5",
|
||
"description": (
|
||
"Инженерно-технические профессии. Требуются точность, практическое мышление, "
|
||
"техническая фантазия. "
|
||
"Примеры: слесарь, токарь, водитель, электрик, инженер, монтажник, программист."
|
||
),
|
||
},
|
||
{
|
||
"code": "Ч",
|
||
"name": "Человек — человек",
|
||
"position": 3,
|
||
"color": "#D4A84B",
|
||
"description": (
|
||
"Профессии, основанные на взаимодействии с людьми. Требуются общительность, "
|
||
"эмпатия, терпение, умение слушать. "
|
||
"Примеры: учитель, врач, психолог, продавец, администратор."
|
||
),
|
||
},
|
||
{
|
||
"code": "З",
|
||
"name": "Человек — знаковая система",
|
||
"position": 4,
|
||
"color": "#8B7355",
|
||
"description": (
|
||
"Работа с цифрами, кодами, текстами, чертежами, формулами. Требуются "
|
||
"внимательность, аккуратность, умение абстрагироваться. "
|
||
"Примеры: программист, экономист, бухгалтер, корректор, чертёжник, переводчик."
|
||
),
|
||
},
|
||
{
|
||
"code": "Х",
|
||
"name": "Человек — художественный образ",
|
||
"position": 5,
|
||
"color": "#8E44AD",
|
||
"description": (
|
||
"Творческие профессии. Требуются воображение, образное мышление, талант, трудолюбие. "
|
||
"Примеры: художник, музыкант, актёр, архитектор, фотограф, дизайнер, писатель."
|
||
),
|
||
},
|
||
]
|
||
|
||
|
||
|
||
|
||
@router.post("/klimov", status_code=201)
|
||
def create_klimov_poll():
|
||
"""
|
||
Создаёт тест ДДО Е.А. Климова в базе данных (20 пар вопросов, 2 варианта каждый).
|
||
После создания вызовите POST /polls/klimov/{id}/seed-scores для загрузки ключа.
|
||
"""
|
||
Session = get_session()
|
||
with Session() as session:
|
||
poll = Poll(
|
||
title="ДДО Е.А. Климова — определение типа профессии",
|
||
description=(
|
||
"Дифференциально-диагностический опросник Е.А. Климова: выберите из каждой пары "
|
||
"предпочтительный вид занятий. 20 пар — выявляет ориентацию на 5 типов профессий: "
|
||
"«Человек — природа», «Человек — техника», «Человек — человек», "
|
||
"«Человек — знаковая система», «Человек — художественный образ»."
|
||
),
|
||
)
|
||
session.add(poll)
|
||
for idx, (a, b) in enumerate(PAIRS, start=1):
|
||
q = Question(text=f"Пара {idx}", position=idx, type="single", poll=poll)
|
||
session.add(q)
|
||
ch1 = Choice(question=q, text=f"А) {a}", position=1)
|
||
ch2 = Choice(question=q, text=f"Б) {b}", position=2)
|
||
session.add(ch1)
|
||
session.add(ch2)
|
||
session.commit()
|
||
session.refresh(poll)
|
||
return {"id": str(poll.id), "questions": len(poll.questions)}
|
||
|
||
|
||
@router.get("/klimov")
|
||
def list_klimov_polls():
|
||
"""Возвращает список тестов ДДО Климова."""
|
||
Session = get_session()
|
||
with Session() as session:
|
||
rows = session.query(Poll).filter(Poll.title.like("%Климова%")).all()
|
||
return [{"id": str(p.id), "title": p.title, "description": p.description} for p in rows]
|
||
|
||
|
||
@router.post("/klimov/{poll_id}/seed-scores", status_code=201)
|
||
def seed_klimov_scores(poll_id: str):
|
||
"""
|
||
Загружает расшифровку ДДО Климова в базу данных:
|
||
- создаёт 5 осей диаграммы (ScaleDimension) для указанного теста
|
||
- для каждого из 20 вопросов привязывает оба варианта ответа к своей оси (ChoiceScore)
|
||
|
||
При повторном вызове оси и расшифровки пересоздаются.
|
||
Диаграмма строится автоматически после прохождения теста (POST /polls/{id}/responses)
|
||
и сохраняется в data/radar/<poll_title>/<user_id>/<result_id>.svg.
|
||
"""
|
||
Session = get_session()
|
||
try:
|
||
pid = uuid.UUID(poll_id)
|
||
except Exception:
|
||
raise HTTPException(status_code=400, detail="Invalid poll UUID")
|
||
|
||
with Session() as session:
|
||
poll = session.get(Poll, pid)
|
||
if not poll:
|
||
raise HTTPException(status_code=404, detail="Poll not found")
|
||
|
||
questions = (
|
||
session.query(Question)
|
||
.filter(Question.poll_id == pid)
|
||
.order_by(Question.position.asc())
|
||
.all()
|
||
)
|
||
if len(questions) != 20:
|
||
raise HTTPException(
|
||
status_code=422,
|
||
detail=f"Ожидается 20 вопросов, найдено {len(questions)}. Убедитесь, что тест создан через POST /polls/klimov.",
|
||
)
|
||
|
||
# Удаляем старые оси (и ChoiceScore через cascade)
|
||
old_dims = session.query(ScaleDimension).filter(ScaleDimension.poll_id == pid).all()
|
||
for d in old_dims:
|
||
session.delete(d)
|
||
session.flush()
|
||
|
||
# Создаём 5 осей: код → объект
|
||
dim_map: dict[str, ScaleDimension] = {}
|
||
for d_data in DIMENSIONS:
|
||
dim = ScaleDimension(
|
||
poll_id=pid,
|
||
name=d_data["name"],
|
||
description=d_data["description"],
|
||
color=d_data["color"],
|
||
position=d_data["position"],
|
||
)
|
||
session.add(dim)
|
||
session.flush()
|
||
dim_map[d_data["code"]] = dim
|
||
|
||
# Для каждого вопроса привязываем варианты к осям
|
||
scores_created = 0
|
||
for q in questions:
|
||
pair_num = q.position # 1..20
|
||
if pair_num not in CHOICE_KEY:
|
||
continue
|
||
type_a, type_b = CHOICE_KEY[pair_num]
|
||
|
||
choices = (
|
||
session.query(Choice)
|
||
.filter(Choice.question_id == q.id)
|
||
.order_by(Choice.position.asc())
|
||
.all()
|
||
)
|
||
if len(choices) < 2:
|
||
continue
|
||
|
||
choice_a, choice_b = choices[0], choices[1]
|
||
session.add(ChoiceScore(choice_id=choice_a.id, dimension_id=dim_map[type_a].id, score=1.0))
|
||
session.add(ChoiceScore(choice_id=choice_b.id, dimension_id=dim_map[type_b].id, score=1.0))
|
||
scores_created += 2
|
||
|
||
session.commit()
|
||
|
||
return {
|
||
"status": "ok",
|
||
"dimensions": len(DIMENSIONS),
|
||
"choice_scores": scores_created,
|
||
"message": (
|
||
"Расшифровка ДДО Климова успешно загружена. "
|
||
"После прохождения теста диаграмма строится автоматически и "
|
||
"сохраняется в data/radar/."
|
||
),
|
||
}
|