Files
api-copp/route/holland_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

261 lines
17 KiB
Python
Raw 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.
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")
# ---------------------------------------------------------------------------
# Вопросы теста — модификация Г.В. Резапкиной
# Пары профессий: (вариант А, вариант В)
# ---------------------------------------------------------------------------
PAIRS = [
("инженер-технолог", "инженер-конструктор"), # 1
("электрорадиотехник", "врач-терапевт"), # 2
("оператор станков с числовым программным управлением", "кодировщик (обработка информации)"), # 3
("фотограф", "коммерсант"), # 4
("спасатель МЧС", "дизайнер"), # 5
("политолог", "психиатр"), # 6
("ученый химик", "бухгалтер"), # 7
("философ", "частный предприниматель"), # 8
("лингвист", "модельер"), # 9
("инспектор службы занятости населения", "статист"), # 10
("социальный педагог", "биржевой маклер"), # 11
("тренер", "искусствовед"), # 12
("нотариус", "менеджер"), # 13
("перфораторщик", "художник"), # 14
("лидер политической партии, общего движения", "писатель"), # 15
("закройщик", "метеоролог"), # 16
("водитель", "работник пресс-службы"), # 17
("чертежник", "риэлтер"), # 18
("специалист по ремонту компьютеров и оргтехники", "секретарь-референт"), # 19
("микробиолог", "психолог"), # 20
("видеооператор", "режиссер"), # 21
("экономист", "провизор"), # 22
("зоолог", "главный инженер"), # 23
("программист", "архитектор"), # 24
("работник инспекции по делам несовершеннолетних", "коммивояжер (сетевой маркетинг)"), # 25
("преподаватель", "биржевой маклер"), # 26
("воспитатель", "декоратор"), # 27
("реставратор", "зав. отделом предприятия"), # 28
("корректор", "литератор и кинокритик"), # 29
("фермер", "визажист"), # 30
("парикмахер", "социолог"), # 31
("экспедитор", "редактор"), # 32
("ветеринар", "директор (финансовый)"), # 33
("автомеханик", "стилист"), # 34
("археолог", "эксперт"), # 35
("библиограф", "корреспондент"), # 36
("эколог", "актер"), # 37
("логопед", "контролер"), # 38
("адвокат", "директор (глава АО)"), # 39
("кассир", "продюсер"), # 40
("поэт, писатель", "продавец"), # 41
("криминалист (баллистик)", "композитор"), # 42
]
# ---------------------------------------------------------------------------
# Ключ расшифровки: номер пары → (тип выбора А, тип выбора В)
# Типы: Р=Реалистический, И=Интеллектуальный, С=Социальный,
# К=Конвенциональный, П=Предприимчивый, А=Художественный
# Источник: Тест Дж. Голланда, модификация Г.В. Резапкиной
# ---------------------------------------------------------------------------
CHOICE_KEY: dict[int, tuple[str, str]] = {
1: ("Р", "И"), 2: ("Р", "С"), 3: ("Р", "К"), 4: ("Р", "П"), 5: ("Р", "А"),
6: ("И", "С"), 7: ("И", "К"), 8: ("И", "П"), 9: ("И", "А"), 10: ("С", "К"),
11: ("С", "П"), 12: ("С", "А"), 13: ("К", "П"), 14: ("К", "А"), 15: ("П", "А"),
16: ("Р", "И"), 17: ("Р", "С"), 18: ("К", "П"), 19: ("Р", "К"), 20: ("И", "С"),
21: ("Р", "А"), 22: ("И", "К"), 23: ("И", "П"), 24: ("И", "А"), 25: ("С", "П"),
26: ("С", "П"), 27: ("С", "А"), 28: ("Р", "П"), 29: ("К", "А"), 30: ("П", "А"),
31: ("Р", "И"), 32: ("Р", "К"), 33: ("Р", "П"), 34: ("Р", "А"), 35: ("И", "К"),
36: ("И", "С"), 37: ("И", "А"), 38: ("С", "К"), 39: ("С", "П"), 40: ("К", "П"),
41: ("А", "С"), 42: ("К", "А"),
}
# ---------------------------------------------------------------------------
# Описания шести типов личности по Голланду
# ---------------------------------------------------------------------------
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": "#C0392B",
"description": (
"Выбирает цели и задачи, которые позволяют проявить энергию и энтузиазм. Самоуверен, настойчив. "
"Находчив, практичен, быстро ориентируется в сложной обстановке. Социально активен, готов рисковать. "
"Предпочитает деятельность, связанную с руководством и влиянием на людей. "
"Рекомендуемые профессии: руководитель, директор, судья, адвокат, брокер, предприниматель, риэлтор."
),
},
{
"code": "А", "name": "Художественный", "position": 6, "color": "#8E44AD",
"description": (
"Сложный взгляд на жизнь, гибкость и независимость в принятии решений. Богатое воображение, "
"склонность к творческой деятельности. Оригинален, эмоционален, не терпит жёсткой регламентации. "
"Преобладают вербальные способности. "
"Рекомендуемые профессии: писатель, фотограф, музыкант, художник, журналист, архитектор, дизайнер."
),
},
]
@router.post("/holland", status_code=201)
def create_holland_poll(author_id: Optional[str] = None):
Session = get_session()
with Session() as session:
poll = Poll(
title="Тест Дж. Голланда — определение типа личности",
description=(
"Тест Дж. Голланда (модификация Г.В. Резапкиной): выберите из каждой пары "
"предпочитаемую профессию. Из 42 пар профессий отметьте ту, которая вам больше нравится."
),
)
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("/holland")
def list_holland_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("/holland/{poll_id}/seed-scores", status_code=201)
def seed_holland_scores(poll_id: str):
"""
Загружает расшифровку теста Голланда (модификация Резапкиной) в базу данных:
- создаёт 6 осей диаграммы (ScaleDimension) для указанного теста
- для каждого из 42 вопросов привязывает оба варианта ответа к своей оси (ChoiceScore)
При повторном вызове старые оси и расшифровки пересоздаются.
"""
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) != 42:
raise HTTPException(
status_code=422,
detail=f"Ожидается 42 вопроса, найдено {len(questions)}. Убедитесь, что тест создан через POST /polls/holland.",
)
# Удаляем старые оси (и ChoiceScore через cascade)
old_dims = session.query(ScaleDimension).filter(ScaleDimension.poll_id == pid).all()
for d in old_dims:
session.delete(d)
session.flush()
# Создаём 6 осей: код → объект
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..42
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": "Расшифровка теста Голланда успешно загружена в базу данных.",
}