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()