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": "Расшифровка теста Голланда успешно загружена в базу данных.", }