Add paged list of all users to admin panel
This commit is contained in:
@@ -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":
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -911,9 +954,9 @@ async def process_ban_user_handler(message: types.Message, state: FSMContext,
|
|||||||
|
|
||||||
@router.message(AdminStates.waiting_for_user_id_to_unban, F.text)
|
@router.message(AdminStates.waiting_for_user_id_to_unban, F.text)
|
||||||
async def process_unban_user_handler(message: types.Message, state: FSMContext,
|
async def process_unban_user_handler(message: types.Message, state: FSMContext,
|
||||||
settings: Settings, i18n_data: dict,
|
settings: Settings, i18n_data: dict,
|
||||||
panel_service: PanelApiService,
|
panel_service: PanelApiService,
|
||||||
session: AsyncSession):
|
session: AsyncSession):
|
||||||
"""Process user unban input"""
|
"""Process user unban input"""
|
||||||
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")
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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": "📊 Статистика Бота",
|
||||||
|
|||||||
Reference in New Issue
Block a user