DispelEvilBot/admin/routes/users.py

184 lines
7.4 KiB
Python
Raw Permalink 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.

"""Раздел «Пользователи»: список с поиском, карточка и управление."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Request, Response
from fastapi.responses import HTMLResponse, RedirectResponse
from admin.security import check_csrf
from common import __version__
from common.eggs import is_egg_event_active
from common.repositories import users_admin_repo
router = APIRouter(prefix="/users", tags=["users"])
#: Сколько пользователей показывать на странице.
PAGE_SIZE = 25
def _editor_id(request: Request) -> int:
"""Telegram-ID текущего администратора (ставит middleware)."""
return int(request.state.admin["telegram_id"])
async def _card_response(
request: Request,
user_id: int,
*,
errors: list[str] | None = None,
saved: str | None = None,
status_code: int = 200,
) -> HTMLResponse:
"""Отрисовать карточку (общая точка для GET и ошибок валидации)."""
user = await users_admin_repo.get_user(user_id)
if user is None:
raise HTTPException(status_code=404, detail="Пользователь не найден")
return request.app.state.templates.TemplateResponse(
request=request,
name="users/card.html",
context={
"version": __version__,
"user": user,
"money": await users_admin_repo.money_history(user_id),
"titles": await users_admin_repo.list_titles(),
"history": await users_admin_repo.user_history(user_id),
"errors": errors or [],
"saved": saved,
"eggs_visible": is_egg_event_active(request.app.state.settings),
"max_nick_length": users_admin_repo.MAX_NICK_LENGTH,
"max_comment_length": users_admin_repo.MAX_COMMENT_LENGTH,
},
status_code=status_code,
)
@router.get("", response_class=HTMLResponse)
async def list_users(request: Request, q: str = "", page: int = 1) -> HTMLResponse:
"""Список пользователей: поиск по нику/username/имени/ID и пагинация."""
total = await users_admin_repo.count_users(q)
pages = max((total + PAGE_SIZE - 1) // PAGE_SIZE, 1)
page = min(max(page, 1), pages)
return request.app.state.templates.TemplateResponse(
request=request,
name="users/list.html",
context={
"version": __version__,
"users": await users_admin_repo.list_users(
q,
limit=PAGE_SIZE,
offset=(page - 1) * PAGE_SIZE,
),
"q": q,
"page": page,
"pages": pages,
"total": total,
},
)
@router.get("/{user_id}", response_class=HTMLResponse)
async def user_card(request: Request, user_id: int, saved: str | None = None) -> HTMLResponse:
"""Карточка пользователя: сводка, управление, история и операции."""
return await _card_response(request, user_id, saved=saved)
@router.post("/{user_id}/nick")
async def update_nick(request: Request, user_id: int) -> Response:
"""Сменить ник игрока."""
form = await request.form()
check_csrf(request, str(form.get("csrf_token", "")))
nick = str(form.get("nick", "")).strip()
errors: list[str] = []
if not nick:
errors.append("Ник не может быть пустым")
elif len(nick) > users_admin_repo.MAX_NICK_LENGTH:
errors.append(f"Ник длиннее {users_admin_repo.MAX_NICK_LENGTH} символов")
elif any(char.isspace() for char in nick):
errors.append("Ник не должен содержать пробелы")
if errors:
return await _card_response(request, user_id, errors=errors, status_code=400)
if not await users_admin_repo.set_nick(user_id, nick, editor_id=_editor_id(request)):
raise HTTPException(status_code=404, detail="Пользователь не найден")
return RedirectResponse(url=f"/users/{user_id}?saved=nick", status_code=303)
@router.post("/{user_id}/title")
async def update_title(request: Request, user_id: int) -> Response:
"""Сменить звание игрока."""
form = await request.form()
check_csrf(request, str(form.get("csrf_token", "")))
try:
title_id = int(str(form.get("title_id", "")))
except (TypeError, ValueError):
return await _card_response(request, user_id, errors=["Выберите звание"], status_code=400)
titles = await users_admin_repo.list_titles()
if not any(int(title["id_title"]) == title_id for title in titles):
return await _card_response(request, user_id, errors=["Звание не найдено"], status_code=400)
if not await users_admin_repo.set_title(user_id, title_id, editor_id=_editor_id(request)):
raise HTTPException(status_code=404, detail="Пользователь не найден")
return RedirectResponse(url=f"/users/{user_id}?saved=title", status_code=303)
@router.post("/{user_id}/subscription")
async def update_subscription(request: Request, user_id: int) -> Response:
"""Подписать или отписать от рассылок."""
form = await request.form()
check_csrf(request, str(form.get("csrf_token", "")))
value = str(form.get("subscribed", ""))
if value not in {"0", "1"}:
return await _card_response(request, user_id, errors=["Некорректное значение подписки"], status_code=400)
if not await users_admin_repo.set_subscription(
user_id,
value == "1",
editor_id=_editor_id(request),
):
raise HTTPException(status_code=404, detail="Пользователь не найден")
return RedirectResponse(url=f"/users/{user_id}?saved=subscription", status_code=303)
@router.post("/{user_id}/balance")
async def update_balance(request: Request, user_id: int) -> Response:
"""Скорректировать баланс записью в журнал money (действие «Корректировка админом»)."""
form = await request.form()
check_csrf(request, str(form.get("csrf_token", "")))
raw_amount = str(form.get("amount", "")).strip()
reason = str(form.get("reason", "")).strip()
errors: list[str] = []
try:
amount = int(raw_amount)
except (TypeError, ValueError):
amount = 0
errors.append("Сумма должна быть целым числом")
if amount == 0 and "Сумма должна быть целым числом" not in errors:
errors.append("Сумма не может быть нулевой")
if not reason:
errors.append("Укажите причину корректировки")
elif len(reason) > users_admin_repo.MAX_COMMENT_LENGTH:
errors.append(f"Причина длиннее {users_admin_repo.MAX_COMMENT_LENGTH} символов")
if errors:
return await _card_response(request, user_id, errors=errors, status_code=400)
balance = await users_admin_repo.adjust_balance(
user_id,
amount,
reason,
editor_id=_editor_id(request),
)
if balance is None:
raise HTTPException(status_code=404, detail="Пользователь не найден")
return RedirectResponse(url=f"/users/{user_id}?saved=balance", status_code=303)