new view testing
This commit is contained in:
@@ -3,7 +3,8 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile.api
|
dockerfile: Dockerfile.api
|
||||||
# ports НЕ публикуются — API доступен только внутри Docker-сети
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
environment:
|
environment:
|
||||||
- APP_PATH=/app
|
- APP_PATH=/app
|
||||||
- DEBUG=True
|
- DEBUG=True
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -6,7 +6,7 @@ from fastapi import Depends, FastAPI, HTTPException, status
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from route import base, holland_crud, klimov_crud, init_data_base, groups_crud, organizations_crud, users_crud, question_crud, choice_crud, response_crud, poll_crud, scale_crud, radar_crud
|
from route import base, holland_crud, klimov_crud, glomshtok_crud, init_data_base, groups_crud, organizations_crud, users_crud, question_crud, choice_crud, response_crud, poll_crud, scale_crud, radar_crud
|
||||||
from route import auth
|
from route import auth
|
||||||
from bd import wait_for_db
|
from bd import wait_for_db
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -71,6 +71,7 @@ app.include_router(response_crud.router)
|
|||||||
app.include_router(poll_crud.router)
|
app.include_router(poll_crud.router)
|
||||||
app.include_router(holland_crud.router)
|
app.include_router(holland_crud.router)
|
||||||
app.include_router(klimov_crud.router)
|
app.include_router(klimov_crud.router)
|
||||||
|
app.include_router(glomshtok_crud.router)
|
||||||
app.include_router(scale_crud.router)
|
app.include_router(scale_crud.router)
|
||||||
app.include_router(radar_crud.router)
|
app.include_router(radar_crud.router)
|
||||||
|
|
||||||
|
|||||||
504
route/glomshtok_crud.py
Normal file
504
route/glomshtok_crud.py
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
"""Эндпоинты для работы с тестом «Карта интересов»
|
||||||
|
(А.Е. Гломшток, модификация А.А. Азабель, А.Г. Грецова).
|
||||||
|
|
||||||
|
POST /polls/glomshtok — создать опрос в БД (144 утверждения, 5 вариантов ответа)
|
||||||
|
POST /polls/glomshtok/{id}/seed-scores — загрузить ключ расшифровки (24 сферы интересов)
|
||||||
|
GET /polls/glomshtok — список тестов «Карта интересов»
|
||||||
|
|
||||||
|
Ключ: вопрос с position=N относится к измерению dimension = ((N - 1) % 24) + 1.
|
||||||
|
Пример: вопросы 1, 25, 49, 73, 97, 121 → Биология; вопросы 13, 37, 61, 85, 109, 133 → Военные специальности.
|
||||||
|
|
||||||
|
Варианты ответа и баллы: ++ = +2 | + = +1 | 0 = 0 | - = −1 | -- = −2
|
||||||
|
|
||||||
|
Интерпретация суммы по сфере (6 вопросов × ±2, диапазон −12..+12):
|
||||||
|
−12..−6 → область деятельности активно отрицается («Что угодно, только не это!»)
|
||||||
|
−5..−1 → область деятельности интереса не вызывает
|
||||||
|
+1..+4 → интерес выражен слабо
|
||||||
|
+5..+7 → выраженный интерес
|
||||||
|
+8..+12 → ярко выраженный интерес
|
||||||
|
|
||||||
|
После прохождения теста (POST /polls/{id}/responses) диаграмма строится автоматически
|
||||||
|
и сохраняется в data/radar/<poll_title>/<user_id>/<result_id>.svg.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
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")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 144 утверждения «Карты интересов»
|
||||||
|
# Порядок строго соответствует PDF; индекс + 1 = номер вопроса.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
STATEMENTS: list[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
|
||||||
|
"Изучать спрос покупателей на определённый товар.", # 43
|
||||||
|
"Читать специальные спортивные новости в газетах и на сайтах.", # 44
|
||||||
|
"Исполнять произведения на музыкальных инструментах с использованием различных техник игры.", # 45
|
||||||
|
"Изучать основы сценического искусства, творчество знаменитых мастеров сцены.", # 46
|
||||||
|
"Искать наиболее рациональное и эстетическое цветовое решение для интерьеров.", # 47
|
||||||
|
"Проводить наблюдения и контроль загрязнения окружающей среды.", # 48
|
||||||
|
"Изучать анатомию и физиологию животных.", # 49
|
||||||
|
"Собирать установки для проведения физических экспериментов.", # 50
|
||||||
|
"В лабораторных условиях определять степень загрязнённости почвы химическими веществами.", # 51
|
||||||
|
"Создавать ландшафтные карты с помощью компьютерных геоинформационных систем.", # 52
|
||||||
|
"Интересоваться причинами и способами лечения болезни.", # 53
|
||||||
|
"Прокладывать сетевые кабели и кабели силового питания.", # 54
|
||||||
|
"Изучать новые технологии строительства.", # 55
|
||||||
|
"Изучать языки программирования на компьютере.", # 56
|
||||||
|
"Осуществлять финансовые расчёты между предприятиями.", # 57
|
||||||
|
"Читать литературу на иностранном языке или смотреть фильмы без перевода.", # 58
|
||||||
|
"Управлять современным поездом дальнего следования.", # 59
|
||||||
|
"Оказывать экстренную помощь людям на терпящих бедствие судах.", # 60
|
||||||
|
"Быть военным инженером или командиром.", # 61
|
||||||
|
"Обсуждать исторические и текущие политические события России и других стран.", # 62
|
||||||
|
"Подбирать цветовые оттенки, красить дома, расписывать стены.", # 63
|
||||||
|
"Добывать эксклюзивную информацию, передавать её читателям и зрителям.", # 64
|
||||||
|
"Контролировать своевременную уплату налогов фирмами и физическими лицами.", # 65
|
||||||
|
"Обучать маленьких детей, играя с ними.", # 66
|
||||||
|
"Встречать и размещать пассажиров в салоне самолёта.", # 67
|
||||||
|
"Тренироваться в профессиональном спортивном клубе.", # 68
|
||||||
|
"Играть в музыкальной группе или в оркестре, следуя указаниям дирижёра.", # 69
|
||||||
|
"Вести концертные программы, объявлять зрителям новые номера программы.", # 70
|
||||||
|
"Создавать необычные модели одежды.", # 71
|
||||||
|
"Разрабатывать меры по снижению количества вредных производственных выбросов в окружающую среду.", # 72
|
||||||
|
"Разрабатывать средства борьбы с возбудителями заболеваний животных и растений.", # 73
|
||||||
|
"Изучать процессы взаимодействия элементарных частиц в ядерных реакторах.", # 74
|
||||||
|
"Брать на анализ химические пробы воды, продуктов питания, почвы и пр.", # 75
|
||||||
|
"Вести разведку месторождений полезных ископаемых: нефти, газа, драгоценных металлов и др.", # 76
|
||||||
|
"Тренироваться в навыках первой медицинской помощи.", # 77
|
||||||
|
"Проверять, испытывать, регулировать работу узлов автомобиля, самолёта, корабля и т. д.", # 78
|
||||||
|
"Готовить растворы, смеси для строительных работ.", # 79
|
||||||
|
"Разрабатывать программные алгоритмы для выполнения сложных расчётов.", # 80
|
||||||
|
"Вести финансовую документацию фирмы, предприятия.", # 81
|
||||||
|
"Изучать иностранные языки.", # 82
|
||||||
|
"Регулировать движение транспортных потоков на улицах города.", # 83
|
||||||
|
"Оказывать экстренную помощь людям «с воздуха» (например, управляя вертолётом МЧС).", # 84
|
||||||
|
"Изучать устройство оружия, военной техники.", # 85
|
||||||
|
"Анализировать по книгам исторические факты.", # 86
|
||||||
|
"Обеспечивать в домах систему отопления, исправную работу водопровода.", # 87
|
||||||
|
"Редактировать тексты книг, статей, выступлений высокопоставленных лиц.", # 88
|
||||||
|
"Выдвигать обвинения преступникам, назначать им наказания в соответствии с законом.", # 89
|
||||||
|
"Готовить школьников к олимпиадным заданиям, конкурсам.", # 90
|
||||||
|
"Помогать подбирать людям наряды и украшения для торжественных церемоний.", # 91
|
||||||
|
"Сдавать спортивные нормативы.", # 92
|
||||||
|
"Выступать в качестве солиста перед публикой на концертах, в клубах.", # 93
|
||||||
|
"Подбирать актёров для съёмки художественного фильма или рекламного клипа.", # 94
|
||||||
|
"Делать фотоснимки, монтировать фотографии с помощью компьютера.", # 95
|
||||||
|
"Заниматься мониторингом состояния воздуха на оживлённых городских магистралях.", # 96
|
||||||
|
"Отбирать и заготавливать семена для посадки, высаживать растения в зимних садах.", # 97
|
||||||
|
"Моделировать на компьютере физические законы природы.", # 98
|
||||||
|
"Создавать новые синтетические вещества из нефтепродуктов и других полезных ископаемых.", # 99
|
||||||
|
"Изучать особенности флоры и фауны различных регионов.", # 100
|
||||||
|
"Осуществлять уход за человеком во время его выздоровления после болезни.", # 101
|
||||||
|
"Заниматься в технической или электротехнической секции (например, в авиа- или судомоделировании).", # 102
|
||||||
|
"Изучать качества и условия применения строительных материалов.", # 103
|
||||||
|
"Писать компьютерные программы на основе анализа математических алгоритмов.", # 104
|
||||||
|
"Рассчитывать предполагаемый размер прибыли предприятия.", # 105
|
||||||
|
"Осуществлять перевод телепередач на русский язык.", # 106
|
||||||
|
"Консультировать людей по соблюдению правил дорожного движения.", # 107
|
||||||
|
"Изучать особенности управления яхтой в штормовую погоду.", # 108
|
||||||
|
"Предупреждать незаконное пересечение государственной границы иностранными гражданами.", # 109
|
||||||
|
"Разыскивать и собирать материалы, свидетельствующие о событиях прошлого.", # 110
|
||||||
|
"Обрабатывать и изготовлять металлические детали на фрезерном станке.", # 111
|
||||||
|
"Работать в средствах массовой информации, вести телепередачи.", # 112
|
||||||
|
"Выяснять у людей причины незаконных поступков, которые они скрывают.", # 113
|
||||||
|
"Организовывать походы для школьников.", # 114
|
||||||
|
"Разрабатывать рекламные акции для продвижения товара в магазинах города.", # 115
|
||||||
|
"Ездить в качестве участника на спортивные соревнования в другой город.", # 116
|
||||||
|
"Сочинять музыку для кинофильмов, телепередач, для известных исполнителей.", # 117
|
||||||
|
"Подготавливать и ставить трюки в кино.", # 118
|
||||||
|
"Придумывать оригинальные ювелирные изделия, украшения.", # 119
|
||||||
|
"Изучать взаимоотношения живых организмов с их средой обитания.", # 120
|
||||||
|
"Заниматься дрессировкой служебных собак или других животных.", # 121
|
||||||
|
"Читать книги типа «Занимательная физика», «Физики шутят» и т. п.", # 122
|
||||||
|
"Управлять технологическими процессами производства лекарств.", # 123
|
||||||
|
"Участвовать в географических экспедициях.", # 124
|
||||||
|
"Работать врачом на станции «Скорой помощи».", # 125
|
||||||
|
"Ремонтировать радиоприборы и различную аппаратуру.", # 126
|
||||||
|
"Продумывать планировку домов, этажей, квартир. Намечать план строительства.", # 127
|
||||||
|
"Отражать в виде чисел и формул какие-либо события, процессы или явления.", # 128
|
||||||
|
"Заключать сделки, договоры на выполнение определённых работ.", # 129
|
||||||
|
"Изучать происхождение слов и различных словосочетаний в разных языках.", # 130
|
||||||
|
"Тренироваться водить машину или мотоцикл для подготовки к гонке.", # 131
|
||||||
|
"Изучать особенности управления лёгким самолётом в ветреную погоду.", # 132
|
||||||
|
"Жить согласно уставу, носить военную форму.", # 133
|
||||||
|
"Участвовать в археологических экспедициях, работать на раскопках.", # 134
|
||||||
|
"Делать из дерева различные предметы, мебель и пр.", # 135
|
||||||
|
"Осуществлять литературную обработку статей в соответствии с жанровым своеобразием.", # 136
|
||||||
|
"Продумывать новые законы и поправки в кодексы, которые были бы удобны для применения.", # 137
|
||||||
|
"Руководить одной из секций в доме детского творчества.", # 138
|
||||||
|
"Помогать человеку улучшить внешность с помощью причёски, макияжа, подбора красивой одежды и т. п.", # 139
|
||||||
|
"Осуществлять честное и грамотное судейство на спортивных соревнованиях.", # 140
|
||||||
|
"Изучать творчество выдающихся композиторов, поэтов песен и исполнителей.", # 141
|
||||||
|
"Создавать творческие проекты, «раскручивать» исполнителей.", # 142
|
||||||
|
"Заниматься дизайном интерьеров.", # 143
|
||||||
|
"Следить за качеством воды, поступающей в городскую водопроводную сеть.", # 144
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 24 сферы интересов
|
||||||
|
# Порядок соответствует столбцам бланка ответов из PDF.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DIMENSIONS = [
|
||||||
|
{
|
||||||
|
"code": "1", "name": "Биология", "position": 1, "color": "#6B8E5E",
|
||||||
|
"description": (
|
||||||
|
"Интерес к живой природе, биологии, зоологии, ботанике, экспедициям. "
|
||||||
|
"Рекомендуемые профессии: биолог, зоолог, ботаник, агроном, ветеринар, эколог."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "2", "name": "Физика", "position": 2, "color": "#4A7FA5",
|
||||||
|
"description": (
|
||||||
|
"Интерес к физическим законам, экспериментам, моделированию. "
|
||||||
|
"Рекомендуемые профессии: физик, инженер-физик, научный сотрудник, метролог."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "3", "name": "Химия", "position": 3, "color": "#9B59B6",
|
||||||
|
"description": (
|
||||||
|
"Интерес к химическим процессам, лабораторным опытам, синтезу веществ. "
|
||||||
|
"Рекомендуемые профессии: химик, технолог, лаборант, провизор."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "4", "name": "География", "position": 4, "color": "#27AE60",
|
||||||
|
"description": (
|
||||||
|
"Интерес к изучению Земли, природных ресурсов, экспедициям, геоинформатике. "
|
||||||
|
"Рекомендуемые профессии: географ, геолог, картограф, метеоролог."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "5", "name": "Медицина", "position": 5, "color": "#E74C3C",
|
||||||
|
"description": (
|
||||||
|
"Интерес к здоровью, лечению людей и животных, медицинским технологиям. "
|
||||||
|
"Рекомендуемые профессии: врач, медсестра, фельдшер, фармацевт."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "6", "name": "Техника и электроника", "position": 6, "color": "#2980B9",
|
||||||
|
"description": (
|
||||||
|
"Интерес к приборам, компьютерам, электронике, ремонту техники. "
|
||||||
|
"Рекомендуемые профессии: инженер, электроник, системный администратор, радиотехник."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "7", "name": "Строительство", "position": 7, "color": "#795548",
|
||||||
|
"description": (
|
||||||
|
"Интерес к строительным технологиям, архитектуре, планировке зданий. "
|
||||||
|
"Рекомендуемые профессии: архитектор, прораб, строитель, проектировщик."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "8", "name": "Математика", "position": 8, "color": "#1ABC9C",
|
||||||
|
"description": (
|
||||||
|
"Интерес к числам, формулам, задачам, алгоритмам, программированию. "
|
||||||
|
"Рекомендуемые профессии: математик, программист, статистик, аналитик данных."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "9", "name": "Экономика", "position": 9, "color": "#F39C12",
|
||||||
|
"description": (
|
||||||
|
"Интерес к финансам, торговле, производственному планированию. "
|
||||||
|
"Рекомендуемые профессии: экономист, бухгалтер, финансист, менеджер."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "10", "name": "Иностранные языки", "position": 10, "color": "#3498DB",
|
||||||
|
"description": (
|
||||||
|
"Интерес к языкам, переводу, межкультурным коммуникациям. "
|
||||||
|
"Рекомендуемые профессии: переводчик, лингвист, преподаватель иностранного языка."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "11", "name": "Транспорт", "position": 11, "color": "#E67E22",
|
||||||
|
"description": (
|
||||||
|
"Интерес к управлению транспортом, логистике, ПДД. "
|
||||||
|
"Рекомендуемые профессии: водитель, машинист, диспетчер, логист."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "12", "name": "Авиация, морское дело", "position": 12, "color": "#16A085",
|
||||||
|
"description": (
|
||||||
|
"Интерес к полётам, мореплаванию, экстремальным видам деятельности. "
|
||||||
|
"Рекомендуемые профессии: лётчик, штурман, капитан, водолаз, парашютист."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "13", "name": "Военные специальности", "position": 13, "color": "#2C3E50",
|
||||||
|
"description": (
|
||||||
|
"Интерес к армии, военной технике, патриотическим видам деятельности. "
|
||||||
|
"Рекомендуемые профессии: офицер, пограничник, военный инженер, командир."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "14", "name": "История", "position": 14, "color": "#8E6B3E",
|
||||||
|
"description": (
|
||||||
|
"Интерес к истории, политике, археологии, изучению прошлого. "
|
||||||
|
"Рекомендуемые профессии: историк, археолог, политолог, архивист."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "15", "name": "Рабочие специальности", "position": 15, "color": "#7F8C8D",
|
||||||
|
"description": (
|
||||||
|
"Интерес к ручному труду, ремёслам, работе на станках. "
|
||||||
|
"Рекомендуемые профессии: слесарь, токарь, столяр, сантехник, маляр."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "16", "name": "Журналистика", "position": 16, "color": "#C0392B",
|
||||||
|
"description": (
|
||||||
|
"Интерес к написанию текстов, СМИ, репортажам, медиапроизводству. "
|
||||||
|
"Рекомендуемые профессии: журналист, редактор, корреспондент, телеведущий."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "17", "name": "Юриспруденция", "position": 17, "color": "#1F3A5F",
|
||||||
|
"description": (
|
||||||
|
"Интерес к законам, правопорядку, криминалистике, судопроизводству. "
|
||||||
|
"Рекомендуемые профессии: юрист, адвокат, судья, следователь, нотариус."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "18", "name": "Педагогика", "position": 18, "color": "#D4A84B",
|
||||||
|
"description": (
|
||||||
|
"Интерес к обучению, воспитанию, работе с детьми. "
|
||||||
|
"Рекомендуемые профессии: учитель, воспитатель, педагог-психолог, тренер."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "19", "name": "Сфера обслуживания, торговля", "position": 19, "color": "#F1948A",
|
||||||
|
"description": (
|
||||||
|
"Интерес к торговле, обслуживанию клиентов, маркетингу, сервису. "
|
||||||
|
"Рекомендуемые профессии: продавец, менеджер по продажам, администратор, стилист."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "20", "name": "Физкультура и спорт", "position": 20, "color": "#28B463",
|
||||||
|
"description": (
|
||||||
|
"Интерес к спорту, тренировкам, соревнованиям, физическому развитию. "
|
||||||
|
"Рекомендуемые профессии: спортсмен, тренер, инструктор, спортивный судья."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "21", "name": "Музыка", "position": 21, "color": "#A569BD",
|
||||||
|
"description": (
|
||||||
|
"Интерес к музыке, игре на инструментах, сочинению, звукорежиссуре. "
|
||||||
|
"Рекомендуемые профессии: музыкант, композитор, дирижёр, звукорежиссёр."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "22", "name": "Сценическое искусство", "position": 22, "color": "#EC407A",
|
||||||
|
"description": (
|
||||||
|
"Интерес к театру, кино, сцене, перевоплощению, продюсированию. "
|
||||||
|
"Рекомендуемые профессии: актёр, режиссёр, сценарист, продюсер."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "23", "name": "Изобразительное искусство", "position": 23, "color": "#FF8C42",
|
||||||
|
"description": (
|
||||||
|
"Интерес к рисованию, дизайну, фотографии, созданию визуальных образов. "
|
||||||
|
"Рекомендуемые профессии: художник, дизайнер, фотограф, ювелир."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"code": "24", "name": "Экология", "position": 24, "color": "#2ECC71",
|
||||||
|
"description": (
|
||||||
|
"Интерес к охране природы, экологическому мониторингу, контролю загрязнений. "
|
||||||
|
"Рекомендуемые профессии: эколог, природоохранный инспектор, санитарный врач."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Шкала оценок: позиция варианта ответа → числовой балл
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CHOICE_SCORES: dict[int, float] = {
|
||||||
|
1: 2.0, # ++
|
||||||
|
2: 1.0, # +
|
||||||
|
3: 0.0, # 0
|
||||||
|
4: -1.0, # -
|
||||||
|
5: -2.0, # --
|
||||||
|
}
|
||||||
|
|
||||||
|
CHOICE_LABELS: list[str] = ["++", "+", "0", "-", "--"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_session():
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
return sessionmaker(bind=make_engine(), future=True)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/glomshtok", status_code=201)
|
||||||
|
def create_glomshtok_poll():
|
||||||
|
"""
|
||||||
|
Создаёт тест «Карта интересов» в базе данных (144 утверждения, 5 вариантов ответа).
|
||||||
|
После создания вызовите POST /polls/glomshtok/{id}/seed-scores для загрузки ключа расшифровки.
|
||||||
|
"""
|
||||||
|
Session = get_session()
|
||||||
|
with Session() as session:
|
||||||
|
poll = Poll(
|
||||||
|
title="Карта интересов — А.Е. Гломшток (модификация А.А. Азабель, А.Г. Грецова)",
|
||||||
|
description=(
|
||||||
|
"Методика «Карта интересов» для профориентации. Оцените 144 утверждения по шкале: "
|
||||||
|
"«++» (очень хотелось бы), «+» (нравится), «0» (сомневаюсь), "
|
||||||
|
"«-» (не нравится), «--» (совершенно не нравится). "
|
||||||
|
"Определяет выраженность интересов в 24 сферах деятельности."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
session.add(poll)
|
||||||
|
for idx, text in enumerate(STATEMENTS, start=1):
|
||||||
|
q = Question(text=text, position=idx, type="single", poll=poll)
|
||||||
|
session.add(q)
|
||||||
|
for pos, label in enumerate(CHOICE_LABELS, start=1):
|
||||||
|
ch = Choice(question=q, text=label, position=pos)
|
||||||
|
session.add(ch)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(poll)
|
||||||
|
return {"id": str(poll.id), "questions": len(poll.questions)}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/glomshtok")
|
||||||
|
def list_glomshtok_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("/glomshtok/{poll_id}/seed-scores", status_code=201)
|
||||||
|
def seed_glomshtok_scores(poll_id: str):
|
||||||
|
"""
|
||||||
|
Загружает ключ расшифровки «Карты интересов» в базу данных:
|
||||||
|
- создаёт 24 оси диаграммы (ScaleDimension) для указанного теста
|
||||||
|
- для каждого из 144 вопросов привязывает 5 вариантов ответа к соответствующей оси (ChoiceScore)
|
||||||
|
с баллами: ++ = +2 | + = +1 | 0 = 0 | - = −1 | -- = −2
|
||||||
|
|
||||||
|
Формула: вопрос с position=N → измерение ((N − 1) % 24) + 1
|
||||||
|
При повторном вызове оси и расшифровки пересоздаются.
|
||||||
|
"""
|
||||||
|
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) != 144:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=422,
|
||||||
|
detail=(
|
||||||
|
f"Ожидается 144 вопроса, найдено {len(questions)}. "
|
||||||
|
"Убедитесь, что тест создан через POST /polls/glomshtok."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Удаляем старые оси (ChoiceScore удаляются каскадно)
|
||||||
|
old_dims = session.query(ScaleDimension).filter(ScaleDimension.poll_id == pid).all()
|
||||||
|
for d in old_dims:
|
||||||
|
session.delete(d)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Создаём 24 оси: position → объект
|
||||||
|
dim_map: dict[int, 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["position"]] = dim
|
||||||
|
|
||||||
|
# Для каждого вопроса привязываем все 5 вариантов к соответствующей оси
|
||||||
|
scores_created = 0
|
||||||
|
for q in questions:
|
||||||
|
dim_idx = ((q.position - 1) % 24) + 1
|
||||||
|
dim = dim_map.get(dim_idx)
|
||||||
|
if dim is None:
|
||||||
|
continue
|
||||||
|
choices = (
|
||||||
|
session.query(Choice)
|
||||||
|
.filter(Choice.question_id == q.id)
|
||||||
|
.order_by(Choice.position.asc())
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for ch in choices:
|
||||||
|
score_val = CHOICE_SCORES.get(ch.position, 0.0)
|
||||||
|
session.add(ChoiceScore(choice_id=ch.id, dimension_id=dim.id, score=score_val))
|
||||||
|
scores_created += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"dimensions": len(DIMENSIONS),
|
||||||
|
"choice_scores": scores_created,
|
||||||
|
"message": (
|
||||||
|
"Расшифровка «Карты интересов» успешно загружена. "
|
||||||
|
"После прохождения теста диаграмма строится автоматически и "
|
||||||
|
"сохраняется в data/radar/."
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -8,23 +8,19 @@ class PollPage(ft.Column):
|
|||||||
def __init__(self, page: ft.Page, poll_id: str):
|
def __init__(self, page: ft.Page, poll_id: str):
|
||||||
self._page = page
|
self._page = page
|
||||||
self._poll_id = poll_id
|
self._poll_id = poll_id
|
||||||
self._answers: dict[str, str] = {} # question_id -> choice_id
|
self._answers: dict[str, str] = {}
|
||||||
self._error_text = ds.CastomText(text="", size=13, color="#FF6B6B")
|
self._questions: list[dict] = []
|
||||||
self._submit_btn = ds.CastomButton(
|
self._current: int = 0
|
||||||
text="Отправить ответы",
|
|
||||||
on_click=self._submit,
|
|
||||||
)
|
|
||||||
self._submit_btn.visible = False
|
|
||||||
self._loading = ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
expand=True,
|
||||||
alignment=ft.MainAxisAlignment.START,
|
alignment=ft.MainAxisAlignment.START,
|
||||||
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||||
spacing=10,
|
spacing=0,
|
||||||
controls=[
|
controls=[
|
||||||
ft.Row(
|
ft.Row(
|
||||||
alignment=ft.MainAxisAlignment.CENTER,
|
alignment=ft.MainAxisAlignment.CENTER,
|
||||||
controls=[self._loading],
|
controls=[ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -32,32 +28,105 @@ class PollPage(ft.Column):
|
|||||||
async def load(self):
|
async def load(self):
|
||||||
poll = await api_client.get_poll(self._poll_id)
|
poll = await api_client.get_poll(self._poll_id)
|
||||||
if poll:
|
if poll:
|
||||||
self._render_poll(poll)
|
self._poll = poll
|
||||||
|
self._questions = poll.get("questions", [])
|
||||||
|
if self._questions:
|
||||||
|
self._current = 0
|
||||||
|
self._render_question()
|
||||||
else:
|
else:
|
||||||
self.controls = [ds.CastomText(text="Тест не найден", size=16, color="#FF6B6B")]
|
self.controls = [ds.CastomText(text="В тесте нет вопросов", size=16)]
|
||||||
|
else:
|
||||||
|
self.controls = [ds.CastomText(text="Тест не найден", size=16)]
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def _build_choice_group(self, choices: list, qid: str) -> ft.Column:
|
def _render_question(self):
|
||||||
"""Кастомные кнопки выбора с переносом текста вместо ft.Radio."""
|
q = self._questions[self._current]
|
||||||
containers: list[ft.Container] = []
|
total = len(self._questions)
|
||||||
|
idx = self._current
|
||||||
|
|
||||||
def make_tile(choice_id: str, choice_text: str) -> ft.Container:
|
# --- Навигационная панель ---
|
||||||
|
prev_btn = ds.CastomButton(
|
||||||
|
text="← Предыдущий вопрос" if idx > 0 else " ← ",
|
||||||
|
on_click=lambda _: self._go(self._current - 1),
|
||||||
|
)
|
||||||
|
if idx == 0:
|
||||||
|
prev_btn.disabled = True
|
||||||
|
|
||||||
|
stop_btn = ft.Button(
|
||||||
|
content=ft.Row(
|
||||||
|
tight=True,
|
||||||
|
spacing=6,
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.STOP_CIRCLE_OUTLINED, color=ds.clolors.ferst, size=16),
|
||||||
|
ft.Text("Прервать тест", color=ds.clolors.ferst, size=13),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
style=ft.ButtonStyle(
|
||||||
|
bgcolor=ds.clolors.laer3,
|
||||||
|
color=ds.clolors.ferst,
|
||||||
|
),
|
||||||
|
on_click=self._confirm_abort,
|
||||||
|
)
|
||||||
|
|
||||||
|
counter_badge = ft.Container(
|
||||||
|
bgcolor=ds.clolors.laer2,
|
||||||
|
border_radius=10,
|
||||||
|
padding=ft.padding.symmetric(horizontal=20, vertical=10),
|
||||||
|
content=ft.Text(
|
||||||
|
f"Вопрос {idx + 1} из {total}",
|
||||||
|
color=ds.clolors.ferst,
|
||||||
|
size=14,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
text_align=ft.TextAlign.CENTER,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
nav_row = ft.Row(
|
||||||
|
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
|
||||||
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
controls=[prev_btn, counter_badge, stop_btn],
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Блок вопроса (alignment на Container, не вложенный Column) ---
|
||||||
|
question_box = ft.Container(
|
||||||
|
expand=True,
|
||||||
|
border_radius=14,
|
||||||
|
padding=24,
|
||||||
|
bgcolor=ds.clolors.laer2,
|
||||||
|
alignment=ft.Alignment(0.0, 0.0),
|
||||||
|
content=ft.Text(
|
||||||
|
value=q.get("text", ""),
|
||||||
|
color=ds.clolors.ferst,
|
||||||
|
size=16,
|
||||||
|
weight=ft.FontWeight.BOLD,
|
||||||
|
text_align=ft.TextAlign.CENTER,
|
||||||
|
no_wrap=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Варианты ответа (Container-тайлы как в оригинальном рабочем коде) ---
|
||||||
|
choices = q.get("choices", [])
|
||||||
|
selected_choice = self._answers.get(q["id"])
|
||||||
|
choice_tiles: list[ft.Container] = []
|
||||||
|
|
||||||
|
def make_tile(c: dict) -> ft.Container:
|
||||||
|
cid = c["id"]
|
||||||
|
is_selected = cid == selected_choice
|
||||||
return ft.Container(
|
return ft.Container(
|
||||||
border_radius=8,
|
border_radius=8,
|
||||||
padding=ft.padding.symmetric(horizontal=12, vertical=10),
|
padding=ft.padding.symmetric(horizontal=12, vertical=10),
|
||||||
bgcolor=ds.clolors.laer3,
|
bgcolor=ds.clolors.laer1 if is_selected else ds.clolors.laer3,
|
||||||
border=ft.border.all(2, ds.clolors.laer3),
|
|
||||||
content=ft.Row(
|
content=ft.Row(
|
||||||
spacing=10,
|
spacing=10,
|
||||||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
controls=[
|
controls=[
|
||||||
ft.Icon(
|
ft.Icon(
|
||||||
ft.Icons.RADIO_BUTTON_UNCHECKED,
|
ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED,
|
||||||
color=ds.clolors.ferst,
|
color=ds.clolors.ferst,
|
||||||
size=20,
|
size=20,
|
||||||
),
|
),
|
||||||
ft.Text(
|
ft.Text(
|
||||||
value=choice_text,
|
value=c["text"],
|
||||||
color=ds.clolors.ferst,
|
color=ds.clolors.ferst,
|
||||||
size=14,
|
size=14,
|
||||||
expand=True,
|
expand=True,
|
||||||
@@ -65,98 +134,126 @@ class PollPage(ft.Column):
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
data=choice_id,
|
data=cid,
|
||||||
|
ink=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_tile_click(e, _qid=qid, _containers=containers):
|
|
||||||
selected_id = e.control.data
|
|
||||||
self._answers[_qid] = selected_id
|
|
||||||
for tile in _containers:
|
|
||||||
is_selected = tile.data == selected_id
|
|
||||||
tile.bgcolor = ds.clolors.laer1 if is_selected else ds.clolors.laer3
|
|
||||||
tile.border = ft.border.all(2, ds.clolors.ferst if is_selected else ds.clolors.laer3)
|
|
||||||
row = tile.content
|
|
||||||
row.controls[0].name = ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED
|
|
||||||
tile.update()
|
|
||||||
|
|
||||||
for c in choices:
|
for c in choices:
|
||||||
tile = make_tile(c["id"], c["text"])
|
tile = make_tile(c)
|
||||||
tile.on_click = on_tile_click
|
tile.on_click = lambda e, _qid=q["id"]: self._select_answer(_qid, e.control.data)
|
||||||
tile.ink = True
|
choice_tiles.append(tile)
|
||||||
containers.append(tile)
|
|
||||||
|
|
||||||
return ft.Column(controls=containers, spacing=6)
|
choices_col = ft.Column(controls=choice_tiles, spacing=6)
|
||||||
|
|
||||||
def _render_poll(self, poll: dict):
|
|
||||||
self._poll = poll
|
|
||||||
question_cards = []
|
|
||||||
|
|
||||||
for q in poll.get("questions", []):
|
|
||||||
choices = q.get("choices", [])
|
|
||||||
|
|
||||||
input_widget = self._build_choice_group(choices, q["id"])
|
|
||||||
|
|
||||||
card = ft.Container(
|
|
||||||
border_radius=12,
|
|
||||||
padding=16,
|
|
||||||
margin=ft.margin.only(bottom=8),
|
|
||||||
bgcolor=ds.clolors.laer2,
|
|
||||||
content=ft.Column(
|
|
||||||
spacing=10,
|
|
||||||
controls=[
|
|
||||||
ds.CastomText(
|
|
||||||
text=q.get("text", ""),
|
|
||||||
size=15,
|
|
||||||
weight=ft.FontWeight.BOLD,
|
|
||||||
),
|
|
||||||
input_widget,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
question_cards.append(card)
|
|
||||||
|
|
||||||
self.controls = [
|
self.controls = [
|
||||||
ft.Container(
|
ft.Column(
|
||||||
padding=ft.padding.only(bottom=4),
|
expand=True,
|
||||||
content=ft.Column(
|
spacing=12,
|
||||||
spacing=4,
|
|
||||||
controls=[
|
controls=[
|
||||||
ds.CastomText(
|
nav_row,
|
||||||
text=poll.get("title", ""),
|
question_box,
|
||||||
size=20,
|
choices_col,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Обработчики
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _select_answer(self, question_id: str, choice_id: str):
|
||||||
|
self._answers[question_id] = choice_id
|
||||||
|
total = len(self._questions)
|
||||||
|
if self._current < total - 1:
|
||||||
|
self._go(self._current + 1)
|
||||||
|
else:
|
||||||
|
self._render_question()
|
||||||
|
self._append_finish_button()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _append_finish_button(self):
|
||||||
|
col = self.controls[0]
|
||||||
|
for ctrl in col.controls:
|
||||||
|
if getattr(ctrl, "data", None) == "__finish__":
|
||||||
|
return
|
||||||
|
finish_btn = ft.Button(
|
||||||
|
content=ft.Text(
|
||||||
|
"Завершить тест →",
|
||||||
|
color=ds.clolors.ferst,
|
||||||
|
size=16,
|
||||||
weight=ft.FontWeight.BOLD,
|
weight=ft.FontWeight.BOLD,
|
||||||
),
|
),
|
||||||
ds.CastomText(
|
data="__finish__",
|
||||||
text=poll.get("description") or "",
|
style=ft.ButtonStyle(
|
||||||
size=13,
|
bgcolor=ds.clolors.laer1,
|
||||||
|
color=ds.clolors.ferst,
|
||||||
),
|
),
|
||||||
],
|
on_click=self._submit,
|
||||||
),
|
)
|
||||||
),
|
col.controls.append(
|
||||||
*question_cards,
|
|
||||||
self._error_text,
|
|
||||||
ft.Row(
|
ft.Row(
|
||||||
alignment=ft.MainAxisAlignment.END,
|
alignment=ft.MainAxisAlignment.END,
|
||||||
controls=[self._submit_btn],
|
controls=[finish_btn],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _go(self, idx: int):
|
||||||
|
self._current = idx
|
||||||
|
self._render_question()
|
||||||
|
q = self._questions[self._current]
|
||||||
|
if self._current == len(self._questions) - 1 and q["id"] in self._answers:
|
||||||
|
self._append_finish_button()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def _confirm_abort(self, _):
|
||||||
|
url_key = self._page.session.store.get("url_key") or ""
|
||||||
|
|
||||||
|
def _do_abort(_):
|
||||||
|
dlg.open = False
|
||||||
|
self._page.update()
|
||||||
|
self._page.go(f"/take_test/{url_key}")
|
||||||
|
|
||||||
|
def _cancel(_):
|
||||||
|
dlg.open = False
|
||||||
|
self._page.update()
|
||||||
|
|
||||||
|
dlg = ft.AlertDialog(
|
||||||
|
modal=True,
|
||||||
|
title=ft.Text("Прервать тест?"),
|
||||||
|
content=ft.Text("Все ответы будут потеряны. Вы уверены?"),
|
||||||
|
actions=[
|
||||||
|
ft.TextButton(
|
||||||
|
content="Отмена",
|
||||||
|
on_click=_cancel,
|
||||||
),
|
),
|
||||||
]
|
ft.TextButton(
|
||||||
self._submit_btn.visible = True
|
content="Прервать",
|
||||||
|
style=ft.ButtonStyle(color=ds.clolors.laer1),
|
||||||
|
on_click=_do_abort,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
actions_alignment=ft.MainAxisAlignment.END,
|
||||||
|
)
|
||||||
|
self._page.overlay.append(dlg)
|
||||||
|
dlg.open = True
|
||||||
|
self._page.update()
|
||||||
|
|
||||||
async def _submit(self, e):
|
async def _submit(self, _):
|
||||||
poll = getattr(self, "_poll", None)
|
questions = self._questions
|
||||||
if not poll:
|
|
||||||
return
|
|
||||||
|
|
||||||
questions = poll.get("questions", [])
|
|
||||||
unanswered = [q for q in questions if q["id"] not in self._answers]
|
unanswered = [q for q in questions if q["id"] not in self._answers]
|
||||||
if unanswered:
|
if unanswered:
|
||||||
self._error_text.value = f"Ответьте на все вопросы ({len(unanswered)} осталось)"
|
first_idx = questions.index(unanswered[0])
|
||||||
self._error_text.update()
|
self._go(first_idx)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._error_text.value = ""
|
url_key = self._page.session.store.get("url_key") or ""
|
||||||
self._submit_btn.disabled = True
|
|
||||||
self._submit_btn.update()
|
self.controls = [
|
||||||
|
ft.Row(
|
||||||
|
alignment=ft.MainAxisAlignment.CENTER,
|
||||||
|
controls=[ft.ProgressRing(width=48, height=48, color=ds.clolors.laer1)],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
self.update()
|
||||||
|
|
||||||
answers = [
|
answers = [
|
||||||
{"question_id": qid, "choice_id": cid, "text": None}
|
{"question_id": qid, "choice_id": cid, "text": None}
|
||||||
@@ -170,24 +267,29 @@ class PollPage(ft.Column):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
self._error_text.value = error
|
|
||||||
self._submit_btn.disabled = False
|
|
||||||
self._submit_btn.update()
|
|
||||||
self._error_text.update()
|
|
||||||
else:
|
|
||||||
url_key = self._page.session.store.get("url_key") or ""
|
|
||||||
|
|
||||||
# Показываем лоадер пока грузим радар
|
|
||||||
self.controls = [
|
self.controls = [
|
||||||
ft.Row(
|
ft.Column(
|
||||||
alignment=ft.MainAxisAlignment.CENTER,
|
alignment=ft.MainAxisAlignment.CENTER,
|
||||||
controls=[ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)],
|
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||||
|
controls=[
|
||||||
|
ft.Icon(ft.Icons.ERROR_OUTLINE, size=48, color=ds.clolors.laer1),
|
||||||
|
ds.CastomText(text=error, size=14),
|
||||||
|
ds.CastomButton(
|
||||||
|
text="Попробовать снова",
|
||||||
|
on_click=lambda _: self._go(self._current),
|
||||||
|
),
|
||||||
|
],
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
self.update()
|
self.update()
|
||||||
|
return
|
||||||
|
|
||||||
radar = await api_client.get_radar_result(response_id)
|
radar = await api_client.get_radar_result(response_id)
|
||||||
svg_bytes = await api_client.get_radar_svg_bytes(response_id) if radar and radar.get("image_url") else None
|
svg_bytes = (
|
||||||
|
await api_client.get_radar_svg_bytes(response_id)
|
||||||
|
if radar and radar.get("image_url")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
success_controls: list[ft.Control] = [
|
success_controls: list[ft.Control] = [
|
||||||
ft.Icon(ft.Icons.CHECK_CIRCLE_OUTLINE, size=48, color=ds.clolors.laer1),
|
ft.Icon(ft.Icons.CHECK_CIRCLE_OUTLINE, size=48, color=ds.clolors.laer1),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ class PollView(ft.View):
|
|||||||
padding=ft.padding.symmetric(horizontal=12, vertical=8),
|
padding=ft.padding.symmetric(horizontal=12, vertical=8),
|
||||||
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||||
vertical_alignment=ft.MainAxisAlignment.START,
|
vertical_alignment=ft.MainAxisAlignment.START,
|
||||||
scroll=ft.ScrollMode.AUTO,
|
|
||||||
controls=[
|
controls=[
|
||||||
ft.Column(
|
ft.Column(
|
||||||
expand=True,
|
expand=True,
|
||||||
|
|||||||
Reference in New Issue
Block a user