fix install diogram

This commit is contained in:
2026-04-02 12:00:11 +05:00
parent 22bbc3a0a6
commit 8a189e310b
23 changed files with 1212 additions and 189 deletions

293
route/klimov_crud.py Normal file
View File

@@ -0,0 +1,293 @@
"""
Эндпоинты для работы с тестом ДДО Е.А. Климова.
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 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")
# ---------------------------------------------------------------------------
# 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": (
"Творческие профессии. Требуются воображение, образное мышление, талант, трудолюбие. "
"Примеры: художник, музыкант, актёр, архитектор, фотограф, дизайнер, писатель."
),
},
]
def get_session():
from sqlalchemy.orm import sessionmaker
return sessionmaker(bind=make_engine(), future=True)
@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/."
),
}