Files
api-copp/route/klimov_crud.py
jze9 94e78a9e77 fix: единый singleton engine в bd, убраны 17 локальных make_engine()
- 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
2026-05-18 11:13:28 +00:00

291 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Эндпоинты для работы с тестом ДДО Е.А. Климова.
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/."
),
}