From 817fb82476dc381f3cc63de4e365c13504e4cedb Mon Sep 17 00:00:00 2001 From: Egor Date: Thu, 25 Sep 2025 18:04:04 +0300 Subject: [PATCH] Revert "Revert "Improve bot configuration admin menu grouping"" --- app/handlers/admin/bot_configuration.py | 351 ++++++++++++++++++++---- 1 file changed, 290 insertions(+), 61 deletions(-) diff --git a/app/handlers/admin/bot_configuration.py b/app/handlers/admin/bot_configuration.py index 76120f61..b2b5281e 100644 --- a/app/handlers/admin/bot_configuration.py +++ b/app/handlers/admin/bot_configuration.py @@ -1,5 +1,5 @@ import math -from typing import Tuple +from typing import Iterable, List, Tuple from aiogram import Dispatcher, F, types from aiogram.fsm.context import FSMContext @@ -16,22 +16,141 @@ CATEGORY_PAGE_SIZE = 10 SETTINGS_PAGE_SIZE = 8 -def _parse_category_payload(payload: str) -> Tuple[str, int]: +CATEGORY_GROUP_DEFINITIONS: Tuple[Tuple[str, str, Tuple[str, ...]], ...] = ( + ( + "core", + "⚙️ Основные настройки", + ( + "DEFAULT", + "ADMIN", + "SUPPORT", + "REMNAWAVE", + "VERSION", + "DEBUG", + "LOG", + "BACKUP", + ), + ), + ( + "infrastructure", + "🗄️ Инфраструктура", + ("DATABASE", "POSTGRES", "SQLITE", "REDIS", "WEBHOOK", "SERVER", "MONITORING", "MAINTENANCE"), + ), + ( + "billing", + "💳 Оплаты и тарифы", + ("PRICE", "PAYMENT", "YOOKASSA", "CRYPTOBOT", "MULENPAY", "PAL24", "AUTOPAY", "TRIAL"), + ), + ( + "communication", + "📡 Подключение и коммуникации", + ("CONNECT", "CHANNEL", "TELEGRAM", "HAPP"), + ), + ( + "loyalty", + "🎁 Рефералы и лояльность", + ("REFERRAL", "TRIBUTE"), + ), +) + +CATEGORY_FALLBACK_KEY = "other" +CATEGORY_FALLBACK_TITLE = "📦 Прочие настройки" + + +def _chunk(buttons: Iterable[types.InlineKeyboardButton], size: int) -> Iterable[List[types.InlineKeyboardButton]]: + buttons_list = list(buttons) + for index in range(0, len(buttons_list), size): + yield buttons_list[index : index + size] + + +def _parse_category_payload(payload: str) -> Tuple[str, str, int, int]: parts = payload.split(":") - if len(parts) == 3: - _, category_key, page_raw = parts + group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY + category_key = parts[2] if len(parts) > 2 else "" + + def _safe_int(value: str, default: int = 1) -> int: try: - return category_key, max(1, int(page_raw)) - except ValueError: - return category_key, 1 - if len(parts) == 2: - _, category_key = parts - return category_key, 1 - return "", 1 + return max(1, int(value)) + except (TypeError, ValueError): + return default + + category_page = _safe_int(parts[3]) if len(parts) > 3 else 1 + settings_page = _safe_int(parts[4]) if len(parts) > 4 else 1 + return group_key, category_key, category_page, settings_page -def _build_categories_keyboard(language: str, page: int = 1) -> types.InlineKeyboardMarkup: +def _parse_group_payload(payload: str) -> Tuple[str, int]: + parts = payload.split(":") + group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY + try: + page = max(1, int(parts[2])) + except (IndexError, ValueError): + page = 1 + return group_key, page + + +def _get_grouped_categories() -> List[Tuple[str, str, List[Tuple[str, str, int]]]]: categories = bot_configuration_service.get_categories() + categories_map = {key: (label, count) for key, label, count in categories} + used: set[str] = set() + grouped: List[Tuple[str, str, List[Tuple[str, str, int]]]] = [] + + for group_key, title, category_keys in CATEGORY_GROUP_DEFINITIONS: + items: List[Tuple[str, str, int]] = [] + for category_key in category_keys: + if category_key in categories_map: + label, count = categories_map[category_key] + items.append((category_key, label, count)) + used.add(category_key) + if items: + grouped.append((group_key, title, items)) + + remaining = [ + (key, label, count) + for key, (label, count) in categories_map.items() + if key not in used + ] + + if remaining: + remaining.sort(key=lambda item: item[1]) + grouped.append((CATEGORY_FALLBACK_KEY, CATEGORY_FALLBACK_TITLE, remaining)) + + return grouped + + +def _build_groups_keyboard() -> types.InlineKeyboardMarkup: + grouped = _get_grouped_categories() + rows: list[list[types.InlineKeyboardButton]] = [] + + for group_key, title, items in grouped: + total = sum(count for _, _, count in items) + rows.append( + [ + types.InlineKeyboardButton( + text=f"{title} ({total})", + callback_data=f"botcfg_group:{group_key}:1", + ) + ] + ) + + rows.append( + [ + types.InlineKeyboardButton( + text="⬅️ Назад", + callback_data="admin_submenu_settings", + ) + ] + ) + + return types.InlineKeyboardMarkup(inline_keyboard=rows) + + +def _build_categories_keyboard( + group_key: str, + group_title: str, + categories: List[Tuple[str, str, int]], + page: int = 1, +) -> types.InlineKeyboardMarkup: total_pages = max(1, math.ceil(len(categories) / CATEGORY_PAGE_SIZE)) page = max(1, min(page, total_pages)) @@ -40,45 +159,68 @@ def _build_categories_keyboard(language: str, page: int = 1) -> types.InlineKeyb sliced = categories[start:end] rows: list[list[types.InlineKeyboardButton]] = [] + rows.append( + [ + types.InlineKeyboardButton( + text=f"— {group_title} —", + callback_data="botcfg_group:noop", + ) + ] + ) + + buttons: List[types.InlineKeyboardButton] = [] for category_key, label, count in sliced: button_text = f"{label} ({count})" - rows.append([ + buttons.append( types.InlineKeyboardButton( text=button_text, - callback_data=f"botcfg_cat:{category_key}:1", + callback_data=f"botcfg_cat:{group_key}:{category_key}:{page}:1", ) - ]) + ) + + for chunk in _chunk(buttons, 2): + rows.append(list(chunk)) if total_pages > 1: nav_row: list[types.InlineKeyboardButton] = [] if page > 1: nav_row.append( types.InlineKeyboardButton( - text="⬅️", callback_data=f"botcfg_categories:{page - 1}" + text="⬅️", + callback_data=f"botcfg_group:{group_key}:{page - 1}", ) ) nav_row.append( types.InlineKeyboardButton( - text=f"{page}/{total_pages}", callback_data="botcfg_categories:noop" + text=f"{page}/{total_pages}", + callback_data="botcfg_group:noop", ) ) if page < total_pages: nav_row.append( types.InlineKeyboardButton( - text="➡️", callback_data=f"botcfg_categories:{page + 1}" + text="➡️", + callback_data=f"botcfg_group:{group_key}:{page + 1}", ) ) rows.append(nav_row) - rows.append([ - types.InlineKeyboardButton(text="⬅️ Назад", callback_data="admin_submenu_settings") - ]) + rows.append( + [ + types.InlineKeyboardButton( + text="⬅️ К разделам", + callback_data="admin_bot_config", + ) + ] + ) return types.InlineKeyboardMarkup(inline_keyboard=rows) def _build_settings_keyboard( category_key: str, + group_key: str, + category_page: int, language: str, page: int = 1, ) -> types.InlineKeyboardMarkup: @@ -94,13 +236,19 @@ def _build_settings_keyboard( for definition in sliced: value_preview = bot_configuration_service.format_value_for_list(definition.key) - button_text = f"{definition.key} = {value_preview}" - rows.append([ - types.InlineKeyboardButton( - text=button_text, - callback_data=f"botcfg_setting:{definition.key}", - ) - ]) + button_text = f"{definition.display_name} · {value_preview}" + if len(button_text) > 64: + button_text = button_text[:63] + "…" + rows.append( + [ + types.InlineKeyboardButton( + text=button_text, + callback_data=( + f"botcfg_setting:{group_key}:{category_page}:{page}:{definition.key}" + ), + ) + ] + ) if total_pages > 1: nav_row: list[types.InlineKeyboardButton] = [] @@ -108,7 +256,9 @@ def _build_settings_keyboard( nav_row.append( types.InlineKeyboardButton( text="⬅️", - callback_data=f"botcfg_cat:{category_key}:{page - 1}", + callback_data=( + f"botcfg_cat:{group_key}:{category_key}:{category_page}:{page - 1}" + ), ) ) nav_row.append( @@ -120,7 +270,9 @@ def _build_settings_keyboard( nav_row.append( types.InlineKeyboardButton( text="➡️", - callback_data=f"botcfg_cat:{category_key}:{page + 1}", + callback_data=( + f"botcfg_cat:{group_key}:{category_key}:{category_page}:{page + 1}" + ), ) ) rows.append(nav_row) @@ -128,14 +280,19 @@ def _build_settings_keyboard( rows.append([ types.InlineKeyboardButton( text="⬅️ К категориям", - callback_data="admin_bot_config", + callback_data=f"botcfg_group:{group_key}:{category_page}", ) ]) return types.InlineKeyboardMarkup(inline_keyboard=rows) -def _build_setting_keyboard(key: str) -> types.InlineKeyboardMarkup: +def _build_setting_keyboard( + key: str, + group_key: str, + category_page: int, + settings_page: int, +) -> types.InlineKeyboardMarkup: definition = bot_configuration_service.get_definition(key) rows: list[list[types.InlineKeyboardButton]] = [] @@ -143,14 +300,18 @@ def _build_setting_keyboard(key: str) -> types.InlineKeyboardMarkup: rows.append([ types.InlineKeyboardButton( text="🔁 Переключить", - callback_data=f"botcfg_toggle:{key}", + callback_data=( + f"botcfg_toggle:{group_key}:{category_page}:{settings_page}:{key}" + ), ) ]) rows.append([ types.InlineKeyboardButton( text="✏️ Изменить", - callback_data=f"botcfg_edit:{key}", + callback_data=( + f"botcfg_edit:{group_key}:{category_page}:{settings_page}:{key}" + ), ) ]) @@ -158,14 +319,18 @@ def _build_setting_keyboard(key: str) -> types.InlineKeyboardMarkup: rows.append([ types.InlineKeyboardButton( text="♻️ Сбросить", - callback_data=f"botcfg_reset:{key}", + callback_data=( + f"botcfg_reset:{group_key}:{category_page}:{settings_page}:{key}" + ), ) ]) rows.append([ types.InlineKeyboardButton( text="⬅️ Назад", - callback_data=f"botcfg_cat:{definition.category_key}:1", + callback_data=( + f"botcfg_cat:{group_key}:{definition.category_key}:{category_page}:{settings_page}" + ), ) ]) @@ -177,7 +342,9 @@ def _render_setting_text(key: str) -> str: lines = [ "🧩 Настройка", + f"Название: {summary['name']}", f"Ключ: {summary['key']}", + f"Категория: {summary['category_label']}", f"Тип: {summary['type']}", f"Текущее значение: {summary['current']}", f"Значение по умолчанию: {summary['original']}", @@ -194,9 +361,9 @@ async def show_bot_config_menu( db_user: User, db: AsyncSession, ): - keyboard = _build_categories_keyboard(db_user.language) + keyboard = _build_groups_keyboard() await callback.message.edit_text( - "🧩 Конфигурация бота\n\nВыберите категорию настроек:", + "🧩 Конфигурация бота\n\nВыберите раздел настроек:", reply_markup=keyboard, ) await callback.answer() @@ -204,20 +371,23 @@ async def show_bot_config_menu( @admin_required @error_handler -async def show_bot_config_categories_page( +async def show_bot_config_group( callback: types.CallbackQuery, db_user: User, db: AsyncSession, ): - parts = callback.data.split(":") - try: - page = int(parts[1]) - except (IndexError, ValueError): - page = 1 + group_key, page = _parse_group_payload(callback.data) + grouped = _get_grouped_categories() + group_lookup = {key: (title, items) for key, title, items in grouped} - keyboard = _build_categories_keyboard(db_user.language, page) + if group_key not in group_lookup: + await callback.answer("Эта группа больше недоступна", show_alert=True) + return + + group_title, items = group_lookup[group_key] + keyboard = _build_categories_keyboard(group_key, group_title, items, page) await callback.message.edit_text( - "🧩 Конфигурация бота\n\nВыберите категорию настроек:", + f"🧩 {group_title}\n\nВыберите категорию настроек:", reply_markup=keyboard, ) await callback.answer() @@ -230,7 +400,9 @@ async def show_bot_config_category( db_user: User, db: AsyncSession, ): - category_key, page = _parse_category_payload(callback.data) + group_key, category_key, category_page, settings_page = _parse_category_payload( + callback.data + ) definitions = bot_configuration_service.get_settings_for_category(category_key) if not definitions: @@ -238,7 +410,13 @@ async def show_bot_config_category( return category_label = definitions[0].category_label - keyboard = _build_settings_keyboard(category_key, db_user.language, page) + keyboard = _build_settings_keyboard( + category_key, + group_key, + category_page, + db_user.language, + settings_page, + ) await callback.message.edit_text( f"🧩 {category_label}\n\nВыберите настройку для просмотра:", reply_markup=keyboard, @@ -253,9 +431,19 @@ async def show_bot_config_setting( db_user: User, db: AsyncSession, ): - key = callback.data.split(":", 1)[1] + parts = callback.data.split(":", 4) + group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY + try: + category_page = max(1, int(parts[2])) if len(parts) > 2 else 1 + except ValueError: + category_page = 1 + try: + settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1 + except ValueError: + settings_page = 1 + key = parts[4] if len(parts) > 4 else "" text = _render_setting_text(key) - keyboard = _build_setting_keyboard(key) + keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page) await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer() @@ -268,7 +456,17 @@ async def start_edit_setting( db: AsyncSession, state: FSMContext, ): - key = callback.data.split(":", 1)[1] + parts = callback.data.split(":", 4) + group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY + try: + category_page = max(1, int(parts[2])) if len(parts) > 2 else 1 + except ValueError: + category_page = 1 + try: + settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1 + except ValueError: + settings_page = 1 + key = parts[4] if len(parts) > 4 else "" definition = bot_configuration_service.get_definition(key) summary = bot_configuration_service.get_setting_summary(key) @@ -276,6 +474,7 @@ async def start_edit_setting( instructions = [ "✏️ Редактирование настройки", + f"Название: {summary['name']}", f"Ключ: {summary['key']}", f"Тип: {summary['type']}", f"Текущее значение: {summary['current']}", @@ -293,14 +492,22 @@ async def start_edit_setting( inline_keyboard=[ [ types.InlineKeyboardButton( - text=texts.BACK, callback_data=f"botcfg_setting:{key}" + text=texts.BACK, + callback_data=( + f"botcfg_setting:{group_key}:{category_page}:{settings_page}:{key}" + ), ) ] ] ), ) - await state.update_data(setting_key=key) + await state.update_data( + setting_key=key, + setting_group_key=group_key, + setting_category_page=category_page, + setting_settings_page=settings_page, + ) await state.set_state(BotConfigStates.waiting_for_value) await callback.answer() @@ -315,6 +522,9 @@ async def handle_edit_setting( ): data = await state.get_data() key = data.get("setting_key") + group_key = data.get("setting_group_key", CATEGORY_FALLBACK_KEY) + category_page = data.get("setting_category_page", 1) + settings_page = data.get("setting_settings_page", 1) if not key: await message.answer("Не удалось определить редактируемую настройку. Попробуйте снова.") @@ -331,7 +541,7 @@ async def handle_edit_setting( await db.commit() text = _render_setting_text(key) - keyboard = _build_setting_keyboard(key) + keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page) await message.answer("✅ Настройка обновлена") await message.answer(text, reply_markup=keyboard) await state.clear() @@ -344,12 +554,22 @@ async def reset_setting( db_user: User, db: AsyncSession, ): - key = callback.data.split(":", 1)[1] + parts = callback.data.split(":", 4) + group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY + try: + category_page = max(1, int(parts[2])) if len(parts) > 2 else 1 + except ValueError: + category_page = 1 + try: + settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1 + except ValueError: + settings_page = 1 + key = parts[4] if len(parts) > 4 else "" await bot_configuration_service.reset_value(db, key) await db.commit() text = _render_setting_text(key) - keyboard = _build_setting_keyboard(key) + keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page) await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer("Сброшено к значению по умолчанию") @@ -361,14 +581,24 @@ async def toggle_setting( db_user: User, db: AsyncSession, ): - key = callback.data.split(":", 1)[1] + parts = callback.data.split(":", 4) + group_key = parts[1] if len(parts) > 1 else CATEGORY_FALLBACK_KEY + try: + category_page = max(1, int(parts[2])) if len(parts) > 2 else 1 + except ValueError: + category_page = 1 + try: + settings_page = max(1, int(parts[3])) if len(parts) > 3 else 1 + except ValueError: + settings_page = 1 + key = parts[4] if len(parts) > 4 else "" current = bot_configuration_service.get_current_value(key) new_value = not bool(current) await bot_configuration_service.set_value(db, key, new_value) await db.commit() text = _render_setting_text(key) - keyboard = _build_setting_keyboard(key) + keyboard = _build_setting_keyboard(key, group_key, category_page, settings_page) await callback.message.edit_text(text, reply_markup=keyboard) await callback.answer("Обновлено") @@ -379,9 +609,8 @@ def register_handlers(dp: Dispatcher) -> None: F.data == "admin_bot_config", ) dp.callback_query.register( - show_bot_config_categories_page, - F.data.startswith("botcfg_categories:") - & (~F.data.endswith(":noop")), + show_bot_config_group, + F.data.startswith("botcfg_group:") & (~F.data.endswith(":noop")), ) dp.callback_query.register( show_bot_config_category,