Files
api-copp/web/views/content_users/poll_page.py
2026-04-02 14:14:08 +05:00

354 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()