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.
Files changed (41) hide show
  1. brilliance_admin/api/routers.py +2 -2
  2. brilliance_admin/api/views/autocomplete.py +1 -1
  3. brilliance_admin/api/views/{graphs.py → dashboard.py} +5 -5
  4. brilliance_admin/api/views/table.py +7 -6
  5. brilliance_admin/exceptions.py +1 -0
  6. brilliance_admin/integrations/sqlalchemy/__init__.py +1 -0
  7. brilliance_admin/integrations/sqlalchemy/auth.py +3 -4
  8. brilliance_admin/integrations/sqlalchemy/autocomplete.py +12 -3
  9. brilliance_admin/integrations/sqlalchemy/fields.py +36 -19
  10. brilliance_admin/integrations/sqlalchemy/fields_schema.py +21 -12
  11. brilliance_admin/integrations/sqlalchemy/table/base.py +4 -4
  12. brilliance_admin/integrations/sqlalchemy/table/create.py +6 -3
  13. brilliance_admin/integrations/sqlalchemy/table/delete.py +2 -2
  14. brilliance_admin/integrations/sqlalchemy/table/list.py +44 -14
  15. brilliance_admin/integrations/sqlalchemy/table/retrieve.py +41 -10
  16. brilliance_admin/integrations/sqlalchemy/table/update.py +10 -5
  17. brilliance_admin/locales/en.yml +20 -10
  18. brilliance_admin/locales/ru.yml +20 -10
  19. brilliance_admin/schema/__init__.py +3 -3
  20. brilliance_admin/schema/admin_schema.py +31 -23
  21. brilliance_admin/schema/category.py +88 -14
  22. brilliance_admin/schema/dashboard/__init__.py +1 -0
  23. brilliance_admin/schema/dashboard/category_dashboard.py +87 -0
  24. brilliance_admin/schema/table/category_table.py +13 -8
  25. brilliance_admin/schema/table/fields/base.py +102 -19
  26. brilliance_admin/schema/table/fields_schema.py +9 -2
  27. brilliance_admin/static/{index-D9axz5zK.js → index-8ahvKI6W.js} +190 -190
  28. brilliance_admin/static/{index-vlBToOhT.css → index-B8JOx1Ps.css} +1 -1
  29. brilliance_admin/templates/index.html +2 -2
  30. brilliance_admin/translations.py +6 -3
  31. brilliance_admin/utils.py +41 -3
  32. brilliance_admin-0.44.12.dist-info/METADATA +165 -0
  33. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/RECORD +36 -37
  34. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/WHEEL +1 -1
  35. brilliance_admin-0.44.12.dist-info/licenses/LICENSE +21 -0
  36. brilliance_admin/schema/graphs/__init__.py +0 -1
  37. brilliance_admin/schema/graphs/category_graphs.py +0 -51
  38. brilliance_admin/schema/group.py +0 -67
  39. brilliance_admin-0.43.1.dist-info/METADATA +0 -214
  40. brilliance_admin-0.43.1.dist-info/licenses/LICENSE +0 -17
  41. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/top_level.txt +0 -0
@@ -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.graphs import router as schema_graphs
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(schema_graphs)
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.graphs.category_graphs import CategoryGraphs, GraphData, GraphsDataResult
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="/graph", tags=["Category - Graph"])
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 graph_data(request: Request, group: str, category: str, data: GraphData) -> GraphsDataResult:
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=CategoryGraphs)
19
+ schema_category, user = await get_category(request, group, category, check_type=CategoryDashboard)
20
20
 
21
- result: GraphsDataResult = await schema_category.get_data(data, user)
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 CreateResult, ListData, RetrieveResult, TableListResult, UpdateResult
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)
@@ -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, data: AutocompleteData, user: UserABC, language_context: LanguageContext,
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
- async with self.db_async_session() as session:
36
- results = await field.autocomplete(self.model, data, user, extra={'db_session': session})
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 = 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('db_session') is None:
105
- msg = f'SQLAlchemyRelatedField.autocomplete {type(self).__name__} requires extra["db_session"] (AsyncSession)'
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
- session = extra['db_session']
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
- if hasattr(target_model, 'id'):
118
- stmt = stmt.where(getattr(target_model, 'id') == data.search_string)
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
- if existed_choices and hasattr(target_model, 'id'):
126
- stmt = stmt.where(getattr(target_model, 'id').in_(existed_choices) | expression.true())
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, 'id'), title=str(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} (many=True)')
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
- raise FieldError(f'Related field "{self.rel_name}" is missing on record (many=False)')
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
- is_mutable = isinstance(impl, Mutable)
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"] = not is_mutable
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
- attr.key: getattr(record, attr.key, None)
242
- for attr in inspect(record).mapper.column_attrs
243
- if self.get_field(attr.key)
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=_('db_error_create'), code='db_error_create'), status_code=500,
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
- raise AdminAPIException(APIError(message=_('filters_exception'), code='filters_exception'), status_code=500) from e
124
-
125
- data = []
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=_('db_error_list'), code='db_error_list'), status_code=500,
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.has_delete:
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', type(self).__name__, e,
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=_('db_error_retrieve'), code='db_error_retrieve'), status_code=500,
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,