diff --git a/tests/test_routes/test_user_get.py b/tests/test_routes/test_user_get.py index a20ad96..bc8c25e 100644 --- a/tests/test_routes/test_user_get.py +++ b/tests/test_routes/test_user_get.py @@ -16,6 +16,8 @@ def test_get(client, dbsession, source, info_no_scopes): assert info1.category.name == response.json()["items"][0]["category"] assert info1.param.name == response.json()["items"][0]["param"] assert info1.value == response.json()["items"][0]["value"] + dbsession.delete(info1) + dbsession.commit() @pytest.mark.authenticated(user_id=1) @@ -26,6 +28,7 @@ def test_get_no_all_scopes(client, dbsession, source, info_no_scopes): response = client.get(f"/user/{info1.owner_id}") assert response.status_code == 200 assert info1.category.name not in response.json() + dbsession.delete(info1) dbsession.commit() @@ -79,6 +82,10 @@ def test_get_a_few(client, dbsession, category_no_scopes, source): dbsession.delete(param2) dbsession.delete(param3) dbsession.delete(param4) + dbsession.flush() + dbsession.delete(category1) + dbsession.delete(category2) + dbsession.delete(category3) dbsession.commit() @@ -147,6 +154,10 @@ def test_get_a_few_with_trust_level(client, dbsession, category_no_scopes, sourc dbsession.delete(param2) dbsession.delete(param3) dbsession.delete(param4) + dbsession.flush() + dbsession.delete(category1) + dbsession.delete(category2) + dbsession.delete(category3) dbsession.commit() @@ -181,4 +192,6 @@ def test_get_last_most_trusted(client, dbsession, category_no_scopes, source): dbsession.delete(info4) dbsession.flush() dbsession.delete(param1) + dbsession.flush() + dbsession.delete(category1) dbsession.commit() diff --git a/tests/test_routes/test_users_get.py b/tests/test_routes/test_users_get.py new file mode 100644 index 0000000..65d1753 --- /dev/null +++ b/tests/test_routes/test_users_get.py @@ -0,0 +1,156 @@ +from time import sleep + +import pytest + +from userdata_api.models.db import Info, Param +from userdata_api.utils.utils import random_string + + +@pytest.mark.authenticated("userdata.info.admin") +def test_get(client, dbsession, category_no_scopes, source): + source = source() + category1 = category_no_scopes() + category2 = category_no_scopes() + category3 = category_no_scopes() + param1 = Param( + name=f"test{random_string()}", category_id=category1.id, type="last", changeable=True, is_required=True + ) + param2 = Param( + name=f"test{random_string()}", category_id=category1.id, type="last", changeable=True, is_required=True + ) + param3 = Param( + name=f"test{random_string()}", category_id=category2.id, type="last", changeable=True, is_required=True + ) + param4 = Param( + name=f"test{random_string()}", category_id=category3.id, type="last", changeable=True, is_required=True + ) + dbsession.add_all([param1, param2, param3, param4]) + dbsession.flush() + info1 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param1.id, owner_id=0) + info2 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param2.id, owner_id=1) + info3 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param3.id, owner_id=0) + info4 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param4.id, owner_id=1) + dbsession.add_all([info1, info2, info3, info4]) + dbsession.commit() + response = client.get(f"/user", params={"users": [0, 1], "categories": [category1.id, category2.id, category3.id]}) + assert response.status_code == 200 + assert {"user_id": 1, "category": category1.name, "param": info2.param.name, "value": info2.value} in list( + response.json()["items"] + ) + assert {"user_id": 1, "category": category3.name, "param": info4.param.name, "value": info4.value} in list( + response.json()["items"] + ) + assert {"user_id": 0, "category": category1.name, "param": info1.param.name, "value": info1.value} in list( + response.json()["items"] + ) + assert {"user_id": 0, "category": category2.name, "param": info3.param.name, "value": info3.value} in list( + response.json()["items"] + ) + dbsession.delete(info1) + dbsession.delete(info2) + dbsession.delete(info3) + dbsession.delete(info4) + dbsession.flush() + dbsession.delete(param1) + dbsession.delete(param2) + dbsession.delete(param3) + dbsession.delete(param4) + dbsession.flush() + dbsession.delete(category1) + dbsession.delete(category2) + dbsession.delete(category3) + dbsession.commit() + + +@pytest.mark.authenticated("userdata.info.admin") +def test_get_some_users(client, dbsession, category_no_scopes, source): + source = source() + category1 = category_no_scopes() + param1 = Param( + name=f"test{random_string()}", category_id=category1.id, type="last", changeable=True, is_required=True + ) + dbsession.add_all([param1]) + dbsession.flush() + info1 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param1.id, owner_id=1) + info2 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param1.id, owner_id=2) + info3 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param1.id, owner_id=3) + dbsession.add_all([info1, info2, info3]) + dbsession.commit() + response = client.get(f"/user", params={"users": [1, 2], "categories": [category1.id]}) + assert response.status_code == 200 + assert {"user_id": 1, "category": category1.name, "param": param1.name, "value": info1.value} in list( + response.json()["items"] + ) + assert {"user_id": 2, "category": category1.name, "param": param1.name, "value": info2.value} in list( + response.json()["items"] + ) + assert {"user_id": 3, "category": category1.name, "param": param1.name, "value": info3.value} not in list( + response.json()["items"] + ) + response = client.get(f"/user", params={"users": [3], "categories": [category1.id]}) + assert response.status_code == 200 + assert len(response.json()["items"]) == 1 + assert {"user_id": 3, "category": category1.name, "param": param1.name, "value": info3.value} in list( + response.json()["items"] + ) + dbsession.delete(info1) + dbsession.delete(info2) + dbsession.delete(info3) + dbsession.flush() + dbsession.delete(param1) + dbsession.flush() + dbsession.delete(category1) + dbsession.commit() + + +@pytest.mark.authenticated("userdata.info.admin") +def test_get_some_categories(client, dbsession, category_no_scopes, source): + source = source() + category1 = category_no_scopes() + category2 = category_no_scopes() + category3 = category_no_scopes() + param1 = Param( + name=f"test{random_string()}", category_id=category1.id, type="last", changeable=True, is_required=True + ) + param2 = Param( + name=f"test{random_string()}", category_id=category2.id, type="last", changeable=True, is_required=True + ) + param3 = Param( + name=f"test{random_string()}", category_id=category3.id, type="last", changeable=True, is_required=True + ) + dbsession.add_all([param1, param2, param3]) + dbsession.flush() + info1 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param1.id, owner_id=1) + info2 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param2.id, owner_id=1) + info3 = Info(value=f"test{random_string()}", source_id=source.id, param_id=param3.id, owner_id=1) + dbsession.add_all([info1, info2, info3]) + dbsession.commit() + response = client.get(f"/user", params={"users": [1], "categories": [category1.id, category2.id]}) + assert response.status_code == 200 + assert {"user_id": 1, "category": category1.name, "param": info1.param.name, "value": info1.value} in list( + response.json()["items"] + ) + assert {"user_id": 1, "category": category2.name, "param": info2.param.name, "value": info2.value} in list( + response.json()["items"] + ) + assert {"user_id": 1, "category": category3.name, "param": info3.param.name, "value": info3.value} not in list( + response.json()["items"] + ) + + response = client.get(f"/user", params={"users": [1], "categories": [category3.id]}) + assert {"user_id": 1, "category": category3.name, "param": info3.param.name, "value": info3.value} in list( + response.json()["items"] + ) + + dbsession.delete(info1) + dbsession.delete(info2) + dbsession.delete(info3) + dbsession.flush() + dbsession.delete(param1) + dbsession.delete(param2) + dbsession.delete(param3) + dbsession.flush() + dbsession.delete(category1) + dbsession.delete(category2) + dbsession.delete(category3) + dbsession.commit() diff --git a/userdata_api/routes/user.py b/userdata_api/routes/user.py index 491f368..2fdec1b 100644 --- a/userdata_api/routes/user.py +++ b/userdata_api/routes/user.py @@ -1,11 +1,14 @@ from typing import Any from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Query +from fastapi_sqlalchemy import db +from userdata_api.models.db import Category, Info from userdata_api.schemas.response_model import StatusResponseModel -from userdata_api.schemas.user import UserInfoGet, UserInfoUpdate +from userdata_api.schemas.user import UserInfoGet, UserInfoUpdate, UsersInfoGet from userdata_api.utils.user import get_user_info as get +from userdata_api.utils.user import get_users_info_batch as get_users from userdata_api.utils.user import patch_user_info as patch @@ -14,7 +17,8 @@ @user.get("/{id}", response_model=UserInfoGet) async def get_user_info( - id: int, user: dict[str, Any] = Depends(UnionAuth(scopes=[], allow_none=False, auto_error=True)) + id: int, + user: dict[str, Any] = Depends(UnionAuth(scopes=[], allow_none=False, auto_error=True)), ) -> UserInfoGet: """ Получить информацию о пользователе @@ -63,4 +67,19 @@ async def update_user( :return: """ await patch(new_info, id, user) - return StatusResponseModel(status='Success', message='User patch succeeded', ru="Изменение успешно") + return StatusResponseModel(status="Success", message="User patch succeeded", ru="Изменение успешно") + + +@user.get("", response_model=UsersInfoGet, response_model_exclude_unset=True) +async def get_users_info( + users: list[int] = Query(), + categories: list[int] = Query(), + user: dict[str, Any] = Depends(UnionAuth(scopes=["userdata.info.admin"], allow_none=False, auto_error=True)), +) -> UsersInfoGet: + """ + Получить информацию о пользователях. + :param users: список id юзеров, про которых нужно вернуть информацию + :param categories: список id категорий, параметры которых нужно вернуть + :return: список данных о пользователях и данных категориях + """ + return UsersInfoGet.model_validate(await get_users(users, categories, user)) diff --git a/userdata_api/schemas/user.py b/userdata_api/schemas/user.py index 0b16cc3..b48d27f 100644 --- a/userdata_api/schemas/user.py +++ b/userdata_api/schemas/user.py @@ -9,6 +9,10 @@ class UserInfo(Base): value: str | None = None +class ExtendedUserInfo(UserInfo): + user_id: int + + class UserInfoGet(Base): items: list[UserInfo] @@ -24,5 +28,9 @@ def unique_validator(cls, v): return v +class UsersInfoGet(Base): + items: list[ExtendedUserInfo] + + class UserInfoUpdate(UserInfoGet): source: constr(min_length=1) diff --git a/userdata_api/utils/user.py b/userdata_api/utils/user.py index 490eaf5..f1e99ca 100644 --- a/userdata_api/utils/user.py +++ b/userdata_api/utils/user.py @@ -5,7 +5,7 @@ from userdata_api.exceptions import Forbidden, ObjectNotFound from userdata_api.models.db import Category, Info, Param, Source, ViewType -from userdata_api.schemas.user import UserInfoGet, UserInfoUpdate +from userdata_api.schemas.user import UserInfoGet, UserInfoUpdate, UsersInfoGet async def patch_user_info(new: UserInfoUpdate, user_id: int, user: dict[str, int | list[dict[str, str | int]]]) -> None: @@ -68,7 +68,10 @@ async def patch_user_info(new: UserInfoUpdate, user_id: int, user: dict[str, int db.session.query(Info) .join(Source) .filter( - Info.param_id == param.id, Info.owner_id == user_id, Source.name == new.source, not_(Info.is_deleted) + Info.param_id == param.id, + Info.owner_id == user_id, + Source.name == new.source, + not_(Info.is_deleted), ) .one_or_none() ) @@ -96,52 +99,66 @@ async def patch_user_info(new: UserInfoUpdate, user_id: int, user: dict[str, int info.value = item.value db.session.flush() continue + if item.value is None: info.is_deleted = True db.session.flush() continue -async def get_user_info(user_id: int, user: dict[str, int | list[dict[str, str | int]]]) -> UserInfoGet: - """ - Возвращает информауию о пользователе в соотетствии с переданным токеном. - - Пользователь может прочитать любую информацию о себе +async def get_users_info( + user_ids: list[int], category_ids: list[int] | None, user: dict[str, int | list[dict[str, str | int]]] +) -> list[dict[str, str | None]]: + """. + Возвращает информацию о данных пользователей в указанных категориях - Токен с доступом к read_scope категории может получить доступ к данным категории у любых пользователей - - :param user_id: Айди пользователя + :param user_ids: Список айди юзеров + :param category_ids: Список айди необходимых категорий, если None, то мы запрашиваем информацию только обо одном пользователе user_ids[0] обо всех досутпных категориях :param user: Сессия выполняющего запрос данных - :return: Список словарей содержащих категорию, параметр категории и значение этого параметра у польщователя + :return: Список словарей содержащих id пользователя, категорию, параметр категории и значение этого параметра у пользователя """ - infos: list[Info] = ( + is_single_user = category_ids is None + scope_names = [scope["name"] for scope in user["session_scopes"]] + param_dict: dict[Param, dict[int, list[Info] | Info | None] | None] = {} + query: list[Info] = ( Info.query(session=db.session) .join(Param) .join(Category) - .filter(Info.owner_id == user_id, not_(Param.is_deleted), not_(Category.is_deleted)) - .all() + .filter( + Info.owner_id.in_(user_ids), + not_(Param.is_deleted), + not_(Category.is_deleted), + not_(Info.is_deleted), + ) ) + if not is_single_user: + query = query.filter(Param.category_id.in_(category_ids)) + infos = query.all() if not infos: - raise ObjectNotFound(Info, user_id) - scope_names = [scope["name"] for scope in user["session_scopes"]] - param_dict: dict[Param, list[Info] | Info | None] = {} + raise ObjectNotFound(Info, user_ids) + result = [] for info in infos: - ## Проверка доступов - нужен либо скоуп на категориию либо нужно быть овнером информации - if info.category.read_scope and info.category.read_scope not in scope_names and user["id"] != user_id: + if ( + info.category.read_scope + and info.category.read_scope not in scope_names + and (not is_single_user or info.owner_id != user["id"]) + ): continue - if info.param not in param_dict.keys(): - param_dict[info.param] = [] if info.param.pytype == list[str] else None + if info.param not in param_dict: + param_dict[info.param] = {} + if info.owner_id not in param_dict[info.param]: + param_dict[info.param][info.owner_id] = [] if info.param.type == ViewType.ALL else None if info.param.type == ViewType.ALL: - param_dict[info.param].append(info) - elif (param_dict[info.param] is None) or ( - (info.param.type == ViewType.LAST and info.create_ts > param_dict[info.param].create_ts) + param_dict[info.param][info.owner_id].append(info) + elif param_dict[info.param][info.owner_id] is None or ( + (info.param.type == ViewType.LAST and info.create_ts > param_dict[info.param][info.owner_id].create_ts) or ( info.param.type == ViewType.MOST_TRUSTED and ( - param_dict[info.param].source.trust_level < info.source.trust_level + param_dict[info.param][info.owner_id].source.trust_level < info.source.trust_level or ( - param_dict[info.param].source.trust_level <= info.source.trust_level - and info.create_ts > param_dict[info.param].create_ts + param_dict[info.param][info.owner_id].source.trust_level <= info.source.trust_level + and info.create_ts > param_dict[info.param][info.owner_id].create_ts ) ) ) @@ -156,13 +173,60 @@ async def get_user_info(user_id: int, user: dict[str, int | list[dict[str, str | Если у параметра отображение по времени то более релевантная - более позднаяя """ - param_dict[info.param] = info + param_dict[info.param][info.owner_id] = info result = [] - for item in param_dict.values(): - if isinstance(item, list): - result.extend( - [{"category": _item.category.name, "param": _item.param.name, "value": _item.value} for _item in item] - ) - else: - result.append({"category": item.category.name, "param": item.param.name, "value": item.value}) + for param, user_dict in param_dict.items(): + for owner_id, item in user_dict.items(): + if isinstance(item, list): + result.extend( + [ + { + "user_id": owner_id, + "category": _item.category.name, + "param": param.name, + "value": _item.value, + } + for _item in item + ] + ) + else: + result.append( + { + "user_id": owner_id, + "category": item.category.name, + "param": param.name, + "value": item.value, + } + ) + return result + + +async def get_users_info_batch( + user_ids: list[int], category_ids: list[int], user: dict[str, int | list[dict[str, str | int]]] +) -> UsersInfoGet: + """. + Возвращает информацию о данных пользователей в указанных категориях + + :param user_ids: Список айди юзеров + :param category_ids: Список айди необходимых категорий + :param user: Сессия выполняющего запрос данных + :return: Список словарей содержащих id пользователя, категорию, параметр категории и значение этого параметра у пользователя + """ + return UsersInfoGet(items=await get_users_info(user_ids, category_ids, user)) + + +async def get_user_info(user_id: int, user: dict[str, int | list[dict[str, str | int]]]) -> UserInfoGet: + """Возвращает информауию о пользователе в соотетствии с переданным токеном. + + Пользователь может прочитать любую информацию о себе + + Токен с доступом к read_scope категории может получить доступ к данным категории у любых пользователей + + :param user_id: Айди пользователя + :param user: Сессия выполняющего запрос данных + :return: Список словарей содержащих категорию, параметр категории и значение этого параметра у пользователя + """ + result = await get_users_info([user_id], None, user) + for value in result: + del value["user_id"] return UserInfoGet(items=result)