dmart 0.1.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alembic/__init__.py +0 -0
- alembic/env.py +91 -0
- alembic/scripts/__init__.py +0 -0
- alembic/scripts/calculate_checksums.py +77 -0
- alembic/scripts/migration_f7a4949eed19.py +28 -0
- alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
- alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
- alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
- alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
- alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
- alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
- alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
- alembic/versions/74288ccbd3b5_initial.py +264 -0
- alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
- alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
- alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
- alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
- alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
- alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
- alembic/versions/__init__.py +0 -0
- alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
- alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
- alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
- api/__init__.py +0 -0
- api/info/__init__.py +0 -0
- api/info/router.py +109 -0
- api/managed/__init__.py +0 -0
- api/managed/router.py +1541 -0
- api/managed/utils.py +1850 -0
- api/public/__init__.py +0 -0
- api/public/router.py +758 -0
- api/qr/__init__.py +0 -0
- api/qr/router.py +108 -0
- api/user/__init__.py +0 -0
- api/user/model/__init__.py +0 -0
- api/user/model/errors.py +14 -0
- api/user/model/requests.py +165 -0
- api/user/model/responses.py +11 -0
- api/user/router.py +1401 -0
- api/user/service.py +270 -0
- bundler.py +44 -0
- config/__init__.py +0 -0
- config/channels.json +11 -0
- config/notification.json +17 -0
- data_adapters/__init__.py +0 -0
- data_adapters/adapter.py +16 -0
- data_adapters/base_data_adapter.py +467 -0
- data_adapters/file/__init__.py +0 -0
- data_adapters/file/adapter.py +2043 -0
- data_adapters/file/adapter_helpers.py +1013 -0
- data_adapters/file/archive.py +150 -0
- data_adapters/file/create_index.py +331 -0
- data_adapters/file/create_users_folders.py +52 -0
- data_adapters/file/custom_validations.py +68 -0
- data_adapters/file/drop_index.py +40 -0
- data_adapters/file/health_check.py +560 -0
- data_adapters/file/redis_services.py +1110 -0
- data_adapters/helpers.py +27 -0
- data_adapters/sql/__init__.py +0 -0
- data_adapters/sql/adapter.py +3210 -0
- data_adapters/sql/adapter_helpers.py +491 -0
- data_adapters/sql/create_tables.py +451 -0
- data_adapters/sql/create_users_folders.py +53 -0
- data_adapters/sql/db_to_json_migration.py +482 -0
- data_adapters/sql/health_check_sql.py +232 -0
- data_adapters/sql/json_to_db_migration.py +454 -0
- data_adapters/sql/update_query_policies.py +101 -0
- data_generator.py +81 -0
- dmart-0.1.9.dist-info/METADATA +64 -0
- dmart-0.1.9.dist-info/RECORD +149 -0
- dmart-0.1.9.dist-info/WHEEL +5 -0
- dmart-0.1.9.dist-info/entry_points.txt +2 -0
- dmart-0.1.9.dist-info/top_level.txt +23 -0
- dmart.py +513 -0
- get_settings.py +7 -0
- languages/__init__.py +0 -0
- languages/arabic.json +15 -0
- languages/english.json +16 -0
- languages/kurdish.json +14 -0
- languages/loader.py +13 -0
- main.py +506 -0
- migrate.py +24 -0
- models/__init__.py +0 -0
- models/api.py +203 -0
- models/core.py +597 -0
- models/enums.py +255 -0
- password_gen.py +8 -0
- plugins/__init__.py +0 -0
- plugins/action_log/__init__.py +0 -0
- plugins/action_log/plugin.py +121 -0
- plugins/admin_notification_sender/__init__.py +0 -0
- plugins/admin_notification_sender/plugin.py +124 -0
- plugins/ldap_manager/__init__.py +0 -0
- plugins/ldap_manager/plugin.py +100 -0
- plugins/local_notification/__init__.py +0 -0
- plugins/local_notification/plugin.py +123 -0
- plugins/realtime_updates_notifier/__init__.py +0 -0
- plugins/realtime_updates_notifier/plugin.py +58 -0
- plugins/redis_db_update/__init__.py +0 -0
- plugins/redis_db_update/plugin.py +188 -0
- plugins/resource_folders_creation/__init__.py +0 -0
- plugins/resource_folders_creation/plugin.py +81 -0
- plugins/system_notification_sender/__init__.py +0 -0
- plugins/system_notification_sender/plugin.py +188 -0
- plugins/update_access_controls/__init__.py +0 -0
- plugins/update_access_controls/plugin.py +9 -0
- pytests/__init__.py +0 -0
- pytests/api_user_models_erros_test.py +16 -0
- pytests/api_user_models_requests_test.py +98 -0
- pytests/archive_test.py +72 -0
- pytests/base_test.py +300 -0
- pytests/get_settings_test.py +14 -0
- pytests/json_to_db_migration_test.py +237 -0
- pytests/service_test.py +26 -0
- pytests/test_info.py +55 -0
- pytests/test_status.py +15 -0
- run_notification_campaign.py +98 -0
- scheduled_notification_handler.py +121 -0
- schema_migration.py +208 -0
- schema_modulate.py +192 -0
- set_admin_passwd.py +55 -0
- sync.py +202 -0
- utils/__init__.py +0 -0
- utils/access_control.py +306 -0
- utils/async_request.py +8 -0
- utils/exporter.py +309 -0
- utils/firebase_notifier.py +57 -0
- utils/generate_email.py +38 -0
- utils/helpers.py +352 -0
- utils/hypercorn_config.py +12 -0
- utils/internal_error_code.py +60 -0
- utils/jwt.py +124 -0
- utils/logger.py +167 -0
- utils/middleware.py +99 -0
- utils/notification.py +75 -0
- utils/password_hashing.py +16 -0
- utils/plugin_manager.py +215 -0
- utils/query_policies_helper.py +112 -0
- utils/regex.py +44 -0
- utils/repository.py +529 -0
- utils/router_helper.py +19 -0
- utils/settings.py +165 -0
- utils/sms_notifier.py +21 -0
- utils/social_sso.py +67 -0
- utils/templates/activation.html.j2 +26 -0
- utils/templates/reminder.html.j2 +17 -0
- utils/ticket_sys_utils.py +203 -0
- utils/web_notifier.py +29 -0
- websocket.py +231 -0
api/public/router.py
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from re import sub as res_sub
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
from fastapi import APIRouter, Body, Depends, Form, Path, Query, UploadFile, status
|
|
5
|
+
from models.enums import AttachmentType, ContentType, ResourceType, TaskType, PublicSubmitResourceType
|
|
6
|
+
from data_adapters.adapter import data_adapter as db
|
|
7
|
+
import models.api as api
|
|
8
|
+
from utils.helpers import camel_case
|
|
9
|
+
from utils.internal_error_code import InternalErrorCode
|
|
10
|
+
import utils.regex as regex
|
|
11
|
+
import models.core as core
|
|
12
|
+
from api.managed.utils import get_mime_type, get_resource_content_type_from_payload_content_type, \
|
|
13
|
+
create_or_update_resource_with_payload_handler, iter_bytesio
|
|
14
|
+
from typing import Any, Union, Optional
|
|
15
|
+
import sys
|
|
16
|
+
import re
|
|
17
|
+
import os
|
|
18
|
+
from utils.access_control import access_control
|
|
19
|
+
import utils.repository as repository
|
|
20
|
+
from utils.plugin_manager import plugin_manager
|
|
21
|
+
from utils.router_helper import is_space_exist
|
|
22
|
+
from utils.settings import settings
|
|
23
|
+
from starlette.responses import FileResponse, StreamingResponse
|
|
24
|
+
|
|
25
|
+
from utils.ticket_sys_utils import set_init_state_for_record
|
|
26
|
+
from fastapi.responses import ORJSONResponse
|
|
27
|
+
|
|
28
|
+
router = APIRouter(default_response_class=ORJSONResponse)
|
|
29
|
+
|
|
30
|
+
# Retrieve publically-available content
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.post("/query", response_model=api.Response, response_model_exclude_none=True)
|
|
34
|
+
async def query_entries(query: api.Query) -> api.Response:
|
|
35
|
+
await plugin_manager.before_action(
|
|
36
|
+
core.Event(
|
|
37
|
+
space_name=query.space_name,
|
|
38
|
+
subpath=query.subpath,
|
|
39
|
+
action_type=core.ActionType.query,
|
|
40
|
+
user_shortname="anonymous",
|
|
41
|
+
attributes={"filter_shortnames": query.filter_shortnames},
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
total, records = await repository.serve_query(
|
|
46
|
+
query, "anonymous"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
await plugin_manager.after_action(
|
|
50
|
+
core.Event(
|
|
51
|
+
space_name=query.space_name,
|
|
52
|
+
subpath=query.subpath,
|
|
53
|
+
action_type=core.ActionType.query,
|
|
54
|
+
user_shortname="anonymous",
|
|
55
|
+
attributes={"filter_shortnames": query.filter_shortnames},
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return api.Response(
|
|
60
|
+
status=api.Status.success,
|
|
61
|
+
records=records,
|
|
62
|
+
attributes={"total": total, "returned": len(records)},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.get(
|
|
67
|
+
"/entry/{resource_type}/{space_name}/{subpath:path}/{shortname}",
|
|
68
|
+
response_model_exclude_none=True,
|
|
69
|
+
)
|
|
70
|
+
async def retrieve_entry_meta(
|
|
71
|
+
resource_type: ResourceType,
|
|
72
|
+
space_name: str = Path(..., pattern=regex.SPACENAME),
|
|
73
|
+
subpath: str = Path(..., pattern=regex.SUBPATH),
|
|
74
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME),
|
|
75
|
+
retrieve_json_payload: bool = False,
|
|
76
|
+
retrieve_attachments: bool = False,
|
|
77
|
+
filter_attachments_types: list = Query(default=[], examples=["media", "comment", "json"]),
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
if subpath == settings.root_subpath_mw:
|
|
80
|
+
subpath = "/"
|
|
81
|
+
|
|
82
|
+
await plugin_manager.before_action(
|
|
83
|
+
core.Event(
|
|
84
|
+
space_name=space_name,
|
|
85
|
+
subpath=subpath,
|
|
86
|
+
shortname=shortname,
|
|
87
|
+
action_type=core.ActionType.view,
|
|
88
|
+
resource_type=resource_type,
|
|
89
|
+
user_shortname="anonymous",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
resource_class = getattr(
|
|
94
|
+
sys.modules["models.core"], camel_case(resource_type))
|
|
95
|
+
meta = await db.load(
|
|
96
|
+
space_name=space_name,
|
|
97
|
+
subpath=subpath,
|
|
98
|
+
shortname=shortname,
|
|
99
|
+
class_type=resource_class,
|
|
100
|
+
user_shortname="anonymous",
|
|
101
|
+
)
|
|
102
|
+
if meta is None:
|
|
103
|
+
raise api.Exception(
|
|
104
|
+
status.HTTP_400_BAD_REQUEST,
|
|
105
|
+
error=api.Error(
|
|
106
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not found"
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if not await access_control.check_access(
|
|
111
|
+
user_shortname="anonymous",
|
|
112
|
+
space_name=space_name,
|
|
113
|
+
subpath=subpath,
|
|
114
|
+
resource_type=resource_type,
|
|
115
|
+
action_type=core.ActionType.view,
|
|
116
|
+
resource_is_active=meta.is_active,
|
|
117
|
+
resource_owner_shortname=meta.owner_shortname,
|
|
118
|
+
resource_owner_group=meta.owner_group_shortname,
|
|
119
|
+
entry_shortname=meta.shortname,
|
|
120
|
+
):
|
|
121
|
+
raise api.Exception(
|
|
122
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
123
|
+
api.Error(
|
|
124
|
+
type="request",
|
|
125
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
126
|
+
message="You don't have permission to this action [14]",
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
attachments = {}
|
|
131
|
+
entry_path = (
|
|
132
|
+
settings.spaces_folder
|
|
133
|
+
/ f"{space_name}/{subpath}/.dm/{shortname}"
|
|
134
|
+
)
|
|
135
|
+
if retrieve_attachments:
|
|
136
|
+
attachments = await db.get_entry_attachments(
|
|
137
|
+
subpath=subpath,
|
|
138
|
+
attachments_path=entry_path,
|
|
139
|
+
retrieve_json_payload=retrieve_json_payload,
|
|
140
|
+
filter_types=filter_attachments_types
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if (not retrieve_json_payload or
|
|
144
|
+
not meta.payload or
|
|
145
|
+
not meta.payload.body or
|
|
146
|
+
not isinstance(meta.payload.body, str) or
|
|
147
|
+
meta.payload.content_type != ContentType.json
|
|
148
|
+
):
|
|
149
|
+
# TODO
|
|
150
|
+
# include locked before returning the dictionary
|
|
151
|
+
return {
|
|
152
|
+
**meta.dict(exclude_none=True),
|
|
153
|
+
"attachments": attachments
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
payload_body = await db.load_resource_payload(
|
|
157
|
+
space_name=space_name,
|
|
158
|
+
subpath=subpath,
|
|
159
|
+
filename=meta.payload.body,
|
|
160
|
+
class_type=resource_class,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if meta.payload and meta.payload.schema_shortname and payload_body:
|
|
164
|
+
await db.validate_payload_with_schema(
|
|
165
|
+
payload_data=payload_body,
|
|
166
|
+
space_name=space_name,
|
|
167
|
+
schema_shortname=meta.payload.schema_shortname,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
meta.payload.body = payload_body
|
|
171
|
+
await plugin_manager.after_action(
|
|
172
|
+
core.Event(
|
|
173
|
+
space_name=space_name,
|
|
174
|
+
subpath=subpath,
|
|
175
|
+
shortname=shortname,
|
|
176
|
+
action_type=core.ActionType.view,
|
|
177
|
+
resource_type=resource_type,
|
|
178
|
+
user_shortname="anonymous",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
**meta.dict(exclude_none=True),
|
|
184
|
+
"attachments": attachments
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Public payload retrieval; can be used in "src=" in html pages
|
|
189
|
+
@router.get(
|
|
190
|
+
"/payload/{resource_type}/{space_name}/{subpath:path}/{shortname}.{ext}",
|
|
191
|
+
response_model=None
|
|
192
|
+
)
|
|
193
|
+
async def retrieve_entry_or_attachment_payload(
|
|
194
|
+
resource_type: ResourceType,
|
|
195
|
+
space_name: str = Path(..., pattern=regex.SPACENAME),
|
|
196
|
+
subpath: str = Path(..., pattern=regex.SUBPATH),
|
|
197
|
+
shortname: str = Path(..., pattern=regex.SHORTNAME),
|
|
198
|
+
ext: str = Path(..., pattern=regex.EXT),
|
|
199
|
+
) -> FileResponse | api.Response | StreamingResponse:
|
|
200
|
+
await plugin_manager.before_action(
|
|
201
|
+
core.Event(
|
|
202
|
+
space_name=space_name,
|
|
203
|
+
subpath=subpath,
|
|
204
|
+
shortname=shortname,
|
|
205
|
+
action_type=core.ActionType.view,
|
|
206
|
+
resource_type=resource_type,
|
|
207
|
+
user_shortname="anonymous",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
cls = getattr(sys.modules["models.core"], camel_case(resource_type))
|
|
211
|
+
meta = await db.load(
|
|
212
|
+
space_name=space_name,
|
|
213
|
+
subpath=subpath,
|
|
214
|
+
shortname=shortname,
|
|
215
|
+
class_type=cls,
|
|
216
|
+
user_shortname="anonymous",
|
|
217
|
+
)
|
|
218
|
+
if (
|
|
219
|
+
meta.payload is None
|
|
220
|
+
or meta.payload.body is None
|
|
221
|
+
or meta.payload.body != f"{shortname}.{ext}"
|
|
222
|
+
):
|
|
223
|
+
raise api.Exception(
|
|
224
|
+
status.HTTP_400_BAD_REQUEST,
|
|
225
|
+
error=api.Error(
|
|
226
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if not await access_control.check_access(
|
|
231
|
+
user_shortname="anonymous",
|
|
232
|
+
space_name=space_name,
|
|
233
|
+
subpath=subpath,
|
|
234
|
+
resource_type=resource_type,
|
|
235
|
+
action_type=core.ActionType.view,
|
|
236
|
+
resource_is_active=meta.is_active,
|
|
237
|
+
resource_owner_shortname=meta.owner_shortname,
|
|
238
|
+
resource_owner_group=meta.owner_group_shortname,
|
|
239
|
+
entry_shortname=meta.shortname,
|
|
240
|
+
):
|
|
241
|
+
raise api.Exception(
|
|
242
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
243
|
+
api.Error(
|
|
244
|
+
type="request",
|
|
245
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
246
|
+
message="You don't have permission to this action [15]",
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
await plugin_manager.after_action(
|
|
251
|
+
core.Event(
|
|
252
|
+
space_name=space_name,
|
|
253
|
+
subpath=subpath,
|
|
254
|
+
shortname=shortname,
|
|
255
|
+
action_type=core.ActionType.view,
|
|
256
|
+
resource_type=resource_type,
|
|
257
|
+
user_shortname="anonymous",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
if settings.active_data_db == "file":
|
|
262
|
+
payload_path = db.payload_path(
|
|
263
|
+
space_name=space_name,
|
|
264
|
+
subpath=subpath,
|
|
265
|
+
class_type=cls,
|
|
266
|
+
)
|
|
267
|
+
return FileResponse(payload_path / str(meta.payload.body))
|
|
268
|
+
|
|
269
|
+
if meta.payload.content_type == ContentType.json and isinstance(meta.payload.body, dict):
|
|
270
|
+
return api.Response(
|
|
271
|
+
status=api.Status.success,
|
|
272
|
+
attributes=meta.payload.body
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
data = await db.get_media_attachment(space_name, subpath, shortname)
|
|
276
|
+
if data:
|
|
277
|
+
return StreamingResponse(iter_bytesio(data), media_type=get_mime_type(meta.payload.content_type))
|
|
278
|
+
|
|
279
|
+
return api.Response(status=api.Status.failed)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
"""
|
|
283
|
+
@router.post("/submit", response_model_exclude_none=True)
|
|
284
|
+
async def submit() -> api.Response:
|
|
285
|
+
return api.Response(status=api.Status.success)
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@router.get(
|
|
290
|
+
"/query/{type}/{space_name}/{subpath:path}",
|
|
291
|
+
response_model=api.Response,
|
|
292
|
+
response_model_exclude_none=True,
|
|
293
|
+
)
|
|
294
|
+
async def query_via_urlparams(
|
|
295
|
+
query: api.Query = Depends(api.Query),
|
|
296
|
+
) -> api.Response:
|
|
297
|
+
await plugin_manager.before_action(
|
|
298
|
+
core.Event(
|
|
299
|
+
space_name=query.space_name,
|
|
300
|
+
subpath=query.subpath,
|
|
301
|
+
action_type=core.ActionType.query,
|
|
302
|
+
user_shortname="anonymous",
|
|
303
|
+
attributes={"filter_shortnames": query.filter_shortnames},
|
|
304
|
+
)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
total, records = await repository.serve_query(
|
|
308
|
+
query, "anonymous"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
await plugin_manager.after_action(
|
|
312
|
+
core.Event(
|
|
313
|
+
space_name=query.space_name,
|
|
314
|
+
subpath=query.subpath,
|
|
315
|
+
action_type=core.ActionType.query,
|
|
316
|
+
user_shortname="anonymous",
|
|
317
|
+
attributes={"filter_shortnames": query.filter_shortnames},
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
return api.Response(
|
|
322
|
+
status=api.Status.success,
|
|
323
|
+
records=records,
|
|
324
|
+
attributes={"total": total, "returned": len(records)},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@router.post(
|
|
329
|
+
"/resource_with_payload",
|
|
330
|
+
response_model=api.Response,
|
|
331
|
+
response_model_exclude_none=True,
|
|
332
|
+
)
|
|
333
|
+
async def create_or_update_resource_with_payload(
|
|
334
|
+
payload_file: UploadFile,
|
|
335
|
+
request_record: UploadFile,
|
|
336
|
+
space_name: str = Form(..., examples=["data"]),
|
|
337
|
+
sha: str | None = Form(None, examples=["data"]),
|
|
338
|
+
):
|
|
339
|
+
# NOTE We currently make no distinction between create and update.
|
|
340
|
+
# in such case update should contain all the data every time.
|
|
341
|
+
await is_space_exist(space_name)
|
|
342
|
+
|
|
343
|
+
record = core.Record.model_validate_json(request_record.file.read())
|
|
344
|
+
|
|
345
|
+
payload_filename = payload_file.filename or ""
|
|
346
|
+
if payload_filename and not re.search(regex.EXT, os.path.splitext(payload_filename)[1][1:]):
|
|
347
|
+
raise api.Exception(
|
|
348
|
+
status.HTTP_400_BAD_REQUEST,
|
|
349
|
+
api.Error(
|
|
350
|
+
type="request",
|
|
351
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
352
|
+
message=f"Invalid payload file extention, it should end with {regex.EXT}",
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
resource_content_type = get_resource_content_type_from_payload_content_type(
|
|
356
|
+
payload_file, payload_filename, record
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
await plugin_manager.before_action(
|
|
360
|
+
core.Event(
|
|
361
|
+
space_name=space_name,
|
|
362
|
+
subpath=record.subpath,
|
|
363
|
+
shortname=record.shortname,
|
|
364
|
+
action_type=core.ActionType.create,
|
|
365
|
+
schema_shortname=record.attributes.get("payload", {}).get(
|
|
366
|
+
"schema_shortname", None
|
|
367
|
+
),
|
|
368
|
+
resource_type=record.resource_type,
|
|
369
|
+
user_shortname="anonymous",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
if not await access_control.check_access(
|
|
374
|
+
user_shortname="anonymous",
|
|
375
|
+
space_name=space_name,
|
|
376
|
+
subpath=record.subpath,
|
|
377
|
+
resource_type=record.resource_type,
|
|
378
|
+
action_type=core.ActionType.create,
|
|
379
|
+
record_attributes=record.attributes,
|
|
380
|
+
entry_shortname=record.shortname,
|
|
381
|
+
):
|
|
382
|
+
raise api.Exception(
|
|
383
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
384
|
+
api.Error(
|
|
385
|
+
type="request",
|
|
386
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
387
|
+
message="You don't have permission to this action [50]",
|
|
388
|
+
),
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
sha1 = hashlib.sha1()
|
|
392
|
+
sha1.update(payload_file.file.read())
|
|
393
|
+
checksum = sha1.hexdigest()
|
|
394
|
+
if isinstance(sha, str) and sha != checksum:
|
|
395
|
+
raise api.Exception(
|
|
396
|
+
status.HTTP_400_BAD_REQUEST,
|
|
397
|
+
api.Error(
|
|
398
|
+
type="request",
|
|
399
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
400
|
+
message="The provided file doesn't match the sha",
|
|
401
|
+
),
|
|
402
|
+
)
|
|
403
|
+
await payload_file.seek(0)
|
|
404
|
+
resource_obj, record = await create_or_update_resource_with_payload_handler(
|
|
405
|
+
record, "anonymous", space_name, payload_file, payload_filename, checksum, sha, resource_content_type
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
await db.save(space_name, record.subpath, resource_obj)
|
|
409
|
+
await db.save_payload(
|
|
410
|
+
space_name, record.subpath, resource_obj, payload_file
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
await plugin_manager.after_action(
|
|
414
|
+
core.Event(
|
|
415
|
+
space_name=space_name,
|
|
416
|
+
subpath=record.subpath,
|
|
417
|
+
shortname=record.shortname,
|
|
418
|
+
action_type=core.ActionType.create,
|
|
419
|
+
schema_shortname=record.attributes.get("payload", {}).get(
|
|
420
|
+
"schema_shortname", None
|
|
421
|
+
),
|
|
422
|
+
resource_type=record.resource_type,
|
|
423
|
+
user_shortname="anonymous",
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
return api.Response(
|
|
428
|
+
status=api.Status.success,
|
|
429
|
+
records=[record],
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@router.post("/submit/{space_name}/{schema_shortname}/{subpath:path}")
|
|
434
|
+
@router.post("/submit/{space_name}/{resource_type}/{schema_shortname}/{subpath:path}")
|
|
435
|
+
@router.post("/submit/{space_name}/{resource_type}/{workflow_shortname}/{schema_shortname}/{subpath:path}")
|
|
436
|
+
async def create_entry(
|
|
437
|
+
space_name: str = Path(...),
|
|
438
|
+
schema_shortname: str = Path(...),
|
|
439
|
+
subpath: str = Path(..., pattern=regex.SUBPATH),
|
|
440
|
+
resource_type: PublicSubmitResourceType | None = None,
|
|
441
|
+
workflow_shortname: str | None = None,
|
|
442
|
+
body_dict: dict[str, Any] = Body(...),
|
|
443
|
+
):
|
|
444
|
+
allowed_models = settings.allowed_submit_models
|
|
445
|
+
entry_resource_type: ResourceType = ResourceType(resource_type.name) if resource_type else ResourceType.content
|
|
446
|
+
if (
|
|
447
|
+
space_name not in allowed_models
|
|
448
|
+
or schema_shortname not in allowed_models[space_name]
|
|
449
|
+
):
|
|
450
|
+
raise api.Exception(
|
|
451
|
+
status.HTTP_400_BAD_REQUEST,
|
|
452
|
+
api.Error(
|
|
453
|
+
type="request",
|
|
454
|
+
code=InternalErrorCode.NOT_ALLOWED_LOCATION,
|
|
455
|
+
message="Selected location is not allowed",
|
|
456
|
+
),
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if not await access_control.check_access(
|
|
460
|
+
user_shortname="anonymous",
|
|
461
|
+
space_name=space_name,
|
|
462
|
+
subpath=subpath,
|
|
463
|
+
resource_type=entry_resource_type,
|
|
464
|
+
action_type=core.ActionType.create,
|
|
465
|
+
record_attributes=body_dict,
|
|
466
|
+
):
|
|
467
|
+
raise api.Exception(
|
|
468
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
469
|
+
api.Error(
|
|
470
|
+
type="request",
|
|
471
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
472
|
+
message="You don't have permission to this action [13]",
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
uuid = uuid4()
|
|
477
|
+
shortname = str(uuid)[:8]
|
|
478
|
+
await plugin_manager.before_action(
|
|
479
|
+
core.Event(
|
|
480
|
+
space_name=space_name,
|
|
481
|
+
subpath=subpath,
|
|
482
|
+
shortname=shortname,
|
|
483
|
+
action_type=core.ActionType.create,
|
|
484
|
+
schema_shortname=schema_shortname,
|
|
485
|
+
resource_type=entry_resource_type,
|
|
486
|
+
user_shortname="anonymous",
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
content_obj: Optional[Union[core.Content, core.Ticket]] = None
|
|
491
|
+
if entry_resource_type == ResourceType.ticket:
|
|
492
|
+
if workflow_shortname is None:
|
|
493
|
+
raise api.Exception(
|
|
494
|
+
status.HTTP_400_BAD_REQUEST,
|
|
495
|
+
api.Error(
|
|
496
|
+
type="request",
|
|
497
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
498
|
+
message="Workflow shortname is required for ticket creation",
|
|
499
|
+
),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
record = core.Record(
|
|
503
|
+
subpath=subpath,
|
|
504
|
+
shortname=shortname,
|
|
505
|
+
resource_type=entry_resource_type,
|
|
506
|
+
attributes={
|
|
507
|
+
"workflow_shortname": workflow_shortname,
|
|
508
|
+
**body_dict
|
|
509
|
+
}
|
|
510
|
+
)
|
|
511
|
+
record = await set_init_state_for_record(record, space_name, "anonymous")
|
|
512
|
+
if not record or not record.attributes.get("state"):
|
|
513
|
+
raise api.Exception(
|
|
514
|
+
status.HTTP_400_BAD_REQUEST,
|
|
515
|
+
api.Error(
|
|
516
|
+
type="request",
|
|
517
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
518
|
+
message="Failed to set initial state",
|
|
519
|
+
),
|
|
520
|
+
)
|
|
521
|
+
content_obj = core.Ticket(
|
|
522
|
+
uuid=uuid,
|
|
523
|
+
shortname=shortname,
|
|
524
|
+
is_active=True,
|
|
525
|
+
owner_shortname="anonymous",
|
|
526
|
+
payload=core.Payload(
|
|
527
|
+
content_type=ContentType.json,
|
|
528
|
+
schema_shortname=schema_shortname,
|
|
529
|
+
body=f"{shortname}.json",
|
|
530
|
+
),
|
|
531
|
+
state=record.attributes["state"],
|
|
532
|
+
workflow_shortname=workflow_shortname,
|
|
533
|
+
is_open=record.attributes["is_open"]
|
|
534
|
+
)
|
|
535
|
+
elif entry_resource_type == ResourceType.content:
|
|
536
|
+
content_obj = core.Content(
|
|
537
|
+
uuid=uuid,
|
|
538
|
+
shortname=shortname,
|
|
539
|
+
is_active=True,
|
|
540
|
+
owner_shortname="anonymous",
|
|
541
|
+
payload=core.Payload(
|
|
542
|
+
content_type=ContentType.json,
|
|
543
|
+
schema_shortname=schema_shortname,
|
|
544
|
+
body=f"{shortname}.json",
|
|
545
|
+
),
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if content_obj is None:
|
|
549
|
+
raise api.Exception(
|
|
550
|
+
status.HTTP_400_BAD_REQUEST,
|
|
551
|
+
api.Error(
|
|
552
|
+
type="request",
|
|
553
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
554
|
+
message="Invalid resource type for entry creation",
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if content_obj.payload and content_obj.payload.schema_shortname:
|
|
559
|
+
await db.validate_payload_with_schema(
|
|
560
|
+
payload_data=body_dict,
|
|
561
|
+
space_name=space_name,
|
|
562
|
+
schema_shortname=content_obj.payload.schema_shortname,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
response_data = await db.save(space_name, subpath, content_obj)
|
|
566
|
+
if response_data is None:
|
|
567
|
+
raise api.Exception(
|
|
568
|
+
status.HTTP_400_BAD_REQUEST,
|
|
569
|
+
api.Error(
|
|
570
|
+
type="request",
|
|
571
|
+
code=InternalErrorCode.SOMETHING_WRONG,
|
|
572
|
+
message="Something went wrong while saving the entry",
|
|
573
|
+
),
|
|
574
|
+
)
|
|
575
|
+
await db.save_payload_from_json(
|
|
576
|
+
space_name, subpath, content_obj, body_dict
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
await plugin_manager.after_action(
|
|
580
|
+
core.Event(
|
|
581
|
+
space_name=space_name,
|
|
582
|
+
subpath=subpath,
|
|
583
|
+
shortname=shortname,
|
|
584
|
+
action_type=core.ActionType.create,
|
|
585
|
+
schema_shortname=schema_shortname,
|
|
586
|
+
resource_type=entry_resource_type,
|
|
587
|
+
user_shortname="anonymous",
|
|
588
|
+
attributes={}
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
response_data = response_data.model_dump(exclude_none=True, by_alias=True)
|
|
592
|
+
del response_data["query_policies"]
|
|
593
|
+
return api.Response(
|
|
594
|
+
status=api.Status.success,
|
|
595
|
+
records=[response_data],
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@router.post("/attach/{space_name}")
|
|
600
|
+
async def create_attachment(
|
|
601
|
+
space_name: str,
|
|
602
|
+
record: core.Record
|
|
603
|
+
):
|
|
604
|
+
if record.resource_type not in AttachmentType.__members__:
|
|
605
|
+
raise api.Exception(
|
|
606
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
607
|
+
api.Error(
|
|
608
|
+
type="request",
|
|
609
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
610
|
+
message="You don't have permission to this action [54]",
|
|
611
|
+
),
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if not await access_control.check_access(
|
|
615
|
+
user_shortname="anonymous",
|
|
616
|
+
space_name=space_name,
|
|
617
|
+
subpath=record.subpath,
|
|
618
|
+
resource_type=record.resource_type,
|
|
619
|
+
action_type=core.ActionType.create,
|
|
620
|
+
record_attributes=record.attributes,
|
|
621
|
+
):
|
|
622
|
+
raise api.Exception(
|
|
623
|
+
status.HTTP_401_UNAUTHORIZED,
|
|
624
|
+
api.Error(
|
|
625
|
+
type="request",
|
|
626
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
627
|
+
message="You don't have permission to this action [55]",
|
|
628
|
+
),
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
await plugin_manager.before_action(
|
|
632
|
+
core.Event(
|
|
633
|
+
space_name=space_name,
|
|
634
|
+
subpath=record.subpath,
|
|
635
|
+
action_type=core.ActionType.create,
|
|
636
|
+
resource_type=record.resource_type,
|
|
637
|
+
user_shortname="anonymous",
|
|
638
|
+
)
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
record.shortname = 'auto'
|
|
642
|
+
attachment_obj = core.Meta.from_record(
|
|
643
|
+
record=record, owner_shortname="anonymous"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
await db.save(space_name, record.subpath, attachment_obj)
|
|
647
|
+
|
|
648
|
+
await plugin_manager.after_action(
|
|
649
|
+
core.Event(
|
|
650
|
+
space_name=space_name,
|
|
651
|
+
subpath=record.subpath,
|
|
652
|
+
shortname=attachment_obj.shortname,
|
|
653
|
+
action_type=core.ActionType.create,
|
|
654
|
+
resource_type=record.resource_type,
|
|
655
|
+
user_shortname="anonymous",
|
|
656
|
+
attributes={}
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
return api.Response(status=api.Status.success)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@router.post("/excute/{task_type}/{space_name}")
|
|
664
|
+
async def excute(space_name: str, task_type: TaskType, record: core.Record):
|
|
665
|
+
task_type = task_type
|
|
666
|
+
meta = await db.load(
|
|
667
|
+
space_name=space_name,
|
|
668
|
+
subpath=record.subpath,
|
|
669
|
+
shortname=record.shortname,
|
|
670
|
+
class_type=core.Content,
|
|
671
|
+
user_shortname="anonymous"
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if (
|
|
675
|
+
meta.payload is None
|
|
676
|
+
or not isinstance(meta.payload.body, str)
|
|
677
|
+
or not str(meta.payload.body).endswith(".json")
|
|
678
|
+
):
|
|
679
|
+
raise api.Exception(
|
|
680
|
+
status.HTTP_400_BAD_REQUEST,
|
|
681
|
+
error=api.Error(
|
|
682
|
+
type="media", code=InternalErrorCode.OBJECT_NOT_FOUND, message="Request object is not available"
|
|
683
|
+
),
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
mydict = await db.load_resource_payload(
|
|
687
|
+
space_name=space_name,
|
|
688
|
+
subpath=record.subpath,
|
|
689
|
+
filename=str(meta.payload.body),
|
|
690
|
+
class_type=core.Content,
|
|
691
|
+
)
|
|
692
|
+
query_dict = mydict if mydict else {}
|
|
693
|
+
|
|
694
|
+
if meta.payload.schema_shortname == "report":
|
|
695
|
+
query_dict = query_dict["query"]
|
|
696
|
+
else:
|
|
697
|
+
query_dict["subpath"] = query_dict["query_subpath"]
|
|
698
|
+
query_dict.pop("query_subpath")
|
|
699
|
+
|
|
700
|
+
for param, value in record.attributes.items():
|
|
701
|
+
query_dict["search"] = query_dict["search"].replace(
|
|
702
|
+
f"${param}", str(value))
|
|
703
|
+
|
|
704
|
+
query_dict["search"] = res_sub(
|
|
705
|
+
r"@\w*\:({|\()?\$\w*(}|\))?", "", query_dict["search"]
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if "offset" in record.attributes:
|
|
709
|
+
query_dict["offset"] = record.attributes["offset"]
|
|
710
|
+
|
|
711
|
+
if "limit" in record.attributes:
|
|
712
|
+
query_dict["limit"] = record.attributes["limit"]
|
|
713
|
+
|
|
714
|
+
if "from_date" in record.attributes:
|
|
715
|
+
query_dict["from_date"] = record.attributes["from_date"]
|
|
716
|
+
|
|
717
|
+
if "to_date" in record.attributes:
|
|
718
|
+
query_dict["to_date"] = record.attributes["to_date"]
|
|
719
|
+
|
|
720
|
+
filter_shortnames = record.attributes.get("filter_shortnames", [])
|
|
721
|
+
query_dict["filter_shortnames"] = filter_shortnames if isinstance(
|
|
722
|
+
filter_shortnames, list) else []
|
|
723
|
+
|
|
724
|
+
return await query_entries(api.Query(**query_dict))
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
@router.get("/byuuid/{uuid}", response_model_exclude_none=True)
|
|
728
|
+
async def get_entry_by_uuid(
|
|
729
|
+
uuid: str,
|
|
730
|
+
retrieve_json_payload: bool = False,
|
|
731
|
+
retrieve_attachments: bool = False,
|
|
732
|
+
retrieve_lock_status: bool = False
|
|
733
|
+
):
|
|
734
|
+
return await db.get_entry_by_var(
|
|
735
|
+
"uuid",
|
|
736
|
+
uuid,
|
|
737
|
+
"anonymous",
|
|
738
|
+
retrieve_json_payload,
|
|
739
|
+
retrieve_attachments,
|
|
740
|
+
retrieve_lock_status,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
@router.get("/byslug/{slug}", response_model_exclude_none=True)
|
|
745
|
+
async def get_entry_by_slug(
|
|
746
|
+
slug: str,
|
|
747
|
+
retrieve_json_payload: bool = False,
|
|
748
|
+
retrieve_attachments: bool = False,
|
|
749
|
+
retrieve_lock_status: bool = False,
|
|
750
|
+
):
|
|
751
|
+
return await db.get_entry_by_var(
|
|
752
|
+
"slug",
|
|
753
|
+
slug,
|
|
754
|
+
"anonymous",
|
|
755
|
+
retrieve_json_payload,
|
|
756
|
+
retrieve_attachments,
|
|
757
|
+
retrieve_lock_status,
|
|
758
|
+
)
|