264 lines
17 KiB
Python
264 lines
17 KiB
Python
from fastapi import APIRouter, HTTPException
|
||
from typing import Optional
|
||
import uuid
|
||
|
||
from bd import make_engine
|
||
|
||
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": (
|
||
"Сложный взгляд на жизнь, гибкость и независимость в принятии решений. Богатое воображение, "
|
||
"склонность к творческой деятельности. Оригинален, эмоционален, не терпит жёсткой регламентации. "
|
||
"Преобладают вербальные способности. "
|
||
"Рекомендуемые профессии: писатель, фотограф, музыкант, художник, журналист, архитектор, дизайнер."
|
||
),
|
||
},
|
||
]
|
||
|
||
|
||
def get_session():
|
||
from sqlalchemy.orm import sessionmaker
|
||
return sessionmaker(bind=make_engine(), future=True)
|
||
|
||
|
||
@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": "Расшифровка теста Голланда успешно загружена в базу данных.",
|
||
}
|