new shems
This commit is contained in:
@@ -156,6 +156,30 @@ async def get_response(response_id: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
async def get_radar_result(response_id: str) -> dict | None:
|
||||
"""GET /responses/{response_id}/radar — данные для радарной диаграммы."""
|
||||
try:
|
||||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||||
resp = await client.get(f"/responses/{response_id}/radar")
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def get_radar_svg_bytes(response_id: str) -> bytes | None:
|
||||
"""GET /responses/{response_id}/radar/image — SVG-файл с диаграммой."""
|
||||
try:
|
||||
async with httpx.AsyncClient(base_url=API_URL, timeout=_TIMEOUT) as client:
|
||||
resp = await client.get(f"/responses/{response_id}/radar/image")
|
||||
if resp.status_code == 200:
|
||||
return resp.content
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
async def submit_response(
|
||||
poll_id: str,
|
||||
answers: list[dict],
|
||||
|
||||
142
web/radar_chart.py
Normal file
142
web/radar_chart.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
RadarChart — переиспользуемый виджет паутинной диаграммы (ft.Stack).
|
||||
|
||||
Параметры:
|
||||
items — список осей из API /responses/{id}/radar:
|
||||
[{dimension_name, dimension_color, dimension_position, value}, ...]
|
||||
max_val — максимум на шкале (default 14 — для теста Голланда)
|
||||
"""
|
||||
import math
|
||||
import flet as ft
|
||||
import flet.canvas as cv
|
||||
import designer as ds
|
||||
|
||||
# ── размеры ─────────────────────────────────────────────────────────────────
|
||||
_CS = 260 # размер canvas (×2 = итоговая зона графика)
|
||||
_CX = _CS // 2
|
||||
_CY = _CS // 2
|
||||
_R = 80 # радиус внешнего кольца
|
||||
_PAD = 95 # отступ вокруг canvas — оставляем место для подписей
|
||||
_LBL_R = _R + 50 # расстояние от центра до центра подписи (в координатах Stack)
|
||||
_W_LBL = 130 # ширина контейнера подписи (достаточно для «Конвенциональный»)
|
||||
|
||||
|
||||
def _axis_xy(n: int, i: int, frac: float = 1.0) -> tuple[float, float]:
|
||||
a = math.radians(-90.0 + 360.0 * i / n)
|
||||
return _CX + _R * frac * math.cos(a), _CY + _R * frac * math.sin(a)
|
||||
|
||||
|
||||
def _closed_path(pts: list[tuple]) -> list:
|
||||
els = [cv.Path.MoveTo(*pts[0])]
|
||||
for p in pts[1:]:
|
||||
els.append(cv.Path.LineTo(*p))
|
||||
els.append(cv.Path.Close())
|
||||
return els
|
||||
|
||||
|
||||
class RadarChart(ft.Stack):
|
||||
"""Паутинная диаграмма типов/сфер по данным radar API."""
|
||||
|
||||
def __init__(self, items: list[dict], max_val: float = 14.0):
|
||||
n = len(items)
|
||||
shapes: list = []
|
||||
|
||||
# ── сетка (5 уровней) ────────────────────────────────────────────────
|
||||
for lvl in range(1, 6):
|
||||
ring = [_axis_xy(n, i, lvl / 5) for i in range(n)]
|
||||
shapes.append(cv.Path(
|
||||
_closed_path(ring),
|
||||
paint=ft.Paint(
|
||||
style=ft.PaintingStyle.STROKE,
|
||||
stroke_width=1,
|
||||
color="#30F4EFFA",
|
||||
),
|
||||
))
|
||||
|
||||
# ── оси от центра ────────────────────────────────────────────────────
|
||||
for i in range(n):
|
||||
x, y = _axis_xy(n, i)
|
||||
shapes.append(cv.Path(
|
||||
[cv.Path.MoveTo(_CX, _CY), cv.Path.LineTo(x, y)],
|
||||
paint=ft.Paint(stroke_width=1, color="#40F4EFFA"),
|
||||
))
|
||||
|
||||
# ── полигон данных ───────────────────────────────────────────────────
|
||||
safe_max = max_val if max_val > 0 else 1.0
|
||||
data_pts = [
|
||||
_axis_xy(n, i, min(it["value"] / safe_max, 1.0))
|
||||
for i, it in enumerate(items)
|
||||
]
|
||||
shapes.append(cv.Path(
|
||||
_closed_path(data_pts),
|
||||
paint=ft.Paint(style=ft.PaintingStyle.FILL, color="#609B72CF"),
|
||||
))
|
||||
shapes.append(cv.Path(
|
||||
_closed_path(data_pts),
|
||||
paint=ft.Paint(
|
||||
style=ft.PaintingStyle.STROKE,
|
||||
stroke_width=2.5,
|
||||
color="#F4EFFA",
|
||||
),
|
||||
))
|
||||
for px, py in data_pts:
|
||||
shapes.append(cv.Circle(px, py, 4, paint=ft.Paint(color="#F4EFFA")))
|
||||
|
||||
canvas = cv.Canvas(shapes=shapes, width=_CS, height=_CS)
|
||||
|
||||
# ── подписи оси в Stack-координатах ──────────────────────────────────
|
||||
sw = _CS + 2 * _PAD
|
||||
sh = _CS + 2 * _PAD
|
||||
ccx = _PAD + _CX # центр графика в Stack
|
||||
ccy = _PAD + _CY
|
||||
|
||||
labels: list[ft.Control] = []
|
||||
for i, item in enumerate(items):
|
||||
a = math.radians(-90.0 + 360.0 * i / n)
|
||||
lx = ccx + _LBL_R * math.cos(a)
|
||||
ly = ccy + _LBL_R * math.sin(a)
|
||||
col = item.get("dimension_color") or ds.clolors.ferst
|
||||
|
||||
labels.append(ft.Container(
|
||||
left=lx - _W_LBL / 2,
|
||||
top=ly - 24,
|
||||
width=_W_LBL,
|
||||
content=ft.Column(
|
||||
spacing=1,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
controls=[
|
||||
ft.Text(
|
||||
item["dimension_name"],
|
||||
size=10,
|
||||
color=col,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
# разрешаем перенос только по словам (не по символам)
|
||||
overflow=ft.TextOverflow.FADE,
|
||||
max_lines=2,
|
||||
no_wrap=False,
|
||||
),
|
||||
ft.Text(
|
||||
str(int(item["value"])),
|
||||
size=11,
|
||||
color=ds.clolors.ferst,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
super().__init__(
|
||||
width=sw,
|
||||
height=sh,
|
||||
controls=[
|
||||
ft.Container(
|
||||
left=_PAD,
|
||||
top=_PAD,
|
||||
width=_CS,
|
||||
height=_CS,
|
||||
content=canvas,
|
||||
),
|
||||
*labels,
|
||||
],
|
||||
)
|
||||
105
web/radar_svg.py
Normal file
105
web/radar_svg.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""
|
||||
radar_svg.py — генерация SVG паутинной диаграммы для сохранения на устройство.
|
||||
|
||||
Не требует сторонних зависимостей (чистый Python).
|
||||
SVG содержит корректные шрифты и цвета, может быть открыт/сохранён в браузере.
|
||||
"""
|
||||
import math
|
||||
import base64
|
||||
|
||||
|
||||
def generate_radar_svg(items: list[dict], max_val: float = 14.0, size: int = 520) -> str:
|
||||
"""
|
||||
Возвращает SVG-строку паутинной диаграммы.
|
||||
|
||||
items — список осей (dimension_name, dimension_color, value)
|
||||
max_val — максимум шкалы
|
||||
size — размер холста в пикселях
|
||||
"""
|
||||
n = len(items)
|
||||
if n < 3:
|
||||
return f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}"><text x="10" y="20" fill="white">Нет данных</text></svg>'
|
||||
|
||||
cx = cy = size / 2
|
||||
R = size * 0.27 # радиус внешнего кольца
|
||||
lbl_r = R + size * 0.12 # расстояние до подписей от центра
|
||||
safe_max = max_val if max_val > 0 else 1.0
|
||||
|
||||
def axis_xy(i: int, frac: float = 1.0) -> tuple[float, float]:
|
||||
a = math.radians(-90.0 + 360.0 * i / n)
|
||||
return cx + R * frac * math.cos(a), cy + R * frac * math.sin(a)
|
||||
|
||||
def poly_pts(pts: list[tuple]) -> str:
|
||||
return " ".join(f"{x:.2f},{y:.2f}" for x, y in pts)
|
||||
|
||||
lines: list[str] = [
|
||||
f'<svg xmlns="http://www.w3.org/2000/svg" width="{size}" height="{size}">',
|
||||
# фон
|
||||
f'<rect width="{size}" height="{size}" fill="#2F184B" rx="16"/>',
|
||||
]
|
||||
|
||||
# ── концентрические кольца (сетка) ──────────────────────────────────────
|
||||
for lvl in range(1, 6):
|
||||
pts = [axis_xy(i, lvl / 5) for i in range(n)]
|
||||
lines.append(
|
||||
f'<polygon points="{poly_pts(pts)}" '
|
||||
f'fill="none" stroke="rgba(244,239,250,0.18)" stroke-width="1"/>'
|
||||
)
|
||||
|
||||
# ── оси ─────────────────────────────────────────────────────────────────
|
||||
for i in range(n):
|
||||
x, y = axis_xy(i)
|
||||
lines.append(
|
||||
f'<line x1="{cx:.2f}" y1="{cy:.2f}" x2="{x:.2f}" y2="{y:.2f}" '
|
||||
f'stroke="rgba(244,239,250,0.22)" stroke-width="1"/>'
|
||||
)
|
||||
|
||||
# ── полигон данных ───────────────────────────────────────────────────────
|
||||
data_pts = [axis_xy(i, min(it["value"] / safe_max, 1.0)) for i, it in enumerate(items)]
|
||||
lines.append(
|
||||
f'<polygon points="{poly_pts(data_pts)}" '
|
||||
f'fill="rgba(155,114,207,0.55)" stroke="#F4EFFA" stroke-width="2.5"/>'
|
||||
)
|
||||
|
||||
# ── точки на вершинах ────────────────────────────────────────────────────
|
||||
for x, y in data_pts:
|
||||
lines.append(f'<circle cx="{x:.2f}" cy="{y:.2f}" r="5" fill="#F4EFFA"/>')
|
||||
|
||||
# ── подписи осей ─────────────────────────────────────────────────────────
|
||||
for i, item in enumerate(items):
|
||||
a = math.radians(-90.0 + 360.0 * i / n)
|
||||
lx = cx + lbl_r * math.cos(a)
|
||||
ly = cy + lbl_r * math.sin(a)
|
||||
col = item.get("dimension_color") or "#F4EFFA"
|
||||
name = _xml_escape(item["dimension_name"])
|
||||
val = int(item["value"])
|
||||
|
||||
# название типа (жирный, цветной)
|
||||
lines.append(
|
||||
f'<text x="{lx:.2f}" y="{ly:.2f}" text-anchor="middle" '
|
||||
f'font-family="Arial,Helvetica,sans-serif" font-size="14" '
|
||||
f'font-weight="bold" fill="{col}">{name}</text>'
|
||||
)
|
||||
# значение (белый)
|
||||
lines.append(
|
||||
f'<text x="{lx:.2f}" y="{ly + 20:.2f}" text-anchor="middle" '
|
||||
f'font-family="Arial,Helvetica,sans-serif" font-size="13" '
|
||||
f'fill="#F4EFFA">{val}</text>'
|
||||
)
|
||||
|
||||
lines.append("</svg>")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _xml_escape(s: str) -> str:
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
def get_radar_data_uri(items: list[dict], max_val: float = 14.0) -> str:
|
||||
"""
|
||||
Возвращает data URI вида «data:image/svg+xml;base64,…».
|
||||
Передайте в page.launch_url(...) чтобы открыть/сохранить изображение.
|
||||
"""
|
||||
svg = generate_radar_svg(items, max_val)
|
||||
b64 = base64.b64encode(svg.encode("utf-8")).decode()
|
||||
return f"data:image/svg+xml;base64,{b64}"
|
||||
@@ -1,6 +1,8 @@
|
||||
import base64
|
||||
import flet as ft
|
||||
import designer as ds
|
||||
import api_client
|
||||
from radar_chart import RadarChart
|
||||
|
||||
|
||||
class PollPage(ft.Column):
|
||||
@@ -157,27 +159,80 @@ class PollPage(ft.Column):
|
||||
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)
|
||||
|
||||
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 radar and radar.get("items"):
|
||||
items = sorted(
|
||||
radar["items"],
|
||||
key=lambda x: (
|
||||
x.get("dimension_position") is None,
|
||||
x.get("dimension_position", 0),
|
||||
),
|
||||
)
|
||||
actual_max = max((it["value"] for it in items), default=1.0)
|
||||
max_val = max(actual_max, 14.0)
|
||||
|
||||
success_controls.append(
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
controls=[RadarChart(items, max_val)],
|
||||
)
|
||||
)
|
||||
|
||||
async def _download(_evt, _rid=response_id):
|
||||
svg_bytes = await api_client.get_radar_svg_bytes(_rid)
|
||||
if svg_bytes:
|
||||
b64 = base64.b64encode(svg_bytes).decode()
|
||||
uri = f"data:image/svg+xml;base64,{b64}"
|
||||
self._page.launch_url(uri, web_window_name="_blank")
|
||||
|
||||
success_controls.append(
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
controls=[
|
||||
ft.ElevatedButton(
|
||||
text="Сохранить диаграмму",
|
||||
icon=ft.Icons.DOWNLOAD,
|
||||
on_click=_download,
|
||||
style=ft.ButtonStyle(
|
||||
color=ds.clolors.ferst,
|
||||
bgcolor=ds.clolors.laer2,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
success_controls.append(
|
||||
ds.CastomButton(
|
||||
text="Вернуться к тестам",
|
||||
on_click=lambda _: self._page.go(f"/take_test/{url_key}"),
|
||||
)
|
||||
)
|
||||
|
||||
self.controls = [
|
||||
ft.Container(
|
||||
alignment=ft.Alignment(0, 0),
|
||||
padding=40,
|
||||
padding=20,
|
||||
content=ft.Column(
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
spacing=16,
|
||||
controls=[
|
||||
ft.Icon(ft.Icons.CHECK_CIRCLE_OUTLINE, size=64, color=ds.clolors.laer1),
|
||||
ds.CastomText(
|
||||
text="Ответы отправлены!",
|
||||
size=20,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
),
|
||||
ds.CastomButton(
|
||||
text="Вернуться к тестам",
|
||||
on_click=lambda _: self._page.go(f"/take_test/{url_key}"),
|
||||
),
|
||||
],
|
||||
controls=success_controls,
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import base64
|
||||
import flet as ft
|
||||
import designer as ds
|
||||
import api_client
|
||||
from radar_chart import RadarChart
|
||||
|
||||
|
||||
class ResponsePage(ft.Column):
|
||||
@@ -14,10 +16,7 @@ class ResponsePage(ft.Column):
|
||||
horizontal_alignment=ft.CrossAxisAlignment.STRETCH,
|
||||
spacing=8,
|
||||
controls=[
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
controls=[self._loading],
|
||||
)
|
||||
ft.Row(alignment=ft.MainAxisAlignment.CENTER, controls=[self._loading])
|
||||
],
|
||||
)
|
||||
|
||||
@@ -34,84 +33,106 @@ class ResponsePage(ft.Column):
|
||||
self.update()
|
||||
return
|
||||
|
||||
self._render(response, poll)
|
||||
radar = await api_client.get_radar_result(self._response_id)
|
||||
self._render(response, poll, radar)
|
||||
self.update()
|
||||
|
||||
def _render(self, response: dict, poll: dict):
|
||||
# Строим индексы для быстрого поиска
|
||||
def _render(self, response: dict, poll: dict, radar: dict | None):
|
||||
questions_map = {q["id"]: q for q in poll.get("questions", [])}
|
||||
choices_map = {
|
||||
c["id"]: c["text"]
|
||||
for q in poll.get("questions", [])
|
||||
for c in q.get("choices", [])
|
||||
}
|
||||
|
||||
submitted_at = response["submitted_at"][:19].replace("T", " ")
|
||||
answer_cards = []
|
||||
|
||||
controls: list[ft.Control] = [
|
||||
ft.Container(
|
||||
padding=ft.padding.only(bottom=8),
|
||||
content=ft.Column(spacing=4, controls=[
|
||||
ds.CastomText(text=poll.get("title", ""), size=20, weight=ft.FontWeight.BOLD),
|
||||
ds.CastomText(text=f"Пройден: {submitted_at}", size=12),
|
||||
]),
|
||||
)
|
||||
]
|
||||
|
||||
# ── Паутинная диаграмма ──────────────────────────────────────────────
|
||||
if radar and radar.get("items"):
|
||||
items = sorted(
|
||||
radar["items"],
|
||||
key=lambda x: (x.get("dimension_position") is None, x.get("dimension_position", 0)),
|
||||
)
|
||||
actual_max = max((it["value"] for it in items), default=1.0)
|
||||
max_val = max(actual_max, 14.0)
|
||||
|
||||
async def _download(_evt, _rid=self._response_id):
|
||||
svg_bytes = await api_client.get_radar_svg_bytes(_rid)
|
||||
if svg_bytes:
|
||||
b64 = base64.b64encode(svg_bytes).decode()
|
||||
uri = f"data:image/svg+xml;base64,{b64}"
|
||||
self._page.launch_url(uri, web_window_name="_blank")
|
||||
|
||||
controls.append(ft.Container(
|
||||
border_radius=16,
|
||||
padding=12,
|
||||
bgcolor=ds.clolors.laer3,
|
||||
content=ft.Column(
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
spacing=10,
|
||||
controls=[
|
||||
ds.CastomText(text="Результат", size=16, weight=ft.FontWeight.BOLD),
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
controls=[RadarChart(items, max_val)],
|
||||
),
|
||||
ft.Row(
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
controls=[
|
||||
ft.ElevatedButton(
|
||||
text="Сохранить диаграмму",
|
||||
icon=ft.Icons.DOWNLOAD,
|
||||
on_click=_download,
|
||||
style=ft.ButtonStyle(
|
||||
color=ds.clolors.ferst,
|
||||
bgcolor=ds.clolors.laer2,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
# ── Ответы ───────────────────────────────────────────────────────────
|
||||
answer_cards: list[ft.Control] = []
|
||||
for answer in response.get("answers", []):
|
||||
q = questions_map.get(answer["question_id"])
|
||||
if not q:
|
||||
continue
|
||||
|
||||
selected_text = choices_map.get(answer.get("choice_id") or "") or answer.get("text") or "—"
|
||||
|
||||
# Варианты с подсветкой выбранного
|
||||
choice_rows = []
|
||||
for c in q.get("choices", []):
|
||||
is_selected = c["id"] == answer.get("choice_id")
|
||||
choice_rows.append(
|
||||
ft.Container(
|
||||
border_radius=8,
|
||||
padding=ft.padding.symmetric(horizontal=12, vertical=6),
|
||||
bgcolor=ds.clolors.laer1 if is_selected else ds.clolors.laer3,
|
||||
content=ft.Row(
|
||||
spacing=8,
|
||||
controls=[
|
||||
ft.Icon(
|
||||
ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED,
|
||||
color=ds.clolors.ferst,
|
||||
size=16,
|
||||
),
|
||||
ds.CastomText(text=c["text"], size=13),
|
||||
],
|
||||
choice_rows.append(ft.Container(
|
||||
border_radius=8,
|
||||
padding=ft.padding.symmetric(horizontal=12, vertical=6),
|
||||
bgcolor=ds.clolors.laer1 if is_selected else ds.clolors.laer3,
|
||||
content=ft.Row(spacing=8, controls=[
|
||||
ft.Icon(
|
||||
ft.Icons.CHECK_CIRCLE if is_selected else ft.Icons.RADIO_BUTTON_UNCHECKED,
|
||||
color=ds.clolors.ferst, size=16,
|
||||
),
|
||||
)
|
||||
)
|
||||
ds.CastomText(text=c["text"], size=13),
|
||||
]),
|
||||
))
|
||||
|
||||
answer_cards.append(
|
||||
ft.Container(
|
||||
border_radius=12,
|
||||
padding=16,
|
||||
bgcolor=ds.clolors.laer2,
|
||||
content=ft.Column(
|
||||
spacing=8,
|
||||
controls=[
|
||||
ds.CastomText(
|
||||
text=q.get("text", ""),
|
||||
size=14,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
),
|
||||
*choice_rows,
|
||||
],
|
||||
),
|
||||
)
|
||||
)
|
||||
answer_cards.append(ft.Container(
|
||||
border_radius=12,
|
||||
padding=16,
|
||||
bgcolor=ds.clolors.laer2,
|
||||
content=ft.Column(spacing=8, controls=[
|
||||
ds.CastomText(text=q.get("text", ""), size=14, weight=ft.FontWeight.BOLD),
|
||||
*choice_rows,
|
||||
]),
|
||||
))
|
||||
|
||||
self.controls = [
|
||||
ft.Container(
|
||||
padding=ft.padding.only(bottom=8),
|
||||
content=ft.Column(
|
||||
spacing=4,
|
||||
controls=[
|
||||
ds.CastomText(
|
||||
text=poll.get("title", ""),
|
||||
size=20,
|
||||
weight=ft.FontWeight.BOLD,
|
||||
),
|
||||
ds.CastomText(text=f"Пройден: {submitted_at}", size=12),
|
||||
],
|
||||
),
|
||||
),
|
||||
*answer_cards,
|
||||
]
|
||||
if answer_cards:
|
||||
controls.append(ds.CastomText(text="Ответы", size=16, weight=ft.FontWeight.BOLD))
|
||||
controls.extend(answer_cards)
|
||||
|
||||
self.controls = controls
|
||||
|
||||
Reference in New Issue
Block a user