new view testing

This commit is contained in:
2026-04-02 14:14:08 +05:00
parent 8a189e310b
commit fe078b8560
5 changed files with 772 additions and 165 deletions

View File

@@ -8,23 +8,19 @@ 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)
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=10,
spacing=0,
controls=[
ft.Row(
alignment=ft.MainAxisAlignment.CENTER,
controls=[self._loading],
controls=[ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)],
)
],
)
@@ -32,32 +28,105 @@ class PollPage(ft.Column):
async def load(self):
poll = await api_client.get_poll(self._poll_id)
if poll:
self._render_poll(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, color="#FF6B6B")]
self.controls = [ds.CastomText(text="Тест не найден", size=16)]
self.update()
def _build_choice_group(self, choices: list, qid: str) -> ft.Column:
"""Кастомные кнопки выбора с переносом текста вместо ft.Radio."""
containers: list[ft.Container] = []
def _render_question(self):
q = self._questions[self._current]
total = len(self._questions)
idx = self._current
def make_tile(choice_id: str, choice_text: str) -> ft.Container:
# --- Навигационная панель ---
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.laer3,
border=ft.border.all(2, ds.clolors.laer3),
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.RADIO_BUTTON_UNCHECKED,
ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED,
color=ds.clolors.ferst,
size=20,
),
ft.Text(
value=choice_text,
value=c["text"],
color=ds.clolors.ferst,
size=14,
expand=True,
@@ -65,98 +134,126 @@ class PollPage(ft.Column):
),
],
),
data=choice_id,
data=cid,
ink=True,
)
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)
tile = make_tile(c)
tile.on_click = lambda e, _qid=q["id"]: self._select_answer(_qid, e.control.data)
choice_tiles.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)
choices_col = ft.Column(controls=choice_tiles, spacing=6)
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,
),
],
),
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,
),
*question_cards,
self._error_text,
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=[self._submit_btn],
),
]
self._submit_btn.visible = True
controls=[finish_btn],
)
)
async def _submit(self, e):
poll = getattr(self, "_poll", None)
if not poll:
return
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()
questions = poll.get("questions", [])
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:
self._error_text.value = f"Ответьте на все вопросы ({len(unanswered)} осталось)"
self._error_text.update()
first_idx = questions.index(unanswered[0])
self._go(first_idx)
return
self._error_text.value = ""
self._submit_btn.disabled = True
self._submit_btn.update()
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}
@@ -170,82 +267,87 @@ class PollPage(ft.Column):
)
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(
ft.Column(
alignment=ft.MainAxisAlignment.CENTER,
controls=[ft.ProgressRing(width=40, height=40, color=ds.clolors.laer1)],
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
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),
]
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,
)
],
)
)
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(
ds.CastomButton(
text="Вернуться к тестам",
on_click=lambda _: self._page.go(f"/take_test/{url_key}"),
ft.Container(
expand=True,
content=ft.Image(
src=data_uri,
expand=True,
height=380,
fit=ft.BoxFit.CONTAIN,
),
)
)
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,
),
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,
)
],
)
]
self.update()
)
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()

View File

@@ -21,7 +21,6 @@ class PollView(ft.View):
padding=ft.padding.symmetric(horizontal=12, vertical=8),
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
vertical_alignment=ft.MainAxisAlignment.START,
scroll=ft.ScrollMode.AUTO,
controls=[
ft.Column(
expand=True,