From c4721fcf99e1e07e027804410c63970264819b70 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Mon, 27 Jul 2026 21:04:26 +0300 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=D0=BF=D0=BE=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=D0=BF=D0=BE=20=D0=BD=D0=B0=D0=B7=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AdminUserRepository.list_paginated ищет теперь и по названию команды (LEFT JOIN teams, как и раньше) — пользователи без команды по-прежнему не пропадают из общей выдачи, просто не совпадают с этой частью поиска. Плейсхолдер поля поиска в админке обновлён под новое поведение. --- backend/api/admin.py | 2 +- backend/repositories/admin.py | 16 ++++++---- backend/tests/test_admin_api.py | 29 +++++++++++++++++++ .../src/components/admin/AdminUsersTab.tsx | 2 +- 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/backend/api/admin.py b/backend/api/admin.py index d03d584..d53ce60 100644 --- a/backend/api/admin.py +++ b/backend/api/admin.py @@ -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 ) diff --git a/backend/repositories/admin.py b/backend/repositories/admin.py index 6ee3819..e7376be 100644 --- a/backend/repositories/admin.py +++ b/backend/repositories/admin.py @@ -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) diff --git a/backend/tests/test_admin_api.py b/backend/tests/test_admin_api.py index 0b2370c..7747c2d 100644 --- a/backend/tests/test_admin_api.py +++ b/backend/tests/test_admin_api.py @@ -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: diff --git a/frontend/src/components/admin/AdminUsersTab.tsx b/frontend/src/components/admin/AdminUsersTab.tsx index 33a78a1..cd05e54 100644 --- a/frontend/src/components/admin/AdminUsersTab.tsx +++ b/frontend/src/components/admin/AdminUsersTab.tsx @@ -124,7 +124,7 @@ export function AdminUsersTab() {