brilliance-admin 0.43.1__py3-none-any.whl → 0.44.12__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.
- brilliance_admin/api/routers.py +2 -2
- brilliance_admin/api/views/autocomplete.py +1 -1
- brilliance_admin/api/views/{graphs.py → dashboard.py} +5 -5
- brilliance_admin/api/views/table.py +7 -6
- brilliance_admin/exceptions.py +1 -0
- brilliance_admin/integrations/sqlalchemy/__init__.py +1 -0
- brilliance_admin/integrations/sqlalchemy/auth.py +3 -4
- brilliance_admin/integrations/sqlalchemy/autocomplete.py +12 -3
- brilliance_admin/integrations/sqlalchemy/fields.py +36 -19
- brilliance_admin/integrations/sqlalchemy/fields_schema.py +21 -12
- brilliance_admin/integrations/sqlalchemy/table/base.py +4 -4
- brilliance_admin/integrations/sqlalchemy/table/create.py +6 -3
- brilliance_admin/integrations/sqlalchemy/table/delete.py +2 -2
- brilliance_admin/integrations/sqlalchemy/table/list.py +44 -14
- brilliance_admin/integrations/sqlalchemy/table/retrieve.py +41 -10
- brilliance_admin/integrations/sqlalchemy/table/update.py +10 -5
- brilliance_admin/locales/en.yml +20 -10
- brilliance_admin/locales/ru.yml +20 -10
- brilliance_admin/schema/__init__.py +3 -3
- brilliance_admin/schema/admin_schema.py +31 -23
- brilliance_admin/schema/category.py +88 -14
- brilliance_admin/schema/dashboard/__init__.py +1 -0
- brilliance_admin/schema/dashboard/category_dashboard.py +87 -0
- brilliance_admin/schema/table/category_table.py +13 -8
- brilliance_admin/schema/table/fields/base.py +102 -19
- brilliance_admin/schema/table/fields_schema.py +9 -2
- brilliance_admin/static/{index-D9axz5zK.js → index-8ahvKI6W.js} +190 -190
- brilliance_admin/static/{index-vlBToOhT.css → index-B8JOx1Ps.css} +1 -1
- brilliance_admin/templates/index.html +2 -2
- brilliance_admin/translations.py +6 -3
- brilliance_admin/utils.py +41 -3
- brilliance_admin-0.44.12.dist-info/METADATA +165 -0
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/RECORD +36 -37
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/WHEEL +1 -1
- brilliance_admin-0.44.12.dist-info/licenses/LICENSE +21 -0
- brilliance_admin/schema/graphs/__init__.py +0 -1
- brilliance_admin/schema/graphs/category_graphs.py +0 -51
- brilliance_admin/schema/group.py +0 -67
- brilliance_admin-0.43.1.dist-info/METADATA +0 -214
- brilliance_admin-0.43.1.dist-info/licenses/LICENSE +0 -17
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/top_level.txt +0 -0
brilliance_admin/api/routers.py
CHANGED
|
@@ -4,7 +4,7 @@ from .views.schema import router as schema_router
|
|
|
4
4
|
from .views.table import router as schema_table
|
|
5
5
|
from .views.auth import router as schema_auth
|
|
6
6
|
from .views.autocomplete import router as schema_autocomplete
|
|
7
|
-
from .views.
|
|
7
|
+
from .views.dashboard import router as schema_dashboard
|
|
8
8
|
from .views.settings import router as schema_settings
|
|
9
9
|
from .views.index import router as schema_index
|
|
10
10
|
|
|
@@ -13,6 +13,6 @@ brilliance_admin_router.include_router(schema_router)
|
|
|
13
13
|
brilliance_admin_router.include_router(schema_table)
|
|
14
14
|
brilliance_admin_router.include_router(schema_auth)
|
|
15
15
|
brilliance_admin_router.include_router(schema_autocomplete)
|
|
16
|
-
brilliance_admin_router.include_router(
|
|
16
|
+
brilliance_admin_router.include_router(schema_dashboard)
|
|
17
17
|
brilliance_admin_router.include_router(schema_settings)
|
|
18
18
|
brilliance_admin_router.include_router(schema_index)
|
|
@@ -23,7 +23,7 @@ async def autocomplete(request: Request, group: str, category: str, data: Autoco
|
|
|
23
23
|
context = {'language_context': language_context}
|
|
24
24
|
|
|
25
25
|
try:
|
|
26
|
-
result: AutocompleteResult = await schema_category.autocomplete(data, user, language_context)
|
|
26
|
+
result: AutocompleteResult = await schema_category.autocomplete(data, user, language_context, schema)
|
|
27
27
|
except AdminAPIException as e:
|
|
28
28
|
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
29
29
|
except Exception as e:
|
|
@@ -4,21 +4,21 @@ from fastapi.responses import JSONResponse
|
|
|
4
4
|
from brilliance_admin.api.utils import get_category
|
|
5
5
|
from brilliance_admin.exceptions import AdminAPIException
|
|
6
6
|
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
7
|
-
from brilliance_admin.schema.
|
|
7
|
+
from brilliance_admin.schema.dashboard.category_dashboard import CategoryDashboard, DashboardData, DashboardContainer
|
|
8
8
|
from brilliance_admin.translations import LanguageContext
|
|
9
9
|
from brilliance_admin.utils import get_logger
|
|
10
10
|
|
|
11
|
-
router = APIRouter(prefix="/
|
|
11
|
+
router = APIRouter(prefix="/dashboard", tags=["Category - Dashboard"])
|
|
12
12
|
|
|
13
13
|
logger = get_logger()
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@router.post(path='/{group}/{category}/')
|
|
17
|
-
async def
|
|
17
|
+
async def dashboard_data(request: Request, group: str, category: str, data: DashboardData) -> DashboardContainer:
|
|
18
18
|
schema: AdminSchema = request.app.state.schema
|
|
19
|
-
schema_category, user = await get_category(request, group, category, check_type=
|
|
19
|
+
schema_category, user = await get_category(request, group, category, check_type=CategoryDashboard)
|
|
20
20
|
|
|
21
|
-
result:
|
|
21
|
+
result: DashboardContainer = await schema_category.get_data(data, user)
|
|
22
22
|
|
|
23
23
|
language_slug = request.headers.get('Accept-Language')
|
|
24
24
|
language_context: LanguageContext = schema.get_language_context(language_slug)
|
|
@@ -8,7 +8,8 @@ from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
|
8
8
|
from brilliance_admin.schema import AdminSchema
|
|
9
9
|
from brilliance_admin.schema.table.admin_action import ActionData, ActionResult
|
|
10
10
|
from brilliance_admin.schema.table.category_table import CategoryTable
|
|
11
|
-
from brilliance_admin.schema.table.table_models import
|
|
11
|
+
from brilliance_admin.schema.table.table_models import (
|
|
12
|
+
CreateResult, ListData, RetrieveResult, TableListResult, UpdateResult)
|
|
12
13
|
from brilliance_admin.translations import LanguageContext
|
|
13
14
|
from brilliance_admin.utils import get_logger
|
|
14
15
|
|
|
@@ -29,7 +30,7 @@ async def table_list(request: Request, group: str, category: str, list_data: Lis
|
|
|
29
30
|
context = {'language_context': language_context}
|
|
30
31
|
|
|
31
32
|
try:
|
|
32
|
-
result: TableListResult = await schema_category.get_list(list_data, user, language_context)
|
|
33
|
+
result: TableListResult = await schema_category.get_list(list_data, user, language_context, schema)
|
|
33
34
|
except AdminAPIException as e:
|
|
34
35
|
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
35
36
|
|
|
@@ -53,7 +54,7 @@ async def table_retrieve(request: Request, group: str, category: str, pk: Any) -
|
|
|
53
54
|
context = {'language_context': language_context}
|
|
54
55
|
|
|
55
56
|
try:
|
|
56
|
-
result: RetrieveResult = await schema_category.retrieve(pk, user, language_context)
|
|
57
|
+
result: RetrieveResult = await schema_category.retrieve(pk, user, language_context, schema)
|
|
57
58
|
except AdminAPIException as e:
|
|
58
59
|
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
59
60
|
|
|
@@ -76,7 +77,7 @@ async def table_create(request: Request, group: str, category: str) -> CreateRes
|
|
|
76
77
|
context = {'language_context': language_context}
|
|
77
78
|
|
|
78
79
|
try:
|
|
79
|
-
result: CreateResult = await schema_category.create(await request.json(), user, language_context)
|
|
80
|
+
result: CreateResult = await schema_category.create(await request.json(), user, language_context, schema)
|
|
80
81
|
except AdminAPIException as e:
|
|
81
82
|
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
82
83
|
|
|
@@ -99,7 +100,7 @@ async def table_update(request: Request, group: str, category: str, pk: Any) ->
|
|
|
99
100
|
context = {'language_context': language_context}
|
|
100
101
|
|
|
101
102
|
try:
|
|
102
|
-
result: UpdateResult = await schema_category.update(pk, await request.json(), user, language_context)
|
|
103
|
+
result: UpdateResult = await schema_category.update(pk, await request.json(), user, language_context, schema)
|
|
103
104
|
except AdminAPIException as e:
|
|
104
105
|
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
|
105
106
|
|
|
@@ -128,7 +129,7 @@ async def table_action(
|
|
|
128
129
|
try:
|
|
129
130
|
# pylint: disable=protected-access
|
|
130
131
|
result: ActionResult = await schema_category._perform_action(
|
|
131
|
-
request, action, action_data, language_context, user,
|
|
132
|
+
request, action, action_data, language_context, user, schema,
|
|
132
133
|
)
|
|
133
134
|
except AdminAPIException as e:
|
|
134
135
|
return JSONResponse(e.get_error().model_dump(mode='json', context=context), status_code=e.status_code)
|
brilliance_admin/exceptions.py
CHANGED
|
@@ -10,6 +10,7 @@ from brilliance_admin.utils import DataclassBase, SupportsStr
|
|
|
10
10
|
class FieldError(DataclassBase, Exception):
|
|
11
11
|
message: SupportsStr = None
|
|
12
12
|
code: str | None = None
|
|
13
|
+
field_slug: str | None = None
|
|
13
14
|
|
|
14
15
|
def __post_init__(self):
|
|
15
16
|
if not self.message and not self.code:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# pylint: disable=wildcard-import, unused-wildcard-import, unused-import
|
|
2
2
|
# flake8: noqa: F405
|
|
3
|
+
from .fields import SQLAlchemyRelatedField
|
|
3
4
|
from .auth import SQLAlchemyJWTAdminAuthentication
|
|
4
5
|
from .autocomplete import SQLAlchemyAdminAutocompleteMixin
|
|
5
6
|
from .fields_schema import SQLAlchemyFieldsSchema
|
|
@@ -54,7 +54,7 @@ class SQLAlchemyJWTAdminAuthentication(AdminAuthentication):
|
|
|
54
54
|
logger.exception(
|
|
55
55
|
'SQLAlchemy %s login db error: %s', type(self).__name__, e,
|
|
56
56
|
)
|
|
57
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
57
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
58
58
|
raise AdminAPIException(
|
|
59
59
|
APIError(message=msg, code='connection_refused_error'),
|
|
60
60
|
status_code=500,
|
|
@@ -122,19 +122,18 @@ class SQLAlchemyJWTAdminAuthentication(AdminAuthentication):
|
|
|
122
122
|
try:
|
|
123
123
|
async with self.db_async_session() as session:
|
|
124
124
|
result = await session.execute(stmt)
|
|
125
|
+
user = result.scalar_one_or_none()
|
|
125
126
|
|
|
126
127
|
except ConnectionRefusedError as e:
|
|
127
128
|
logger.exception(
|
|
128
129
|
'SQLAlchemy %s authenticate db error: %s', type(self).__name__, e,
|
|
129
130
|
)
|
|
130
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
131
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
131
132
|
raise AdminAPIException(
|
|
132
133
|
APIError(message=msg, code='connection_refused_error'),
|
|
133
134
|
status_code=500,
|
|
134
135
|
) from e
|
|
135
136
|
|
|
136
|
-
user = result.scalar_one_or_none()
|
|
137
|
-
|
|
138
137
|
if not user:
|
|
139
138
|
raise AdminAPIException(
|
|
140
139
|
APIError(message="User not found", code="user_not_found"),
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
from brilliance_admin.auth import UserABC
|
|
2
|
+
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
2
3
|
from brilliance_admin.schema.table.table_models import AutocompleteData, AutocompleteResult
|
|
3
4
|
from brilliance_admin.translations import LanguageContext
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class SQLAlchemyAdminAutocompleteMixin:
|
|
7
8
|
async def autocomplete(
|
|
8
|
-
self,
|
|
9
|
+
self,
|
|
10
|
+
data: AutocompleteData,
|
|
11
|
+
user: UserABC,
|
|
12
|
+
language_context: LanguageContext,
|
|
13
|
+
admin_schema: AdminSchema,
|
|
9
14
|
) -> AutocompleteResult:
|
|
10
15
|
form_schema = None
|
|
11
16
|
|
|
@@ -32,7 +37,11 @@ class SQLAlchemyAdminAutocompleteMixin:
|
|
|
32
37
|
if not field:
|
|
33
38
|
raise Exception(f'Field "{data.field_slug}" is not found')
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
results = await field.autocomplete(
|
|
41
|
+
self.model,
|
|
42
|
+
data,
|
|
43
|
+
user,
|
|
44
|
+
extra={'db_async_session': self.db_async_session},
|
|
45
|
+
)
|
|
37
46
|
|
|
38
47
|
return AutocompleteResult(results=results)
|
|
@@ -6,7 +6,7 @@ from brilliance_admin.auth import UserABC
|
|
|
6
6
|
from brilliance_admin.exceptions import AdminAPIException, APIError, FieldError
|
|
7
7
|
from brilliance_admin.schema.category import FieldSchemaData
|
|
8
8
|
from brilliance_admin.schema.table.fields.base import TableField
|
|
9
|
-
from brilliance_admin.schema.table.table_models import Record
|
|
9
|
+
from brilliance_admin.schema.table.table_models import AutocompleteData, Record
|
|
10
10
|
from brilliance_admin.translations import LanguageContext
|
|
11
11
|
from brilliance_admin.translations import TranslateText as _
|
|
12
12
|
from brilliance_admin.utils import DeserializeAction
|
|
@@ -43,7 +43,7 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
43
43
|
# - для доступа к связи через ORM
|
|
44
44
|
# getattr(record, rel_name)
|
|
45
45
|
# - для записи и чтения связанных объектов
|
|
46
|
-
rel_name: str | None
|
|
46
|
+
rel_name: str | None
|
|
47
47
|
|
|
48
48
|
# Класс связанной SQLAlchemy-модели.
|
|
49
49
|
# Откуда берётся:
|
|
@@ -96,16 +96,15 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
96
96
|
msg = f'Cannot resolve target model for FK "{field_slug}"'
|
|
97
97
|
raise AttributeError(msg)
|
|
98
98
|
|
|
99
|
-
async def autocomplete(self, model, data, user, *, extra: dict | None = None) -> List[Record]:
|
|
99
|
+
async def autocomplete(self, model, data: AutocompleteData, user, *, extra: dict | None = None) -> List[Record]:
|
|
100
100
|
# pylint: disable=import-outside-toplevel
|
|
101
101
|
from sqlalchemy import select
|
|
102
|
-
from sqlalchemy.sql import expression
|
|
103
102
|
|
|
104
|
-
if extra is None or extra.get('
|
|
105
|
-
msg = f'SQLAlchemyRelatedField.autocomplete {type(self).__name__} requires extra["
|
|
103
|
+
if extra is None or extra.get('db_async_session') is None:
|
|
104
|
+
msg = f'SQLAlchemyRelatedField.autocomplete {type(self).__name__} requires extra["db_async_session"] (AsyncSession)'
|
|
106
105
|
raise AttributeError(msg)
|
|
107
106
|
|
|
108
|
-
|
|
107
|
+
db_async_session = extra['db_async_session']
|
|
109
108
|
|
|
110
109
|
results = []
|
|
111
110
|
|
|
@@ -113,21 +112,37 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
113
112
|
limit = min(150, data.limit)
|
|
114
113
|
stmt = select(target_model).limit(limit)
|
|
115
114
|
|
|
115
|
+
pk = get_pk(target_model)
|
|
116
|
+
python_pk_type = pk.property.columns[0].type.python_type
|
|
117
|
+
|
|
116
118
|
if data.search_string:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
try:
|
|
120
|
+
value = python_pk_type(data.search_string)
|
|
121
|
+
except (ValueError, TypeError):
|
|
122
|
+
# Search string cannot be cast to primary key type, skip id filter
|
|
123
|
+
value = None
|
|
124
|
+
|
|
125
|
+
stmt = stmt.where(pk == value)
|
|
119
126
|
|
|
120
127
|
# Add already selected choices
|
|
121
|
-
existed_choices = []
|
|
122
128
|
if data.existed_choices:
|
|
123
129
|
existed_choices = [i['key'] for i in data.existed_choices if 'key' in i]
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
values = []
|
|
132
|
+
for value in existed_choices:
|
|
133
|
+
try:
|
|
134
|
+
values.append(python_pk_type(value))
|
|
135
|
+
except (ValueError, TypeError) as e:
|
|
136
|
+
msg = f'Invalid existed_choices value "{value}" for pk {pk} python_pk_type:{python_pk_type.__name__}'
|
|
137
|
+
raise AdminAPIException(APIError(message=msg), status_code=500) from e
|
|
138
|
+
|
|
139
|
+
stmt = stmt.where(pk.in_(values))
|
|
140
|
+
|
|
141
|
+
async with db_async_session() as session:
|
|
142
|
+
records = (await session.execute(stmt)).scalars().all()
|
|
127
143
|
|
|
128
|
-
records = (await session.execute(stmt)).scalars().all()
|
|
129
144
|
for record in records:
|
|
130
|
-
results.append(Record(key=getattr(record,
|
|
145
|
+
results.append(Record(key=getattr(record, pk.key), title=str(record)))
|
|
131
146
|
|
|
132
147
|
return results
|
|
133
148
|
|
|
@@ -139,20 +154,22 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
139
154
|
- value всегда scalar (None или int)
|
|
140
155
|
- ORM-объект доступен через extra["record"]
|
|
141
156
|
"""
|
|
157
|
+
if not value:
|
|
158
|
+
return
|
|
159
|
+
|
|
142
160
|
record = extra.get('record')
|
|
143
161
|
if record is None:
|
|
144
162
|
raise FieldError(f'Missing record in serialize context in value: {value}')
|
|
145
163
|
|
|
164
|
+
related = getattr(record, self.rel_name, None)
|
|
165
|
+
|
|
146
166
|
if self.many:
|
|
147
|
-
related = getattr(record, self.rel_name, None)
|
|
148
167
|
if related is None:
|
|
149
|
-
raise FieldError(f'Related field "{self.rel_name}" is missing on record {record}
|
|
150
|
-
|
|
168
|
+
raise FieldError(f'Many Related field "{self.rel_name}" is missing on record "{record}"')
|
|
151
169
|
return [{'key': get_pk(obj), 'title': str(obj)} for obj in related]
|
|
152
170
|
|
|
153
|
-
related = getattr(record, self.rel_name, None)
|
|
154
171
|
if related is None:
|
|
155
|
-
|
|
172
|
+
return None
|
|
156
173
|
|
|
157
174
|
return {'key': get_pk(related), 'title': str(related)}
|
|
158
175
|
|
|
@@ -54,9 +54,6 @@ class SQLAlchemyFieldsSchema(schema.FieldsSchema):
|
|
|
54
54
|
and not col.primary_key
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
-
if "choices" in info:
|
|
58
|
-
field_data["choices"] = [(c[0], c[1]) for c in info["choices"]]
|
|
59
|
-
|
|
60
57
|
col_type = col.type
|
|
61
58
|
try:
|
|
62
59
|
py_t = col_type.python_type
|
|
@@ -64,12 +61,16 @@ class SQLAlchemyFieldsSchema(schema.FieldsSchema):
|
|
|
64
61
|
py_t = None
|
|
65
62
|
|
|
66
63
|
impl = getattr(attr, 'impl', None)
|
|
67
|
-
|
|
64
|
+
is_impl_mutable = isinstance(impl, Mutable)
|
|
68
65
|
|
|
69
66
|
# Foreign key column
|
|
70
67
|
if col.foreign_keys:
|
|
71
68
|
continue
|
|
72
69
|
|
|
70
|
+
elif "choices" in info:
|
|
71
|
+
field_data["choices"] = info['choices']
|
|
72
|
+
field_class = schema.ChoiceField
|
|
73
|
+
|
|
73
74
|
elif isinstance(col_type, (sqltypes.BigInteger, sqltypes.Integer)) or py_t is int:
|
|
74
75
|
field_class = schema.IntegerField
|
|
75
76
|
|
|
@@ -97,7 +98,7 @@ class SQLAlchemyFieldsSchema(schema.FieldsSchema):
|
|
|
97
98
|
elif isinstance(col_type, ARRAY):
|
|
98
99
|
field_class = schema.ArrayField
|
|
99
100
|
field_data["array_type"] = type(col_type.item_type).__name__.lower()
|
|
100
|
-
field_data["read_only"] =
|
|
101
|
+
field_data["read_only"] = is_impl_mutable or isinstance(col_type, Mutable)
|
|
101
102
|
|
|
102
103
|
elif isinstance(col_type, sqltypes.NullType):
|
|
103
104
|
continue
|
|
@@ -126,6 +127,12 @@ class SQLAlchemyFieldsSchema(schema.FieldsSchema):
|
|
|
126
127
|
|
|
127
128
|
# relationship-поля
|
|
128
129
|
for rel in mapper.relationships:
|
|
130
|
+
# relationship, у которых есть локальные FK-колонки, не добавляем в схему,
|
|
131
|
+
# так как связь редактируется через scalar-поле (FK),
|
|
132
|
+
# а relationship используется только для ORM-навигации
|
|
133
|
+
if any(col.foreign_keys for col in rel.local_columns):
|
|
134
|
+
continue
|
|
135
|
+
|
|
129
136
|
field_slug = rel.key
|
|
130
137
|
|
|
131
138
|
field_data = {}
|
|
@@ -233,15 +240,17 @@ class SQLAlchemyFieldsSchema(schema.FieldsSchema):
|
|
|
233
240
|
return stmt
|
|
234
241
|
|
|
235
242
|
async def serialize(self, record, extra: dict, *args, **kwargs) -> dict:
|
|
236
|
-
# pylint: disable=import-outside-toplevel
|
|
237
|
-
from sqlalchemy import inspect
|
|
238
243
|
|
|
239
244
|
# Convert model values to dict
|
|
240
|
-
record_data = {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
record_data = {}
|
|
246
|
+
|
|
247
|
+
for slug, field in self.get_fields().items():
|
|
248
|
+
# pylint: disable=protected-access
|
|
249
|
+
if field._type == 'related':
|
|
250
|
+
record_data[slug] = record
|
|
251
|
+
else:
|
|
252
|
+
record_data[slug] = getattr(record, slug, None)
|
|
253
|
+
|
|
245
254
|
return await super().serialize(record_data, extra, *args, **kwargs)
|
|
246
255
|
|
|
247
256
|
def validate_incoming_data(self, data):
|
|
@@ -123,11 +123,11 @@ class SQLAlchemyAdminBase(SQLAlchemyAdminAutocompleteMixin, CategoryTable):
|
|
|
123
123
|
# pylint: disable=protected-access
|
|
124
124
|
if field._type == "related":
|
|
125
125
|
|
|
126
|
-
# pylint: disable=import-outside-toplevel
|
|
127
|
-
from sqlalchemy import inspect
|
|
128
|
-
|
|
129
|
-
model_attrs = [attr.key for attr in inspect(self.model).mapper.attrs]
|
|
130
126
|
if not hasattr(self.model, field.rel_name):
|
|
127
|
+
# pylint: disable=import-outside-toplevel
|
|
128
|
+
from sqlalchemy import inspect
|
|
129
|
+
model_attrs = [attr.key for attr in inspect(self.model).mapper.attrs]
|
|
130
|
+
|
|
131
131
|
msg = EXCEPTION_REL_NAME.format(
|
|
132
132
|
slug=slug,
|
|
133
133
|
model_name=self.model.__name__,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from brilliance_admin import schema
|
|
2
2
|
from brilliance_admin.auth import UserABC
|
|
3
3
|
from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
4
|
+
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
4
5
|
from brilliance_admin.translations import LanguageContext
|
|
5
6
|
from brilliance_admin.translations import TranslateText as _
|
|
6
7
|
from brilliance_admin.utils import get_logger
|
|
@@ -16,9 +17,10 @@ class SQLAlchemyAdminCreate:
|
|
|
16
17
|
data: dict,
|
|
17
18
|
user: UserABC,
|
|
18
19
|
language_context: LanguageContext,
|
|
20
|
+
admin_schema: AdminSchema,
|
|
19
21
|
) -> schema.CreateResult:
|
|
20
22
|
if not self.has_create:
|
|
21
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
23
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
22
24
|
|
|
23
25
|
# pylint: disable=import-outside-toplevel
|
|
24
26
|
from sqlalchemy.exc import IntegrityError
|
|
@@ -37,7 +39,7 @@ class SQLAlchemyAdminCreate:
|
|
|
37
39
|
type(self).__name__, self.table_schema.model.__name__, e,
|
|
38
40
|
extra={'data': data},
|
|
39
41
|
)
|
|
40
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
42
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
41
43
|
raise AdminAPIException(
|
|
42
44
|
APIError(message=msg, code='connection_refused_error'),
|
|
43
45
|
status_code=500,
|
|
@@ -61,8 +63,9 @@ class SQLAlchemyAdminCreate:
|
|
|
61
63
|
type(self).__name__, self.table_schema.model.__name__, e,
|
|
62
64
|
extra={'data': data},
|
|
63
65
|
)
|
|
66
|
+
msg = _('errors.db_error_create') % {'error_type': type(e).__name__}
|
|
64
67
|
raise AdminAPIException(
|
|
65
|
-
APIError(message=
|
|
68
|
+
APIError(message=msg, code='db_error_create'), status_code=500,
|
|
66
69
|
) from e
|
|
67
70
|
|
|
68
71
|
logger.info(
|
|
@@ -12,7 +12,7 @@ class SQLAlchemyDeleteAction:
|
|
|
12
12
|
base_color='red-lighten-2',
|
|
13
13
|
variant='outlined',
|
|
14
14
|
)
|
|
15
|
-
async def delete(self, action_data: ActionData):
|
|
15
|
+
async def delete(self, *args, action_data: ActionData, **kwargs):
|
|
16
16
|
if not self.has_delete:
|
|
17
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
17
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
18
18
|
return ActionResult(message=ActionMessage(_('deleted_successfully')))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from brilliance_admin import auth, schema
|
|
2
2
|
from brilliance_admin.exceptions import AdminAPIException, APIError, FieldError
|
|
3
3
|
from brilliance_admin.integrations.sqlalchemy.fields_schema import SQLAlchemyFieldsSchema
|
|
4
|
+
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
4
5
|
from brilliance_admin.translations import LanguageContext
|
|
5
6
|
from brilliance_admin.translations import TranslateText as _
|
|
6
7
|
from brilliance_admin.utils import get_logger
|
|
@@ -30,12 +31,12 @@ class SQLAlchemyAdminListMixin:
|
|
|
30
31
|
|
|
31
32
|
if list_data.ordering and ordering not in self.ordering_fields:
|
|
32
33
|
msg = f'Ordering "{ordering}" is not allowed; available options: {self.ordering_fields} default_ordering: {self.default_ordering}'
|
|
33
|
-
raise FieldError(message=msg)
|
|
34
|
+
raise FieldError(message=msg, field_slug='ordering')
|
|
34
35
|
|
|
35
36
|
column = getattr(self.model, ordering, None)
|
|
36
37
|
if not isinstance(column, InstrumentedAttribute):
|
|
37
38
|
msg = f'{type(self).__name__} ordering field "{ordering}" not found in model {self.model}'
|
|
38
|
-
raise FieldError(message=msg)
|
|
39
|
+
raise FieldError(message=msg, field_slug='ordering')
|
|
39
40
|
|
|
40
41
|
return stmt.order_by(direction(column))
|
|
41
42
|
|
|
@@ -88,6 +89,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
88
89
|
list_data: schema.ListData,
|
|
89
90
|
user: auth.UserABC,
|
|
90
91
|
language_context: LanguageContext,
|
|
92
|
+
admin_schema: AdminSchema,
|
|
91
93
|
) -> schema.TableListResult:
|
|
92
94
|
# pylint: disable=import-outside-toplevel
|
|
93
95
|
from sqlalchemy import exc, func, select
|
|
@@ -109,7 +111,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
109
111
|
'list_data': list_data,
|
|
110
112
|
}
|
|
111
113
|
)
|
|
112
|
-
msg = _('filter_error') % {'error': e.message}
|
|
114
|
+
msg = _('errors.filter_error') % {'error': e.message}
|
|
113
115
|
raise AdminAPIException(APIError(message=msg, code='filters_exception'), status_code=500) from e
|
|
114
116
|
|
|
115
117
|
except Exception as e:
|
|
@@ -120,20 +122,15 @@ class SQLAlchemyAdminListMixin:
|
|
|
120
122
|
'list_data': list_data,
|
|
121
123
|
}
|
|
122
124
|
)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
msg = _('errors.filters_exception') % {
|
|
126
|
+
'error': str(e) if admin_schema.debug else type(e).__name__,
|
|
127
|
+
}
|
|
128
|
+
raise AdminAPIException(APIError(message=msg, code='filters_exception'), status_code=500) from e
|
|
126
129
|
|
|
127
130
|
try:
|
|
128
131
|
async with self.db_async_session() as session:
|
|
129
132
|
total_count = await session.scalar(count_stmt)
|
|
130
133
|
records = (await session.execute(stmt)).scalars().all()
|
|
131
|
-
for record in records:
|
|
132
|
-
line = await self.table_schema.serialize(
|
|
133
|
-
record,
|
|
134
|
-
extra={"record": record, "user": user},
|
|
135
|
-
)
|
|
136
|
-
data.append(line)
|
|
137
134
|
|
|
138
135
|
except ConnectionRefusedError as e:
|
|
139
136
|
logger.exception(
|
|
@@ -143,7 +140,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
143
140
|
'list_data': list_data,
|
|
144
141
|
}
|
|
145
142
|
)
|
|
146
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
143
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
147
144
|
raise AdminAPIException(
|
|
148
145
|
APIError(message=msg, code='connection_refused_error'),
|
|
149
146
|
status_code=500,
|
|
@@ -171,8 +168,41 @@ class SQLAlchemyAdminListMixin:
|
|
|
171
168
|
'list_data': list_data,
|
|
172
169
|
}
|
|
173
170
|
)
|
|
171
|
+
msg = _('errors.db_error_list') % {
|
|
172
|
+
'error_type': str(e) if admin_schema.debug else type(e).__name__,
|
|
173
|
+
}
|
|
174
174
|
raise AdminAPIException(
|
|
175
|
-
APIError(message=
|
|
175
|
+
APIError(message=msg, code='db_error_list'), status_code=500,
|
|
176
176
|
) from e
|
|
177
177
|
|
|
178
|
+
try:
|
|
179
|
+
data = []
|
|
180
|
+
for record in records:
|
|
181
|
+
line = await self.table_schema.serialize(
|
|
182
|
+
record,
|
|
183
|
+
extra={"record": record, "user": user},
|
|
184
|
+
)
|
|
185
|
+
data.append(line)
|
|
186
|
+
|
|
187
|
+
except FieldError as e:
|
|
188
|
+
logger.exception(
|
|
189
|
+
'SQLAlchemy %s list %s serialize field error: %s',
|
|
190
|
+
type(self).__name__, self.model.__name__, e,
|
|
191
|
+
)
|
|
192
|
+
msg = _('serialize_error.field_error') % {
|
|
193
|
+
'error': e.message,
|
|
194
|
+
'field_slug': e.field_slug,
|
|
195
|
+
}
|
|
196
|
+
raise AdminAPIException(APIError(message=msg, code='field_error'), status_code=500) from e
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.exception(
|
|
200
|
+
'SQLAlchemy %s list %s serialize error: %s',
|
|
201
|
+
type(self).__name__, self.model.__name__, e,
|
|
202
|
+
)
|
|
203
|
+
msg = _('serialize_error.unexpected_error') % {
|
|
204
|
+
'error': str(e) if admin_schema.debug else type(e).__name__,
|
|
205
|
+
}
|
|
206
|
+
raise AdminAPIException(APIError(message=msg, code='unexpected_error'), status_code=500) from e
|
|
207
|
+
|
|
178
208
|
return schema.TableListResult(data=data, total_count=int(total_count or 0))
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
3
|
from brilliance_admin import auth, schema
|
|
4
|
-
from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
4
|
+
from brilliance_admin.exceptions import AdminAPIException, APIError, FieldError
|
|
5
|
+
from brilliance_admin.integrations.sqlalchemy.fields_schema import SQLAlchemyFieldsSchema
|
|
6
|
+
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
5
7
|
from brilliance_admin.translations import LanguageContext
|
|
6
8
|
from brilliance_admin.translations import TranslateText as _
|
|
7
9
|
from brilliance_admin.utils import get_logger
|
|
@@ -12,14 +14,17 @@ logger = get_logger()
|
|
|
12
14
|
class SQLAlchemyAdminRetrieveMixin:
|
|
13
15
|
has_retrieve: bool = True
|
|
14
16
|
|
|
17
|
+
table_schema: SQLAlchemyFieldsSchema
|
|
18
|
+
|
|
15
19
|
async def retrieve(
|
|
16
20
|
self,
|
|
17
21
|
pk: Any,
|
|
18
22
|
user: auth.UserABC,
|
|
19
23
|
language_context: LanguageContext,
|
|
24
|
+
admin_schema: AdminSchema,
|
|
20
25
|
) -> schema.RetrieveResult:
|
|
21
|
-
if not self.
|
|
22
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
26
|
+
if not self.has_retrieve:
|
|
27
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
23
28
|
|
|
24
29
|
# pylint: disable=import-outside-toplevel
|
|
25
30
|
from sqlalchemy import inspect
|
|
@@ -33,26 +38,52 @@ class SQLAlchemyAdminRetrieveMixin:
|
|
|
33
38
|
try:
|
|
34
39
|
async with self.db_async_session() as session:
|
|
35
40
|
record = (await session.execute(stmt)).scalars().first()
|
|
36
|
-
data = await self.table_schema.serialize(
|
|
37
|
-
record,
|
|
38
|
-
extra={"record": record, "user": user},
|
|
39
|
-
)
|
|
40
41
|
|
|
41
42
|
except Exception as e:
|
|
42
43
|
logger.exception(
|
|
43
|
-
'SQLAlchemy %s retrieve db error: %s',
|
|
44
|
+
'SQLAlchemy %s retrieve %s #%s db error: %s',
|
|
45
|
+
type(self).__name__, self.model.__name__, pk, e,
|
|
44
46
|
)
|
|
47
|
+
msg = _('errors.db_error_retrieve') % {
|
|
48
|
+
'error_type': str(e) if admin_schema.debug else type(e).__name__,
|
|
49
|
+
}
|
|
45
50
|
raise AdminAPIException(
|
|
46
|
-
APIError(message=
|
|
51
|
+
APIError(message=msg, code='db_error_retrieve'), status_code=500,
|
|
47
52
|
) from e
|
|
48
53
|
|
|
49
54
|
if record is None:
|
|
50
|
-
msg = _('record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
55
|
+
msg = _('errors.record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
51
56
|
raise AdminAPIException(
|
|
52
57
|
APIError(message=msg, code='record_not_found'),
|
|
53
58
|
status_code=400,
|
|
54
59
|
)
|
|
55
60
|
|
|
61
|
+
try:
|
|
62
|
+
data = await self.table_schema.serialize(
|
|
63
|
+
record,
|
|
64
|
+
extra={"record": record, "user": user},
|
|
65
|
+
)
|
|
66
|
+
except FieldError as e:
|
|
67
|
+
logger.exception(
|
|
68
|
+
'SQLAlchemy %s retrieve %s #%s serialize field error: %s',
|
|
69
|
+
type(self).__name__, self.model.__name__, pk, e,
|
|
70
|
+
)
|
|
71
|
+
msg = _('serialize_error.field_error') % {
|
|
72
|
+
'error': e.message,
|
|
73
|
+
'field_slug': e.field_slug,
|
|
74
|
+
}
|
|
75
|
+
raise AdminAPIException(APIError(message=msg, code='field_error'), status_code=500) from e
|
|
76
|
+
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.exception(
|
|
79
|
+
'SQLAlchemy %s list %s #%s serialize error: %s',
|
|
80
|
+
type(self).__name__, self.model.__name__, pk, e,
|
|
81
|
+
)
|
|
82
|
+
msg = _('serialize_error.unexpected_error') % {
|
|
83
|
+
'error': str(e) if admin_schema.debug else type(e).__name__,
|
|
84
|
+
}
|
|
85
|
+
raise AdminAPIException(APIError(message=msg, code='unexpected_error'), status_code=500) from e
|
|
86
|
+
|
|
56
87
|
logger.debug(
|
|
57
88
|
'%s model %s #%s retrieved by %s',
|
|
58
89
|
type(self).__name__, self.table_schema.model.__name__, pk, user.username,
|