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

252 lines
9.1 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] = {} # question_id -> choice_id
self._error_text = ds.CastomText(text="", size=13, color="#FF6B6B")
self._submit_btn = ds.CastomButton(
text="Отправить ответы",
on_click=self._submit,
)
self._submit_btn.visible = False
self._loading = ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)
super().__init__(
alignment=ft.MainAxisAlignment.START,
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
spacing=10,
controls=[
ft.Row(
alignment=ft.MainAxisAlignment.CENTER,
controls=[self._loading],
)
],
)
async def load(self):
poll = await api_client.get_poll(self._poll_id)
if poll:
self._render_poll(poll)
else:
self.controls = [ds.CastomText(text="Тест не найден", size=16, color="#FF6B6B")]
self.update()
def _build_choice_group(self, choices: list, qid: str) -> ft.Column:
"""Кастомные кнопки выбора с переносом текста вместо ft.Radio."""
containers: list[ft.Container] = []
def make_tile(choice_id: str, choice_text: str) -> ft.Container:
return ft.Container(
border_radius=8,
padding=ft.padding.symmetric(horizontal=12, vertical=10),
bgcolor=ds.clolors.laer3,
border=ft.border.all(2, ds.clolors.laer3),
content=ft.Row(
spacing=10,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
ft.Icon(
ft.Icons.RADIO_BUTTON_UNCHECKED,
color=ds.clolors.ferst,
size=20,
),
ft.Text(
value=choice_text,
color=ds.clolors.ferst,
size=14,
expand=True,
no_wrap=False,
),
],
),
data=choice_id,
)
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:
tile = make_tile(c["id"], c["text"])
tile.on_click = on_tile_click
tile.ink = True
containers.append(tile)
return ft.Column(controls=containers, 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 = [
ft.Container(
padding=ft.padding.only(bottom=4),
content=ft.Column(
spacing=4,
controls=[
ds.CastomText(
text=poll.get("title", ""),
size=20,
weight=ft.FontWeight.BOLD,
),
ds.CastomText(
text=poll.get("description") or "",
size=13,
),
],
),
),
*question_cards,
self._error_text,
ft.Row(
alignment=ft.MainAxisAlignment.END,
controls=[self._submit_btn],
),
]
self._submit_btn.visible = True
async def _submit(self, e):
poll = getattr(self, "_poll", None)
if not poll:
return
questions = poll.get("questions", [])
unanswered = [q for q in questions if q["id"] not in self._answers]
if unanswered:
self._error_text.value = f"Ответьте на все вопросы ({len(unanswered)} осталось)"
self._error_text.update()
return
self._error_text.value = ""
self._submit_btn.disabled = True
self._submit_btn.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._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 = [
ft.Row(
alignment=ft.MainAxisAlignment.CENTER,
controls=[ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)],
)
]
self.update()
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()