API: поиск игроков по нику для автокомплита (GET /api/users/search)
До 10 подсказок по подстроке ника среди активных игроков (role=player). Регистронезависимо и для кириллицы: SQLite LIKE/lower сворачивают регистр только у ASCII, поэтому фильтрация/ранжирование в Python (casefold): префиксные совпадения раньше подстрочных, внутри — по алфавиту. Пустой q — пустой список. Тесты + регенерированный schema.d.ts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -173,3 +173,22 @@ def nickname_available(
|
||||
) -> s.NicknameAvailable:
|
||||
available = user_service.nickname_available(session, value.strip(), exclude_user_id=user.id)
|
||||
return s.NicknameAvailable(available=available)
|
||||
|
||||
|
||||
@router.get("/search", response_model=list[s.UserSuggestion])
|
||||
def search_users(
|
||||
q: str = Query("", max_length=64),
|
||||
limit: int = Query(3, ge=1, le=10),
|
||||
session: Session = Depends(get_session),
|
||||
_user: User = Depends(get_current_user),
|
||||
) -> list[s.UserSuggestion]:
|
||||
"""Автокомплит по нику. Пустой q — пустой список (не ошибка): удобнее при наборе."""
|
||||
users = user_service.search_users(session, q, limit=limit)
|
||||
return [
|
||||
s.UserSuggestion(
|
||||
user_id=u.id, # type: ignore[arg-type]
|
||||
nickname=u.nickname,
|
||||
avatar_url=user_service.avatar_url_for(u.id, u.avatar_path, u.updated_at), # type: ignore[arg-type]
|
||||
)
|
||||
for u in users
|
||||
]
|
||||
|
||||
@@ -112,6 +112,14 @@ class NicknameAvailable(BaseModel):
|
||||
available: bool
|
||||
|
||||
|
||||
class UserSuggestion(BaseModel):
|
||||
"""Подсказка автокомплита по нику (приглашение в группу и т.п.)."""
|
||||
|
||||
user_id: int
|
||||
nickname: str
|
||||
avatar_url: str | None = None
|
||||
|
||||
|
||||
# ─── Группы и членство ───────────────────────────────────────────────────────
|
||||
|
||||
class GroupCreate(BaseModel):
|
||||
|
||||
@@ -34,6 +34,28 @@ def nickname_available(session: Session, nickname: str, exclude_user_id: int | N
|
||||
return existing is None or existing.id == exclude_user_id
|
||||
|
||||
|
||||
def search_users(session: Session, query: str, limit: int = 3) -> list[User]:
|
||||
"""Подсказки по нику: активные игроки (role='player'), без учёта регистра.
|
||||
|
||||
SQLite LIKE/lower() сворачивают регистр только для ASCII, а ники бывают
|
||||
кириллицей — поэтому выбираем активных игроков и фильтруем/ранжируем в
|
||||
Python (casefold). Пользователей десятки, полный проход дёшев; при росте
|
||||
до тысяч — заменить на индексированную колонку nickname_lower.
|
||||
Сначала префиксные совпадения, затем подстрочные, внутри — по алфавиту.
|
||||
"""
|
||||
q = (query or "").strip().casefold()
|
||||
if not q:
|
||||
return []
|
||||
users = session.exec(
|
||||
select(User).where(User.role == "player", User.is_active == True) # noqa: E712
|
||||
).all()
|
||||
matched = [u for u in users if q in u.nickname.casefold()]
|
||||
matched.sort(
|
||||
key=lambda u: (0 if u.nickname.casefold().startswith(q) else 1, u.nickname.casefold())
|
||||
)
|
||||
return matched[:limit]
|
||||
|
||||
|
||||
def nickname_format_ok(nickname: str) -> bool:
|
||||
"""Ник подходит по формату (2–64, буквы/цифры/пробел/.-_)."""
|
||||
return bool(_NICK_RE.match((nickname or "").strip()))
|
||||
|
||||
Reference in New Issue
Block a user