Add paged list of all users to admin panel

This commit is contained in:
Bogdan Strielecki
2025-10-28 15:13:19 +03:00
parent c65b608f0b
commit 207e7751cd
6 changed files with 215 additions and 13 deletions
+15 -1
View File
@@ -98,8 +98,22 @@ async def admin_panel_actions_callback_handler(
await admin_user_mgmnt_handlers.unban_user_prompt_handler( await admin_user_mgmnt_handlers.unban_user_prompt_handler(
callback, state, i18n_data, settings, session) callback, state, i18n_data, settings, session)
elif action == "users_management": elif action == "users_management":
# This is deprecated, kept for compatibility
from . import user_management as admin_user_management_handlers from . import user_management as admin_user_management_handlers
await admin_user_management_handlers.user_management_menu_handler( await admin_user_management_handlers.user_search_prompt_handler(
callback, state, i18n_data, settings, session)
elif action == "users_list" and len(action_parts) > 2:
# Route to users list handler with page number
from . import user_management as admin_user_management_handlers
try:
page = int(action_parts[2])
await admin_user_management_handlers.users_list_handler(
callback, i18n_data, settings, session, page)
except (IndexError, ValueError):
await callback.answer("Invalid page number", show_alert=True)
elif action == "users_search_prompt":
from . import user_management as admin_user_management_handlers
await admin_user_management_handlers.user_search_prompt_handler(
callback, state, i18n_data, settings, session) callback, state, i18n_data, settings, session)
elif action == "view_banned": elif action == "view_banned":
+103 -5
View File
@@ -29,14 +29,57 @@ router = Router(name="admin_user_management_router")
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$") USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
async def user_management_menu_handler(callback: types.CallbackQuery, async def users_list_handler(callback: types.CallbackQuery,
state: FSMContext, i18n_data: dict, i18n_data: dict, settings: Settings,
settings: Settings, session: AsyncSession): session: AsyncSession, page: int = 0):
"""Display user management menu""" """Display paginated list of all users"""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE) current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance") i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message: if not i18n or not callback.message:
await callback.answer("Error preparing user management.", show_alert=True) await callback.answer("Error preparing user list.", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
try:
# Get paginated users
from bot.keyboards.inline.admin_keyboards import get_users_list_keyboard
from db.dal import user_dal
users = await user_dal.get_all_users_paginated(session, page=page, page_size=15)
total_users = await user_dal.count_all_users(session)
total_pages = max(1, (total_users + 14) // 15)
# Format message
header_text = _(
"admin_users_list_header",
default="👥 <b>Список пользователей</b>\n\nСтраница {current}/{total} ({total_users} пользователей)",
current=page + 1,
total=total_pages,
total_users=total_users
)
keyboard = get_users_list_keyboard(users, page, total_users, i18n, current_lang, page_size=15)
await callback.message.edit_text(
header_text,
reply_markup=keyboard,
parse_mode="HTML"
)
await callback.answer()
except Exception as e:
logging.error(f"Error displaying user list: {e}")
await callback.answer("Ошибка отображения списка пользователей", show_alert=True)
async def user_search_prompt_handler(callback: types.CallbackQuery,
state: FSMContext, i18n_data: dict,
settings: Settings, session: AsyncSession):
"""Display search prompt for user management"""
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n or not callback.message:
await callback.answer("Error preparing search.", show_alert=True)
return return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs) _ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
@@ -978,3 +1021,58 @@ async def process_unban_user_handler(message: types.Message, state: FSMContext,
)) ))
await state.clear() await state.clear()
@router.callback_query(F.data.startswith("admin_user_card_from_list:"))
async def user_card_from_list_handler(callback: types.CallbackQuery,
state: FSMContext, i18n_data: dict,
settings: Settings, bot: Bot,
subscription_service: SubscriptionService,
panel_service: PanelApiService,
session: AsyncSession):
"""Display user card when clicked from user list"""
try:
parts = callback.data.split(":")
user_id = int(parts[1])
page = int(parts[2])
except (IndexError, ValueError):
await callback.answer("Invalid user data", show_alert=True)
return
current_lang = i18n_data.get("current_language", settings.DEFAULT_LANGUAGE)
i18n: Optional[JsonI18n] = i18n_data.get("i18n_instance")
if not i18n:
await callback.answer("Language service error", show_alert=True)
return
_ = lambda key, **kwargs: i18n.gettext(current_lang, key, **kwargs)
# Get user from database
user = await user_dal.get_user_by_id(session, user_id)
if not user:
await callback.answer("User not found", show_alert=True)
return
# Create keyboard with back to list button
keyboard = get_user_card_keyboard(user_id, i18n, current_lang)
keyboard.button(
text=_("admin_user_back_to_list_button", default="⬅️ К списку"),
callback_data=f"admin_action:users_list:{page}"
)
keyboard.adjust(2, 2, 2, 2, 1)
# Format user card
try:
from bot.services.referral_service import ReferralService
referral_service = ReferralService(settings, subscription_service, bot, i18n)
user_card_text = await format_user_card(user, session, subscription_service, i18n, current_lang, referral_service)
await callback.message.edit_text(
user_card_text,
reply_markup=keyboard.as_markup(),
parse_mode="HTML"
)
await callback.answer()
except Exception as e:
logging.error(f"Error displaying user card: {e}")
await callback.answer("Error displaying user card", show_alert=True)
+66 -2
View File
@@ -59,13 +59,15 @@ def get_user_management_keyboard(i18n_instance, lang: str) -> InlineKeyboardMark
builder = InlineKeyboardBuilder() builder = InlineKeyboardBuilder()
builder.button(text=_(key="admin_users_management_button"), builder.button(text=_(key="admin_users_management_button"),
callback_data="admin_action:users_management") callback_data="admin_action:users_list:0")
builder.button(text=_(key="admin_users_search_button"),
callback_data="admin_action:users_search_prompt")
builder.button(text=_(key="admin_ban_management_section"), builder.button(text=_(key="admin_ban_management_section"),
callback_data="admin_section:ban_management") callback_data="admin_section:ban_management")
builder.button(text=_(key="back_to_admin_panel_button"), builder.button(text=_(key="back_to_admin_panel_button"),
callback_data="admin_action:main") callback_data="admin_action:main")
builder.adjust(2, 1) builder.adjust(2, 1, 1)
return builder.as_markup() return builder.as_markup()
@@ -305,6 +307,68 @@ def get_banned_users_keyboard(banned_users: List[User], current_page: int,
return builder.as_markup() return builder.as_markup()
def get_users_list_keyboard(users: List[User], current_page: int,
total_users: int, i18n_instance, lang: str,
page_size: int = 15) -> InlineKeyboardMarkup:
"""Generate keyboard for paginated user list"""
_ = lambda key, **kwargs: i18n_instance.gettext(lang, key, **kwargs)
builder = InlineKeyboardBuilder()
# Add user buttons
for user in users:
user_display_parts = []
if user.username:
user_display_parts.append(f"@{user.username}")
user_display_parts.append(f"ID: {user.user_id}")
if user.first_name:
user_display_parts.append(f"- {user.first_name}")
button_text = " ".join(user_display_parts)
builder.row(
InlineKeyboardButton(
text=button_text,
callback_data=f"admin_user_card_from_list:{user.user_id}:{current_page}"
)
)
# Pagination buttons
if total_users > page_size:
total_pages = math.ceil(total_users / page_size)
pagination_buttons = []
if current_page > 0:
pagination_buttons.append(
InlineKeyboardButton(
text=_("prev_page_button"),
callback_data=f"admin_action:users_list:{current_page - 1}"
)
)
pagination_buttons.append(
InlineKeyboardButton(
text=f"{current_page + 1}/{total_pages}",
callback_data="stub_page_display"
)
)
if current_page < total_pages - 1:
pagination_buttons.append(
InlineKeyboardButton(
text=_("next_page_button"),
callback_data=f"admin_action:users_list:{current_page + 1}"
)
)
if pagination_buttons:
builder.row(*pagination_buttons)
# Back button
builder.row(
InlineKeyboardButton(
text=_("back_to_user_management_button"),
callback_data="admin_section:user_management"
)
)
return builder.as_markup()
def get_user_card_keyboard(user_id: int, def get_user_card_keyboard(user_id: int,
is_banned: bool, is_banned: bool,
i18n_instance, i18n_instance,
+20
View File
@@ -227,3 +227,23 @@ async def get_user_ids_without_active_subscription(session: AsyncSession) -> Lis
) )
result = await session.execute(stmt) result = await session.execute(stmt)
return result.scalars().all() return result.scalars().all()
async def get_all_users_paginated(session: AsyncSession, page: int = 0, page_size: int = 15) -> List[User]:
"""Get all users with pagination, ordered by registration date descending."""
offset = page * page_size
stmt = (
select(User)
.order_by(User.registration_date.desc())
.offset(offset)
.limit(page_size)
)
result = await session.execute(stmt)
return result.scalars().all()
async def count_all_users(session: AsyncSession) -> int:
"""Count total number of users in the database."""
stmt = select(func.count(User.user_id))
result = await session.execute(stmt)
return result.scalar() or 0
+4 -1
View File
@@ -101,7 +101,10 @@
"admin_promo_marketing_section": "🎁 Promos", "admin_promo_marketing_section": "🎁 Promos",
"admin_system_functions_section": "⚙️ System", "admin_system_functions_section": "⚙️ System",
"admin_ban_management_section": "🚫 Bans", "admin_ban_management_section": "🚫 Bans",
"admin_users_management_button": "👤 Management", "admin_users_search_button": "🔍 Search",
"admin_users_management_button": "👥 User List",
"admin_users_list_header": "👥 <b>User List</b>\n\nPage {current}/{total} ({total_users} users)",
"admin_user_back_to_list_button": "⬅️ Back to List",
"back_to_user_management_button": "⬅️ To Users", "back_to_user_management_button": "⬅️ To Users",
"back_to_admin_panel_button": "⬅️ To Admin", "back_to_admin_panel_button": "⬅️ To Admin",
"admin_stats_header": "📊 Bot Statistics", "admin_stats_header": "📊 Bot Statistics",
+4 -1
View File
@@ -101,7 +101,10 @@
"admin_promo_marketing_section": "🎁 Промокоды", "admin_promo_marketing_section": "🎁 Промокоды",
"admin_system_functions_section": "⚙️ Система", "admin_system_functions_section": "⚙️ Система",
"admin_ban_management_section": "🚫 Блокировки", "admin_ban_management_section": "🚫 Блокировки",
"admin_users_management_button": "👤 Управление", "admin_users_search_button": "🔍 Поиск",
"admin_users_management_button": "👥 Список пользователей",
"admin_users_list_header": "👥 <b>Список пользователей</b>\n\nСтраница {current}/{total} ({total_users} пользователей)",
"admin_user_back_to_list_button": "⬅️ К списку",
"back_to_user_management_button": "⬅️ К пользователям", "back_to_user_management_button": "⬅️ К пользователям",
"back_to_admin_panel_button": "⬅️ В админку", "back_to_admin_panel_button": "⬅️ В админку",
"admin_stats_header": "📊 Статистика Бота", "admin_stats_header": "📊 Статистика Бота",