-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuser.py
More file actions
257 lines (230 loc) · 12 KB
/
user.py
File metadata and controls
257 lines (230 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
from __future__ import annotations
from re import search
from fastapi_sqlalchemy import db
from sqlalchemy import not_, or_
from userdata_api.exceptions import Forbidden, InvalidValidation, ObjectNotFound
from userdata_api.models.db import Category, Info, Param, Source, ViewType
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:
"""
Обновить информацию о пользователе в соотетствии с переданным токеном.
Метод обновляет только информацию из источников `admin`, `user` или `dwh`.
Для обновления от имени админа нужен скоуп `userdata.info.admin`
Для обновления информации из dwh нужен скоуп `userdata.info.dwh`
Для обновления от иимени пользователя необходима владениие ининформацией
Обноввляет только инормацую созданную самим источником
Для удаления информации передать None в соответствущем словаре из списка new.items
:param new: модель запроса, в ней то на что будет изменена информация о пользователе
:param user_id: Айди пользователя
:param user: Сессия пользователя выполняющего запрос
:return: get_user_info для текущего пользователя с переданными правами
"""
scope_names = tuple(scope["name"] for scope in user["session_scopes"])
if new.source == "admin" and "userdata.info.admin" not in scope_names:
raise Forbidden(
"Admin source requires 'userdata.info.admin' scope",
"Источник 'администратор' требует право 'userdata.info.admin'",
)
if new.source == "dwh" and "userdata.info.dwh" not in scope_names:
raise Forbidden(
"Dwh source requires 'userdata.info.dwh' scope",
"Источник 'dwh' требует право 'userdata.info.dwh'",
)
if new.source != "admin" and new.source != "user" and new.source != "dwh":
raise Forbidden(
"HTTP protocol applying only 'admin', 'user' or 'dwh' source",
"Данный источник информации не обновляется через HTTP",
)
if new.source == "user" and user["id"] != user_id:
raise Forbidden("'user' source requires information own", "Требуется владение информацией")
for item in new.items:
param = (
db.session.query(Param)
.join(Category)
.filter(
Param.name == item.param,
Category.name == item.category,
not_(Param.is_deleted),
not_(Category.is_deleted),
)
.one_or_none()
)
if not param:
raise ObjectNotFound(Param, item.param)
if (
param.category.update_scope is not None
and param.category.update_scope not in scope_names
and not (new.source == "user" and user["id"] == user_id)
):
db.session.rollback()
raise Forbidden(
f"Updating category {param.category.name=} requires {param.category.update_scope=} scope",
f"Обновление категории {param.category.name=} требует {param.category.update_scope=} права",
)
info = (
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),
)
.one_or_none()
)
if not info and item.value is None:
continue
if not info:
source = Source.query(session=db.session).filter(Source.name == new.source).one_or_none()
if not source:
raise ObjectNotFound(Source, new.source)
if param.validation is not None and search(param.validation, item.value) is None:
raise InvalidValidation(Info, "value")
Info.create(
session=db.session,
owner_id=user_id,
param_id=param.id,
source_id=source.id,
value=item.value,
)
continue
if item.value is None:
info.is_deleted = True
db.session.flush()
continue
if not param.changeable and "userdata.info.update" not in scope_names:
db.session.rollback()
raise Forbidden(
f"Param {param.name=} change requires 'userdata.info.update' scope",
f"Изменение {param.name=} параметра требует 'userdata.info.update' права",
)
if param.validation is not None and search(param.validation, item.value) is None:
raise InvalidValidation(Info, "value")
info.value = item.value
db.session.flush()
async def get_users_info(
user_ids: list[int],
category_ids: list[int] | None,
user: dict[str, int | list[dict[str, str | int]]],
additional_data: list[int] | None = None,
) -> list[dict[str, str | None]]:
""".
Возвращает информацию о данных пользователей в указанных категориях
:param user_ids: Список айди юзеров
:param category_ids: Список айди необходимых категорий, если None, то мы запрашиваем информацию только обо одном пользователе user_ids[0] обо всех досутпных категориях
:param user: Сессия выполняющего запрос данных
:return: Список словарей содержащих id пользователя, категорию, параметр категории и значение этого параметра у пользователя
"""
if additional_data is None:
additional_data = []
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.in_(user_ids),
not_(Param.is_deleted),
not_(Category.is_deleted),
not_(Info.is_deleted),
or_(
Param.visible_in_user_response,
Param.id.in_(additional_data),
),
)
)
if not is_single_user:
query = query.filter(Param.category_id.in_(category_ids))
infos = query.all()
if not infos:
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 (not is_single_user or info.owner_id != user["id"])
and not info.param.is_public
):
continue
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][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][info.owner_id].source.trust_level < info.source.trust_level
or (
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
)
)
)
):
"""
Сюда он зайдет либо если параметру не соответствует никакой информации,
либо если встретил более релевантную.
Если у параметра отображение по доверию, то более релевантная
- строго больше индекс доверия/такой же индекс доверия,
но информация более поздняя по времени
Если у параметра отображение по времени то более релевантная - более позднаяя
"""
param_dict[info.param][info.owner_id] = info
result = []
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]]],
additional_data: list[int],
) -> UsersInfoGet:
""".
Возвращает информацию о данных пользователей в указанных категориях
:param user_ids: Список айди юзеров
:param category_ids: Список айди необходимых категорий
:param user: Сессия выполняющего запрос данных
:return: Список словарей содержащих id пользователя, категорию, параметр категории и значение этого параметра у пользователя
"""
return UsersInfoGet(items=await get_users_info(user_ids, category_ids, user, additional_data))
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)