feat(admin): поиск пользователей по названию команды
AdminUserRepository.list_paginated ищет теперь и по названию команды (LEFT JOIN teams, как и раньше) — пользователи без команды по-прежнему не пропадают из общей выдачи, просто не совпадают с этой частью поиска. Плейсхолдер поля поиска в админке обновлён под новое поведение.
This commit is contained in:
@@ -180,7 +180,7 @@ async def list_users(
|
||||
offset: Annotated[int, Query(ge=0)] = 0,
|
||||
) -> AdminUserListOut:
|
||||
"""Список пользователей инстанса — фильтр по статусу (`active`/`blocked`, без
|
||||
параметра — все) и текстовый поиск по email/имени."""
|
||||
параметра — все) и текстовый поиск по email/имени/названию команды."""
|
||||
rows, total = await AdminUserRepository(session).list_paginated(
|
||||
status=status, q=q, limit=limit, offset=offset
|
||||
)
|
||||
|
||||
@@ -67,9 +67,11 @@ class AdminUserRepository:
|
||||
"""Вернуть страницу пользователей (+ имя команды) и общее число совпадений.
|
||||
|
||||
`status` — `"active"`/`"blocked"` (фильтр по `is_blocked`), `None` —
|
||||
без фильтра (все пользователи). `LEFT JOIN` на `teams` — имя команды
|
||||
нужно карточке профиля/таблице админки, у пользователя без команды —
|
||||
`None`.
|
||||
без фильтра (все пользователи). `q` ищет по email/имени пользователя
|
||||
И по названию команды (`LEFT JOIN` на `teams` — нужен и для этого
|
||||
поиска, и для колонки «Команда» в таблице админки; `LEFT`, а не
|
||||
`INNER`, — пользователи без команды не должны пропадать из общей
|
||||
выдачи, только не совпадать с поиском по названию команды).
|
||||
"""
|
||||
filters: list[ColumnElement[bool]] = []
|
||||
if status == "active":
|
||||
@@ -78,9 +80,13 @@ class AdminUserRepository:
|
||||
filters.append(User.is_blocked.is_(True))
|
||||
if q:
|
||||
like = f"%{q}%"
|
||||
filters.append(or_(User.email.ilike(like), User.name_user.ilike(like)))
|
||||
filters.append(
|
||||
or_(User.email.ilike(like), User.name_user.ilike(like), Team.name.ilike(like))
|
||||
)
|
||||
|
||||
count_stmt = select(func.count()).select_from(User)
|
||||
count_stmt = (
|
||||
select(func.count()).select_from(User).outerjoin(Team, User.team_id == Team.id)
|
||||
)
|
||||
items_stmt = (
|
||||
select(User, Team.name)
|
||||
.outerjoin(Team, User.team_id == Team.id)
|
||||
|
||||
@@ -413,6 +413,35 @@ async def test_list_users_filters_by_status(
|
||||
assert str(blocked_user.id) in all_ids
|
||||
|
||||
|
||||
async def test_list_users_search_matches_team_name(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession
|
||||
) -> None:
|
||||
"""`q` находит пользователей по (части) названия их команды; пользователи
|
||||
без команды при этом не пропадают из общей (без поиска) выдачи."""
|
||||
admin = await _make_user(db_session, role="admin")
|
||||
suffix = uuid.uuid4().hex[:8]
|
||||
team = Team(name=f"Rocket-{suffix} Squad")
|
||||
db_session.add(team)
|
||||
await db_session.flush()
|
||||
teamed_user = await _make_user(db_session)
|
||||
teamed_user.team_id = team.id
|
||||
teamless_user = await _make_user(db_session)
|
||||
await db_session.commit()
|
||||
|
||||
response = await client.get(
|
||||
"/api/v1/admin/users", params={"q": f"Rocket-{suffix}"}, headers=_auth_headers(admin)
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
ids = [item["id"] for item in response.json()["items"]]
|
||||
assert str(teamed_user.id) in ids
|
||||
assert str(teamless_user.id) not in ids
|
||||
assert str(admin.id) not in ids
|
||||
|
||||
all_response = await client.get("/api/v1/admin/users", headers=_auth_headers(admin))
|
||||
all_ids = [item["id"] for item in all_response.json()["items"]]
|
||||
assert str(teamless_user.id) in all_ids
|
||||
|
||||
|
||||
async def test_patch_user_role_and_block(
|
||||
client: httpx.AsyncClient, db_session: AsyncSession
|
||||
) -> None:
|
||||
|
||||
@@ -124,7 +124,7 @@ export function AdminUsersTab() {
|
||||
<Search className="icon" style={{ width: 16, height: 16 }} aria-hidden="true" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Поиск по имени или email…"
|
||||
placeholder="Поиск по имени, email или команде…"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user