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=ds.s(40), height=ds.s(40), color=ds.colors.secondary_light)], ) ], ) 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.colors.primary, size=ds.s(16)), ft.Text("Прервать тест", color=ds.colors.primary, size=ds.s(13)), ], ), style=ft.ButtonStyle( bgcolor=ds.colors.surface_alt, color=ds.colors.primary, ), on_click=self._confirm_abort, ) counter_badge = ft.Container( bgcolor=ds.colors.surface_alt, border_radius=ds.s(10), padding=ft.padding.symmetric(horizontal=ds.s(20), vertical=ds.s(10)), content=ft.Text( f"Вопрос {idx + 1} из {total}", color=ds.colors.text_secondary, size=ds.s(14), weight=ft.FontWeight.BOLD, text_align=ft.TextAlign.CENTER, ), ) nav_row = ft.Column( spacing=ds.s(6), controls=[ ft.Row( alignment=ft.MainAxisAlignment.CENTER, controls=[counter_badge], ), ft.Row( wrap=True, spacing=ds.s(8), run_spacing=ds.s(8), controls=[prev_btn, stop_btn], ), ], ) # --- Блок вопроса --- question_box = ft.Container( border_radius=ds.s(14), padding=ds.s(24), bgcolor=ds.colors.surface, content=ft.Text( value=q.get("text", ""), color=ds.colors.text_primary, size=ds.s(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=ds.s(8), padding=ft.padding.symmetric(horizontal=ds.s(12), vertical=ds.s(10)), bgcolor=ds.colors.primary_light if is_selected else ds.colors.surface_alt, content=ft.Row( spacing=ds.s(10), vertical_alignment=ft.CrossAxisAlignment.CENTER, controls=[ ft.Icon( ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED, color=ds.colors.primary, size=ds.s(20), ), ft.Text( value=c["text"], color=ds.colors.text_primary, size=ds.s(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=ds.s(6)) self.controls = [ ft.Column( scroll=ft.ScrollMode.AUTO, spacing=ds.s(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.colors.text_on_primary, size=ds.s(16), weight=ft.FontWeight.BOLD, ), data="__finish__", style=ft.ButtonStyle( bgcolor=ds.colors.primary, color=ds.colors.text_on_primary, ), 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.colors.secondary_light), 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=ds.s(48), height=ds.s(48), color=ds.colors.secondary_light)], ) ] 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=ds.s(48), color=ds.colors.secondary_light), 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=ds.s(48), color=ds.colors.secondary_light), 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=ds.s(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=ds.s(20), bgcolor=ds.colors.accent_lime, border_radius=ds.s(16), expand=True, content=ft.Column( alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=ds.s(16), controls=success_controls, ), ) ] self.update()