Files
api-copp/web/designer.py
2026-04-09 16:43:59 +05:00

546 lines
24 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
# Глобальный масштаб интерфейса. 1.0 = 100%, 1.7 = 170%.
# Меняйте только эту переменную (или через theme_manager.apply_scale),
# чтобы масштабировать весь UI.
SCALE: float = 1.7
def s(v: int | float) -> int:
"""Применяет SCALE к любому размеру: шрифтам, отступам, ицих."""
return round(v * SCALE)
class colors:
# Основная палитра в стиле copp74.ru
primary = "#F85A40" # Основной оранжево-красный (логотип ЦОПП)
primary_dark = "#D94530" # Тёмный для hover/активных состояний
primary_light = "#FFEAB7" # Светлый персиковый (вторичный фон)
# Фоны
background = "#F4F6F8" # Светло-серый фон страницы
surface = "#FFFFFF" # Белый для карточек и форм
surface_alt = "#F0F2F5" # Альтернативные блоки
# Акцент — жёлтый
accent = "#FFC845" # Жёлтый акцент
accent_lime = "#FFC845" # Алиас для совместимости
secondary_light = "#FFEAB7" # Алиас для совместимости
secondary_yellow = "#FFC845" # Алиас
# Синий — информация, ссылки
info = "#007FBD" # Синий
info_light = "#D9F1F4" # Светлый голубой
tertiary_light = "#D9F1F4" # Алиас для совместимости
tertiary_blue = "#007FBD" # Алиас для совместимости
secondary_orange = "#F85A40" # Алиас для совместимости
# Зелёный — успех
success = "#85C446" # Зелёный
success_light = "#C0DE9E" # Светло-зелёный
# Текст
text_primary = "#1E2A3A" # Почти чёрный для заголовков
text_secondary = "#5A6779" # Серый для второстепенного текста
text_on_primary = "#FFFFFF" # Белый текст на оранжевом фоне
# Граница
border = "#DDE3EA" # Светло-серая граница
class CastomText(ft.Text):
def __init__(self, text, size=14, color=colors.text_primary, weight=ft.FontWeight.NORMAL, **kwargs):
super().__init__(
value=text,
color=color,
size=s(size),
weight=weight,
max_lines=99,
**kwargs,
)
class CastomTextField_input(ft.TextField):
def __init__(self, on_change=None, on_submit=None, label="Ваше имя", hint_text="Введите текст...", prefix_icon=None, suffix_icon=None, **kwargs):
super().__init__(
label=label,
hint_text=hint_text,
filled=True,
bgcolor=colors.surface,
border_color=colors.border,
focused_border_color=colors.primary,
border_radius=s(10),
label_style=ft.TextStyle(
color=colors.text_secondary,
size=s(13),
weight=ft.FontWeight.W_500,
),
hint_style=ft.TextStyle(
color=colors.border,
size=s(14),
italic=True,
),
text_style=ft.TextStyle(
color=colors.text_primary,
size=s(15),
),
content_padding=ft.padding.symmetric(horizontal=s(14), vertical=s(14)),
prefix_icon=prefix_icon,
suffix_icon=suffix_icon,
on_change=on_change,
on_submit=on_submit,
**kwargs,
)
class CastomButton(ft.Button):
def __init__(self, text, on_click):
super().__init__(
content=ft.Text(
text,
color=colors.text_on_primary,
size=s(14),
weight=ft.FontWeight.W_600,
) if isinstance(text, str) else text,
style=ft.ButtonStyle(
color=colors.text_on_primary,
bgcolor={
ft.ControlState.DEFAULT: colors.primary,
ft.ControlState.HOVERED: colors.primary_dark,
},
shape=ft.RoundedRectangleBorder(radius=s(10)),
padding=ft.padding.symmetric(horizontal=s(20), vertical=s(12)),
),
on_click=on_click
)
class CastomIconButton(ft.Button):
"""Кнопка с иконкой и текстом."""
def __init__(self, text, icon, on_click):
super().__init__(
content=ft.Row(
spacing=s(8),
tight=True,
controls=[
ft.Icon(icon, color=colors.text_on_primary, size=s(18)),
ft.Text(text, color=colors.text_on_primary, size=s(14), weight=ft.FontWeight.W_600),
],
),
style=ft.ButtonStyle(
bgcolor={
ft.ControlState.DEFAULT: colors.info,
ft.ControlState.HOVERED: "#006AA3",
},
shape=ft.RoundedRectangleBorder(radius=s(10)),
padding=ft.padding.symmetric(horizontal=s(18), vertical=s(12)),
),
on_click=on_click,
)
class CastomDropdown(ft.Dropdown):
def __init__(self, label="", hint_text="", options=None, on_change=None):
super().__init__(
label=label,
hint_text=hint_text,
options=options or [],
filled=True,
fill_color=colors.surface,
color=colors.text_primary,
border_color=colors.border,
border_radius=s(10),
focused_border_color=colors.primary,
label_style=ft.TextStyle(
color=colors.text_secondary,
size=s(13),
weight=ft.FontWeight.W_500,
),
hint_style=ft.TextStyle(
color=colors.border,
size=s(14),
italic=True,
),
text_style=ft.TextStyle(
color=colors.text_primary,
size=s(15),
),
menu_style=ft.MenuStyle(
bgcolor=colors.surface,
),
)
if on_change is not None:
self.on_select = on_change
class SearchableDropdown(ft.Container):
"""Выпадающий список с поисковой строкой — замена CastomDropdown для больших наборов данных."""
def __init__(self, label: str = "", hint_text: str = "", options=None, on_select=None):
self._label_str = label
self._hint_str = hint_text
self._options: list = options or []
self.on_select = on_select
self._value = None
self._value_text = ft.Text(
hint_text,
color=colors.border,
size=s(14),
italic=True,
expand=True,
no_wrap=True,
overflow=ft.TextOverflow.ELLIPSIS,
)
super().__init__(
content=ft.Column(
spacing=s(2),
tight=True,
controls=[
ft.Text(
label,
color=colors.text_secondary,
size=s(13),
weight=ft.FontWeight.W_500,
),
ft.Row(
controls=[
self._value_text,
ft.Icon(ft.Icons.ARROW_DROP_DOWN, color=colors.text_secondary, size=s(20)),
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
],
),
bgcolor=colors.surface,
border=ft.border.all(1, colors.border),
border_radius=s(10),
padding=ft.padding.symmetric(horizontal=s(14), vertical=s(12)),
on_click=self._open_dialog,
ink=True,
)
@property
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = val
if val is None:
self._value_text.value = self._hint_str
self._value_text.color = colors.border
self._value_text.italic = True
else:
for opt in self._options:
if opt.key == val:
self._value_text.value = opt.text
self._value_text.color = colors.text_primary
self._value_text.italic = False
break
if self.page:
self.update()
@property
def options(self):
return self._options
@options.setter
def options(self, opts):
self._options = opts or []
@property
def hint_text(self):
return self._hint_str
@hint_text.setter
def hint_text(self, val):
self._hint_str = val
if self._value is None:
self._value_text.value = val
if self.page:
self.update()
def _open_dialog(self, e):
search_field = CastomTextField_input(
label="Поиск",
hint_text="Введите для поиска...",
prefix_icon=ft.Icons.SEARCH,
autofocus=True,
)
items_list = ft.ListView(spacing=0, expand=True)
def _build_items(query: str = ""):
q = query.lower()
items_list.controls = [
ft.ListTile(
title=ft.Text(opt.text, color=colors.text_primary, size=s(14)),
hover_color=colors.primary_light,
on_click=lambda ev, o=opt: _select(o),
)
for opt in self._options
if not q or q in opt.text.lower()
]
def _on_search(ev):
_build_items(search_field.value or "")
items_list.update()
def _select(option):
self._value = option.key
self._value_text.value = option.text
self._value_text.color = colors.text_primary
self._value_text.italic = False
dlg.open = False
self.page.update()
self.update()
if self.on_select:
self.on_select(type("_E", (), {"control": self})())
search_field.on_change = _on_search
_build_items()
dlg = ft.AlertDialog(
title=ft.Text(
self._label_str,
color=colors.text_primary,
size=s(16),
weight=ft.FontWeight.W_600,
),
content=ft.Container(
content=ft.Column(
controls=[
search_field,
ft.Divider(color=colors.border),
items_list,
],
spacing=s(8),
tight=True,
),
height=s(350),
width=s(400),
),
bgcolor=colors.surface,
shape=ft.RoundedRectangleBorder(radius=s(12)),
)
self.page.overlay.append(dlg)
dlg.open = True
self.page.update()
class CastomSwitch(ft.Switch):
def __init__(self, label, on_change):
super().__init__(
label=label,
label_text_style=ft.TextStyle(size=s(13), color=colors.text_primary),
active_color=colors.primary,
active_track_color=colors.primary_light,
inactive_thumb_color=colors.border,
inactive_track_color=colors.surface_alt,
on_change=on_change
)
class AlterDiaalogApproval(ft.AlertDialog):
def __init__(self, on_accept=None):
self._on_accept = on_accept
self.switch_approval = CastomSwitch(
label="",
on_change=self.switch_changed
)
self.button_approval = CastomButton(
text="Принять",
on_click=self.accept
)
self.button_approval.disabled = True
super().__init__(
bgcolor=colors.surface,
title=ft.Text(
"Пользовательское соглашение",
color=colors.primary,
size=s(16),
weight=ft.FontWeight.BOLD,
),
content=ft.Container(
height=s(400),
content=ft.Column(
scroll=ft.ScrollMode.AUTO,
expand=True,
controls=[
ft.Markdown(
"""
# ПОЛЬЗОВАТЕЛЬСКОЕ СОГЛАШЕНИЕ
## 1. ОБЩИЕ ПОЛОЖЕНИЯ
Настоящее Пользовательское соглашение (далее — «Соглашение») регулирует отношения между администрацией сайта https://copp74.ru (далее — «Сайт», «Мы», «Администрация») и пользователем (далее — «Пользователь», «Вы»).
Используя Сайт, Вы автоматически соглашаетесь со всеми условиями настоящего Соглашения. Если Вы не согласны с этими условиями, пожалуйста, не используйте Сайт.
## 2. ОПИСАНИЕ САЙТА
Сайт https://copp74.ru представляет собой онлайн-платформу для прохождения тестов и проверки знаний. Основная функция Сайта — предоставление Пользователям возможности проходить различные тесты и получать результаты.
## 3. РЕГИСТРАЦИЯ И ЛИЧНЫЕ ДАННЫЕ
### 3.1 Регистрация
Для использования Сайта Пользователь должен зарегистрироваться и указать следующую информацию:
- Фамилию
- Имя
- Учебное заведение (образовательную организацию)
### 3.2 Назначение личных данных
Указанные фамилия, имя и учебное заведение используются исключительно для:
- Идентификации Пользователя в системе
- Отображения результатов тестирования
- Ведения статистики прохождения тестов
- Организации пользователей по учебным заведениям для аналитики
- Внутреннего анализа и улучшения качества тестирования
### 3.3 Защита личных данных
**Мы гарантируем, что Ваши персональные данные (фамилия, имя и информация об учебном заведении):**
- Не будут переданы третьим лицам или организациям без Вашего согласия
- Не будут использованы в коммерческих целях
- Не будут распространяться без Вашего согласия
- Хранятся в защищенной базе данных
Данные будут удалены в соответствии с законодательством Российской Федерации по защите персональных данных при закрытии аккаунта или по запросу Пользователя.
## 4. ПРАВА И ОБЯЗАННОСТИ ПОЛЬЗОВАТЕЛЯ
### 4.1 Права Пользователя
Пользователь имеет право:
- Проходить тесты, размещенные на Сайте
- Получать результаты своего тестирования
- Запросить удаление своего аккаунта и данных
- Обратиться в поддержку по вопросам использования Сайта
- Получить информацию о том, как его данные обрабатываются
### 4.2 Обязанности Пользователя
Пользователь обязуется:
- Предоставлять достоверную информацию при регистрации
- Указывать корректное название учебного заведения
- Не использовать Сайт в противоправных целях
- Не пытаться получить несанкционированный доступ к системам Сайта
- Не воспроизводить, не копировать и не распространять содержимое Сайта без разрешения
- Соблюдать все применимые законы Российской Федерации
## 5. ОГРАНИЧЕНИЕ ОТВЕТСТВЕННОСТИ
### 5.1 Предоставление Сайта «Как есть»
Сайт предоставляется в режиме «как есть» без каких-либо явных или подразумеваемых гарантий. Администрация не гарантирует:
- Непрерывную работу Сайта
- Отсутствие ошибок в тестах или результатах
- Соответствие Сайта конкретным целям Пользователя
### 5.2 Ограничение ответственности
В максимальной степени, допускаемой законодательством Российской Федерации, Администрация не несет ответственность за:
- Убытки, возникшие в результате использования или невозможности использования Сайта
- Технические сбои, потерю данных
- Ущерб, причиненный прямо или косвенно в связи с использованием Сайта
## 6. ИНТЕЛЛЕКТУАЛЬНАЯ СОБСТВЕННОСТЬ
Все содержимое Сайта, включая тесты, вопросы, задания, дизайн, логотипы и тексты, защищено авторским правом и законами об интеллектуальной собственности Российской Федерации. Пользователь не имеет права использовать это содержимое в коммерческих целях без письменного разрешения Администрации.
## 7. ИЗМЕНЕНИЕ СОГЛАШЕНИЯ
Администрация оставляет за собой право изменять условия настоящего Соглашения в любое время. Изменения вступают в силе с момента их публикации на Сайте. Продолжение использования Сайта после публикации изменений означает согласие Пользователя с новыми условиями.
## 8. ПРЕКРАЩЕНИЕ ДОСТУПА
Администрация может заблокировать доступ Пользователя к Сайту без предварительного уведомления, если:
- Пользователь нарушает условия настоящего Соглашения
- Пользователь использует Сайт в противоправных целях
- Пользователь предоставил поддельную информацию при регистрации
## 9. КОНФИДЕНЦИАЛЬНОСТЬ
Пожалуйста, ознакомьтесь с нашей Политикой конфиденциальности, которая описывает, как мы обращаемся с Вашими личными данными и какие меры безопасности мы применяем.
## 10. ПРИМЕНИМОЕ ПРАВО
Настоящее Соглашение регулируется законодательством Российской Федерации. Все споры, возникающие в связи с Соглашением, подлежат разрешению в судах Российской Федерации.
## 11. КОНТАКТНАЯ ИНФОРМАЦИЯ
Если у Вас есть вопросы по данному Соглашению или функционированию Сайта https://copp74.ru, свяжитесь с нами через форму обратной связи на Сайте.
## 12. ЗАКЛЮЧИТЕЛЬНЫЕ ПОЛОЖЕНИЯ
Если какая-либо часть настоящего Соглашения будет признана недействительной, остальные положения остаются в полной силе.
Пользователь подтверждает, что прочитал, понял и согласен со всеми условиями настоящего Соглашения перед использованием Сайта.
**Дата последнего обновления:** 19.03.2026
---
*Соглашение соответствует законодательству Российской Федерации, включая Федеральный закон «О защите персональных данных» и Федеральный закон «О защите прав потребителей».*
""",
expand=True,
md_style_sheet=ft.MarkdownStyleSheet(
p_text_style=ft.TextStyle(color=colors.text_primary, size=s(12)),
h1_text_style=ft.TextStyle(color=colors.primary, size=s(18), weight=ft.FontWeight.BOLD),
h2_text_style=ft.TextStyle(color=colors.info, size=s(15), weight=ft.FontWeight.BOLD),
h3_text_style=ft.TextStyle(color=colors.text_secondary, size=s(13), weight=ft.FontWeight.BOLD),
a_text_style=ft.TextStyle(color=colors.primary),
),
)
]
),
),
actions=[
ft.Column(
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
ft.Row(
expand=True,
alignment=ft.MainAxisAlignment.START,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
controls=[
self.switch_approval,
ft.Text(
"Я согласен с условиями пользовательского соглашения",
size=s(13),
color=colors.text_primary,
expand=True,
no_wrap=False,
),
]
),
ft.Row(
expand=True,
alignment=ft.MainAxisAlignment.CENTER,
controls=[
self.button_approval,
]
)
]
)
],
actions_alignment=ft.MainAxisAlignment.CENTER,
)
def switch_changed(self, e):
#self.switch_approval.value = not e.control.value
self.button_approval.disabled = not e.control.value
self.update()
def accept(self, e):
self.open = False
self.page.update()
if self._on_accept:
self._on_accept()
else:
self.page.run_task(self.page.push_route, "/reg")