354 lines
12 KiB
Python
354 lines
12 KiB
Python
import flet as ft
|
||
import base64
|
||
import designer as ds
|
||
import api_client
|
||
|
||
|
||
class PollPage(ft.Column):
|
||
def __init__(self, page: ft.Page, poll_id: str):
|
||
self._page = page
|
||
self._poll_id = poll_id
|
||
self._answers: dict[str, str] = {}
|
||
self._questions: list[dict] = []
|
||
self._current: int = 0
|
||
|
||
super().__init__(
|
||
expand=True,
|
||
alignment=ft.MainAxisAlignment.START,
|
||
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
||
spacing=0,
|
||
controls=[
|
||
ft.Row(
|
||
alignment=ft.MainAxisAlignment.CENTER,
|
||
controls=[ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)],
|
||
)
|
||
],
|
||
)
|
||
|
||
async def load(self):
|
||
poll = await api_client.get_poll(self._poll_id)
|
||
if poll:
|
||
self._poll = poll
|
||
self._questions = poll.get("questions", [])
|
||
if self._questions:
|
||
self._current = 0
|
||
self._render_question()
|
||
else:
|
||
self.controls = [ds.CastomText(text="В тесте нет вопросов", size=16)]
|
||
else:
|
||
self.controls = [ds.CastomText(text="Тест не найден", size=16)]
|
||
self.update()
|
||
|
||
def _render_question(self):
|
||
q = self._questions[self._current]
|
||
total = len(self._questions)
|
||
idx = self._current
|
||
|
||
# --- Навигационная панель ---
|
||
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(
|
||
border_radius=8,
|
||
padding=ft.padding.symmetric(horizontal=12, vertical=10),
|
||
bgcolor=ds.clolors.laer1 if is_selected else ds.clolors.laer3,
|
||
content=ft.Row(
|
||
spacing=10,
|
||
vertical_alignment=ft.CrossAxisAlignment.CENTER,
|
||
controls=[
|
||
ft.Icon(
|
||
ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED,
|
||
color=ds.clolors.ferst,
|
||
size=20,
|
||
),
|
||
ft.Text(
|
||
value=c["text"],
|
||
color=ds.clolors.ferst,
|
||
size=14,
|
||
expand=True,
|
||
no_wrap=False,
|
||
),
|
||
],
|
||
),
|
||
data=cid,
|
||
ink=True,
|
||
)
|
||
|
||
for c in choices:
|
||
tile = make_tile(c)
|
||
tile.on_click = lambda e, _qid=q["id"]: self._select_answer(_qid, e.control.data)
|
||
choice_tiles.append(tile)
|
||
|
||
choices_col = ft.Column(controls=choice_tiles, spacing=6)
|
||
|
||
self.controls = [
|
||
ft.Column(
|
||
expand=True,
|
||
spacing=12,
|
||
controls=[
|
||
nav_row,
|
||
question_box,
|
||
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,
|
||
),
|
||
data="__finish__",
|
||
style=ft.ButtonStyle(
|
||
bgcolor=ds.clolors.laer1,
|
||
color=ds.clolors.ferst,
|
||
),
|
||
on_click=self._submit,
|
||
)
|
||
col.controls.append(
|
||
ft.Row(
|
||
alignment=ft.MainAxisAlignment.END,
|
||
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(
|
||
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, _):
|
||
questions = self._questions
|
||
unanswered = [q for q in questions if q["id"] not in self._answers]
|
||
if unanswered:
|
||
first_idx = questions.index(unanswered[0])
|
||
self._go(first_idx)
|
||
return
|
||
|
||
url_key = self._page.session.store.get("url_key") or ""
|
||
|
||
self.controls = [
|
||
ft.Row(
|
||
alignment=ft.MainAxisAlignment.CENTER,
|
||
controls=[ft.ProgressRing(width=48, height=48, color=ds.clolors.laer1)],
|
||
)
|
||
]
|
||
self.update()
|
||
|
||
answers = [
|
||
{"question_id": qid, "choice_id": cid, "text": None}
|
||
for qid, cid in self._answers.items()
|
||
]
|
||
user_id = self._page.session.store.get("user_id")
|
||
group_id = self._page.session.store.get("group_id") or None
|
||
org_id = self._page.session.store.get("org_id") or None
|
||
response_id, error = await api_client.submit_response(
|
||
self._poll_id, answers, user_id, group_id, org_id
|
||
)
|
||
|
||
if error:
|
||
self.controls = [
|
||
ft.Column(
|
||
alignment=ft.MainAxisAlignment.CENTER,
|
||
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()
|
||
return
|
||
|
||
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
|
||
)
|
||
|
||
success_controls: list[ft.Control] = [
|
||
ft.Icon(ft.Icons.CHECK_CIRCLE_OUTLINE, size=48, color=ds.clolors.laer1),
|
||
ds.CastomText(text="Тест пройден!", size=20, weight=ft.FontWeight.BOLD),
|
||
]
|
||
|
||
if svg_bytes and radar and radar.get("image_url"):
|
||
b64 = base64.b64encode(svg_bytes).decode()
|
||
data_uri = f"data:image/svg+xml;base64,{b64}"
|
||
download_url = api_client.build_radar_download_url(self._page.url, response_id)
|
||
|
||
success_controls.append(
|
||
ft.Container(
|
||
expand=True,
|
||
content=ft.Image(
|
||
src=data_uri,
|
||
expand=True,
|
||
height=380,
|
||
fit=ft.BoxFit.CONTAIN,
|
||
),
|
||
)
|
||
)
|
||
|
||
async def _download(_evt, _url=download_url):
|
||
await self._page.launch_url(_url)
|
||
|
||
success_controls.append(
|
||
ft.Row(
|
||
alignment=ft.MainAxisAlignment.CENTER,
|
||
controls=[
|
||
ds.CastomIconButton(
|
||
text="Сохранить диаграмму",
|
||
icon=ft.Icons.DOWNLOAD,
|
||
on_click=_download,
|
||
)
|
||
],
|
||
)
|
||
)
|
||
|
||
success_controls.append(
|
||
ds.CastomButton(
|
||
text="Вернуться к тестам",
|
||
on_click=lambda _: self._page.go(f"/take_test/{url_key}"),
|
||
)
|
||
)
|
||
|
||
self.controls = [
|
||
ft.Container(
|
||
padding=20,
|
||
bgcolor=ds.clolors.laer3,
|
||
border_radius=16,
|
||
expand=True,
|
||
content=ft.Column(
|
||
alignment=ft.MainAxisAlignment.CENTER,
|
||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||
spacing=16,
|
||
controls=success_controls,
|
||
),
|
||
)
|
||
]
|
||
self.update()
|