brilliance-admin 0.43.1__py3-none-any.whl → 0.43.7__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/integrations/sqlalchemy/__init__.py +1 -0
- brilliance_admin/integrations/sqlalchemy/auth.py +2 -2
- brilliance_admin/integrations/sqlalchemy/autocomplete.py +6 -2
- brilliance_admin/integrations/sqlalchemy/fields.py +14 -10
- 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 +3 -3
- brilliance_admin/integrations/sqlalchemy/table/delete.py +1 -1
- brilliance_admin/integrations/sqlalchemy/table/list.py +4 -4
- brilliance_admin/integrations/sqlalchemy/table/retrieve.py +17 -5
- brilliance_admin/integrations/sqlalchemy/table/update.py +5 -5
- brilliance_admin/locales/en.yml +13 -10
- brilliance_admin/locales/ru.yml +14 -10
- brilliance_admin/schema/category.py +2 -3
- brilliance_admin/schema/group.py +2 -2
- brilliance_admin/schema/table/fields/base.py +37 -8
- brilliance_admin/schema/table/fields_schema.py +2 -1
- brilliance_admin/static/{index-D9axz5zK.js → index-BnnESruI.js} +130 -130
- brilliance_admin/templates/index.html +1 -1
- brilliance_admin/translations.py +3 -3
- brilliance_admin/utils.py +3 -3
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/METADATA +18 -15
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/RECORD +26 -26
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/WHEEL +0 -0
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/licenses/LICENSE +0 -0
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/top_level.txt +0 -0
|
@@ -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,
|
|
@@ -127,7 +127,7 @@ class SQLAlchemyJWTAdminAuthentication(AdminAuthentication):
|
|
|
127
127
|
logger.exception(
|
|
128
128
|
'SQLAlchemy %s authenticate db error: %s', type(self).__name__, e,
|
|
129
129
|
)
|
|
130
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
130
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
131
131
|
raise AdminAPIException(
|
|
132
132
|
APIError(message=msg, code='connection_refused_error'),
|
|
133
133
|
status_code=500,
|
|
@@ -32,7 +32,11 @@ class SQLAlchemyAdminAutocompleteMixin:
|
|
|
32
32
|
if not field:
|
|
33
33
|
raise Exception(f'Field "{data.field_slug}" is not found')
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
results = await field.autocomplete(
|
|
36
|
+
self.model,
|
|
37
|
+
data,
|
|
38
|
+
user,
|
|
39
|
+
extra={'db_async_session': self.db_async_session},
|
|
40
|
+
)
|
|
37
41
|
|
|
38
42
|
return AutocompleteResult(results=results)
|
|
@@ -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
|
# Откуда берётся:
|
|
@@ -101,11 +101,11 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
101
101
|
from sqlalchemy import select
|
|
102
102
|
from sqlalchemy.sql import expression
|
|
103
103
|
|
|
104
|
-
if extra is None or extra.get('
|
|
105
|
-
msg = f'SQLAlchemyRelatedField.autocomplete {type(self).__name__} requires extra["
|
|
104
|
+
if extra is None or extra.get('db_async_session') is None:
|
|
105
|
+
msg = f'SQLAlchemyRelatedField.autocomplete {type(self).__name__} requires extra["db_async_session"] (AsyncSession)'
|
|
106
106
|
raise AttributeError(msg)
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
db_async_session = extra['db_async_session']
|
|
109
109
|
|
|
110
110
|
results = []
|
|
111
111
|
|
|
@@ -125,7 +125,9 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
125
125
|
if existed_choices and hasattr(target_model, 'id'):
|
|
126
126
|
stmt = stmt.where(getattr(target_model, 'id').in_(existed_choices) | expression.true())
|
|
127
127
|
|
|
128
|
-
|
|
128
|
+
async with db_async_session() as session:
|
|
129
|
+
records = (await session.execute(stmt)).scalars().all()
|
|
130
|
+
|
|
129
131
|
for record in records:
|
|
130
132
|
results.append(Record(key=getattr(record, 'id'), title=str(record)))
|
|
131
133
|
|
|
@@ -139,20 +141,22 @@ class SQLAlchemyRelatedField(TableField):
|
|
|
139
141
|
- value всегда scalar (None или int)
|
|
140
142
|
- ORM-объект доступен через extra["record"]
|
|
141
143
|
"""
|
|
144
|
+
if not value:
|
|
145
|
+
return
|
|
146
|
+
|
|
142
147
|
record = extra.get('record')
|
|
143
148
|
if record is None:
|
|
144
149
|
raise FieldError(f'Missing record in serialize context in value: {value}')
|
|
145
150
|
|
|
151
|
+
related = getattr(record, self.rel_name, None)
|
|
152
|
+
|
|
146
153
|
if self.many:
|
|
147
|
-
related = getattr(record, self.rel_name, None)
|
|
148
154
|
if related is None:
|
|
149
|
-
raise FieldError(f'Related field "{self.rel_name}" is missing on record {record}
|
|
150
|
-
|
|
155
|
+
raise FieldError(f'Many Related field "{self.rel_name}" is missing on record "{record}"')
|
|
151
156
|
return [{'key': get_pk(obj), 'title': str(obj)} for obj in related]
|
|
152
157
|
|
|
153
|
-
related = getattr(record, self.rel_name, None)
|
|
154
158
|
if related is None:
|
|
155
|
-
|
|
159
|
+
return None
|
|
156
160
|
|
|
157
161
|
return {'key': get_pk(related), 'title': str(related)}
|
|
158
162
|
|
|
@@ -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__,
|
|
@@ -18,7 +18,7 @@ class SQLAlchemyAdminCreate:
|
|
|
18
18
|
language_context: LanguageContext,
|
|
19
19
|
) -> schema.CreateResult:
|
|
20
20
|
if not self.has_create:
|
|
21
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
21
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
22
22
|
|
|
23
23
|
# pylint: disable=import-outside-toplevel
|
|
24
24
|
from sqlalchemy.exc import IntegrityError
|
|
@@ -37,7 +37,7 @@ class SQLAlchemyAdminCreate:
|
|
|
37
37
|
type(self).__name__, self.table_schema.model.__name__, e,
|
|
38
38
|
extra={'data': data},
|
|
39
39
|
)
|
|
40
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
40
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
41
41
|
raise AdminAPIException(
|
|
42
42
|
APIError(message=msg, code='connection_refused_error'),
|
|
43
43
|
status_code=500,
|
|
@@ -62,7 +62,7 @@ class SQLAlchemyAdminCreate:
|
|
|
62
62
|
extra={'data': data},
|
|
63
63
|
)
|
|
64
64
|
raise AdminAPIException(
|
|
65
|
-
APIError(message=_('db_error_create'), code='db_error_create'), status_code=500,
|
|
65
|
+
APIError(message=_('errors.db_error_create'), code='db_error_create'), status_code=500,
|
|
66
66
|
) from e
|
|
67
67
|
|
|
68
68
|
logger.info(
|
|
@@ -14,5 +14,5 @@ class SQLAlchemyDeleteAction:
|
|
|
14
14
|
)
|
|
15
15
|
async def delete(self, action_data: ActionData):
|
|
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')))
|
|
@@ -109,7 +109,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
109
109
|
'list_data': list_data,
|
|
110
110
|
}
|
|
111
111
|
)
|
|
112
|
-
msg = _('filter_error') % {'error': e.message}
|
|
112
|
+
msg = _('errors.filter_error') % {'error': e.message}
|
|
113
113
|
raise AdminAPIException(APIError(message=msg, code='filters_exception'), status_code=500) from e
|
|
114
114
|
|
|
115
115
|
except Exception as e:
|
|
@@ -120,7 +120,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
120
120
|
'list_data': list_data,
|
|
121
121
|
}
|
|
122
122
|
)
|
|
123
|
-
raise AdminAPIException(APIError(message=_('filters_exception'), code='filters_exception'), status_code=500) from e
|
|
123
|
+
raise AdminAPIException(APIError(message=_('errors.filters_exception'), code='filters_exception'), status_code=500) from e
|
|
124
124
|
|
|
125
125
|
data = []
|
|
126
126
|
|
|
@@ -143,7 +143,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
143
143
|
'list_data': list_data,
|
|
144
144
|
}
|
|
145
145
|
)
|
|
146
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
146
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
147
147
|
raise AdminAPIException(
|
|
148
148
|
APIError(message=msg, code='connection_refused_error'),
|
|
149
149
|
status_code=500,
|
|
@@ -172,7 +172,7 @@ class SQLAlchemyAdminListMixin:
|
|
|
172
172
|
}
|
|
173
173
|
)
|
|
174
174
|
raise AdminAPIException(
|
|
175
|
-
APIError(message=_('db_error_list'), code='db_error_list'), status_code=500,
|
|
175
|
+
APIError(message=_('errors.db_error_list'), code='db_error_list'), status_code=500,
|
|
176
176
|
) from e
|
|
177
177
|
|
|
178
178
|
return schema.TableListResult(data=data, total_count=int(total_count or 0))
|
|
@@ -1,7 +1,8 @@
|
|
|
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
|
|
5
6
|
from brilliance_admin.translations import LanguageContext
|
|
6
7
|
from brilliance_admin.translations import TranslateText as _
|
|
7
8
|
from brilliance_admin.utils import get_logger
|
|
@@ -12,6 +13,8 @@ logger = get_logger()
|
|
|
12
13
|
class SQLAlchemyAdminRetrieveMixin:
|
|
13
14
|
has_retrieve: bool = True
|
|
14
15
|
|
|
16
|
+
table_schema: SQLAlchemyFieldsSchema
|
|
17
|
+
|
|
15
18
|
async def retrieve(
|
|
16
19
|
self,
|
|
17
20
|
pk: Any,
|
|
@@ -19,7 +22,7 @@ class SQLAlchemyAdminRetrieveMixin:
|
|
|
19
22
|
language_context: LanguageContext,
|
|
20
23
|
) -> schema.RetrieveResult:
|
|
21
24
|
if not self.has_delete:
|
|
22
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
25
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
23
26
|
|
|
24
27
|
# pylint: disable=import-outside-toplevel
|
|
25
28
|
from sqlalchemy import inspect
|
|
@@ -38,16 +41,25 @@ class SQLAlchemyAdminRetrieveMixin:
|
|
|
38
41
|
extra={"record": record, "user": user},
|
|
39
42
|
)
|
|
40
43
|
|
|
44
|
+
except FieldError as e:
|
|
45
|
+
logger.exception(
|
|
46
|
+
'SQLAlchemy %s retrieve %s #%s field error: %s',
|
|
47
|
+
type(self).__name__, self.model.__name__, pk, e,
|
|
48
|
+
)
|
|
49
|
+
msg = _('errors.serialize_field_error') % {'error': e.message}
|
|
50
|
+
raise AdminAPIException(APIError(message=msg, code='filters_exception'), status_code=500) from e
|
|
51
|
+
|
|
41
52
|
except Exception as e:
|
|
42
53
|
logger.exception(
|
|
43
|
-
'SQLAlchemy %s retrieve db error: %s',
|
|
54
|
+
'SQLAlchemy %s retrieve %s #%s db error: %s',
|
|
55
|
+
type(self).__name__, self.model.__name__, pk, e,
|
|
44
56
|
)
|
|
45
57
|
raise AdminAPIException(
|
|
46
|
-
APIError(message=_('db_error_retrieve'), code='db_error_retrieve'), status_code=500,
|
|
58
|
+
APIError(message=_('errors.db_error_retrieve'), code='db_error_retrieve'), status_code=500,
|
|
47
59
|
) from e
|
|
48
60
|
|
|
49
61
|
if record is None:
|
|
50
|
-
msg = _('record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
62
|
+
msg = _('errors.record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
51
63
|
raise AdminAPIException(
|
|
52
64
|
APIError(message=msg, code='record_not_found'),
|
|
53
65
|
status_code=400,
|
|
@@ -21,7 +21,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
21
21
|
language_context: LanguageContext,
|
|
22
22
|
) -> schema.UpdateResult:
|
|
23
23
|
if not self.has_update:
|
|
24
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
24
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
25
25
|
|
|
26
26
|
# pylint: disable=import-outside-toplevel
|
|
27
27
|
from sqlalchemy import inspect
|
|
@@ -29,7 +29,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
29
29
|
|
|
30
30
|
if pk is None:
|
|
31
31
|
raise AdminAPIException(
|
|
32
|
-
APIError(message=_('pk_not_found') % {'pk_name': self.pk_name}, code='pk_not_found'),
|
|
32
|
+
APIError(message=_('errors.pk_not_found') % {'pk_name': self.pk_name}, code='pk_not_found'),
|
|
33
33
|
status_code=400,
|
|
34
34
|
)
|
|
35
35
|
|
|
@@ -42,7 +42,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
42
42
|
async with self.db_async_session() as session:
|
|
43
43
|
record = (await session.execute(stmt)).scalars().first()
|
|
44
44
|
if record is None:
|
|
45
|
-
msg = _('record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
45
|
+
msg = _('errors.record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
46
46
|
raise AdminAPIException(
|
|
47
47
|
APIError(message=msg, code='record_not_found'),
|
|
48
48
|
status_code=400,
|
|
@@ -59,7 +59,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
59
59
|
type(self).__name__, self.table_schema.model.__name__, pk, e,
|
|
60
60
|
extra={'data': data},
|
|
61
61
|
)
|
|
62
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
62
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
63
63
|
raise AdminAPIException(
|
|
64
64
|
APIError(message=msg, code='connection_refused_error'),
|
|
65
65
|
status_code=400,
|
|
@@ -84,7 +84,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
84
84
|
extra={'data': data}
|
|
85
85
|
)
|
|
86
86
|
raise AdminAPIException(
|
|
87
|
-
APIError(message=_('db_error_update'), code='db_error_update'), status_code=500,
|
|
87
|
+
APIError(message=_('errors.db_error_update'), code='db_error_update'), status_code=500,
|
|
88
88
|
) from e
|
|
89
89
|
|
|
90
90
|
logger.info(
|
brilliance_admin/locales/en.yml
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
delete: 'Delete'
|
|
2
2
|
delete_confirmation_text: "Are you sure you want to delete those records?\nThis action cannot be undone."
|
|
3
3
|
deleted_successfully: 'The entries were successfully deleted.'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
db_error_create: 'Error creating a record in the database.'
|
|
7
|
-
db_error_update: 'Error updating the record in the database.'
|
|
8
|
-
db_error_retrieve: 'Error retrieving the record from the database.'
|
|
9
|
-
db_error_list: 'Failed to retrieve table data from the database.'
|
|
10
|
-
|
|
4
|
+
|
|
5
|
+
errors:
|
|
6
|
+
db_error_create: 'Error creating a record in the database.'
|
|
7
|
+
db_error_update: 'Error updating the record in the database.'
|
|
8
|
+
db_error_retrieve: 'Error retrieving the record from the database.'
|
|
9
|
+
db_error_list: 'Failed to retrieve table data from the database.'
|
|
10
|
+
pk_not_found: 'The "%(pk_name)s" field was not found in the submitted data.'
|
|
11
|
+
record_not_found: 'No record found for %(pk_name)s=%(pk)s.'
|
|
12
|
+
connection_refused_error: 'Database connection error: %(error)s'
|
|
13
|
+
filters_exception: 'An unknown technical error occurred while filtering data.'
|
|
14
|
+
method_not_allowed: 'Error, method not allowed. This action is not permitted.'
|
|
15
|
+
filter_error: 'An error occurred during filtering: {error}'
|
|
16
|
+
|
|
11
17
|
search_help: 'Available search fields: %(fields)s'
|
|
12
18
|
sqlalchemy_search_help: |
|
|
13
19
|
<b>Available search fields:</b>
|
|
@@ -17,6 +23,3 @@ sqlalchemy_search_help: |
|
|
|
17
23
|
<b>""</b> - quotes for exact match
|
|
18
24
|
<b>%%</b> - any sequence of characters
|
|
19
25
|
<b>_</b> - any single character
|
|
20
|
-
filters_exception: 'An unknown technical error occurred while filtering data.'
|
|
21
|
-
method_not_allowed: 'Error, method not allowed. This action is not permitted.'
|
|
22
|
-
filter_error: 'An error occurred during filtering: {error}'
|
brilliance_admin/locales/ru.yml
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
delete: 'Удалить'
|
|
2
2
|
delete_confirmation_text: "Вы уверены, что хотите удалить данные записи?\nДанное действие нельзя отменить."
|
|
3
3
|
deleted_successfully: 'Записи успешно удалены.'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
|
|
5
|
+
errors:
|
|
6
|
+
pk_not_found: 'Поле "%(pk_name)s" не найдено среди переданных данных.'
|
|
7
|
+
record_not_found: 'Запись по ключу %(pk_name)s=%(pk)s не найдена.'
|
|
8
|
+
db_error_create: 'Ошибка создания записи в базе данных.'
|
|
9
|
+
db_error_update: 'Ошибка обновления записи в базе данных.'
|
|
10
|
+
db_error_retrieve: 'Ошибка получения записи из базы данных.'
|
|
11
|
+
db_error_list: 'Ошибка получения данных таблицы из базы данных.'
|
|
12
|
+
connection_refused_error: 'Ошибка подключения к базе данных: %(error)s'
|
|
13
|
+
filters_exception: 'Произошла неизвестная техническая ошибка при фильтрации данных.'
|
|
14
|
+
method_not_allowed: 'Ошибка, данный метод недоступен.'
|
|
15
|
+
filter_error: 'Проишла ошибка при фильтрации: %(error)s'
|
|
16
|
+
serialize_field_error: 'Ошибка чтения данных: %(error)s'
|
|
17
|
+
|
|
11
18
|
search_help: 'Доступные поля для поиска: %(fields)s'
|
|
12
19
|
sqlalchemy_search_help: |
|
|
13
20
|
<b>Доступные поля для поиска:</b>
|
|
@@ -17,6 +24,3 @@ sqlalchemy_search_help: |
|
|
|
17
24
|
<b>""</b> - кавычки для точного совпадения
|
|
18
25
|
<b>%%</b> - любая последовательность символов
|
|
19
26
|
<b>_</b> - один любой символ
|
|
20
|
-
filters_exception: 'Произошла неизвестная техническая ошибка при фильтрации данных.'
|
|
21
|
-
method_not_allowed: 'Ошибка, данный метод недоступен.'
|
|
22
|
-
filter_error: 'Проишла ошибка при фильтрации: {error}'
|
|
@@ -7,7 +7,7 @@ from pydantic_core import core_schema
|
|
|
7
7
|
|
|
8
8
|
from brilliance_admin.auth import UserABC
|
|
9
9
|
from brilliance_admin.translations import LanguageContext
|
|
10
|
-
from brilliance_admin.utils import DataclassBase, SupportsStr
|
|
10
|
+
from brilliance_admin.utils import DataclassBase, SupportsStr, humanize_field_name
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
# pylint: disable=too-many-instance-attributes
|
|
@@ -30,7 +30,6 @@ class FieldSchemaData(DataclassBase):
|
|
|
30
30
|
|
|
31
31
|
choices: List[dict] | None = None
|
|
32
32
|
|
|
33
|
-
tag_colors: dict | None = None
|
|
34
33
|
variant: str | None = None
|
|
35
34
|
size: str | None = None
|
|
36
35
|
|
|
@@ -126,7 +125,7 @@ class Category(abc.ABC):
|
|
|
126
125
|
|
|
127
126
|
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
128
127
|
return CategorySchemaData(
|
|
129
|
-
title=language_context.get_text(self.title) or self.slug,
|
|
128
|
+
title=language_context.get_text(self.title) or humanize_field_name(self.slug),
|
|
130
129
|
description=language_context.get_text(self.description),
|
|
131
130
|
icon=self.icon,
|
|
132
131
|
type=self._type_slug,
|
brilliance_admin/schema/group.py
CHANGED
|
@@ -6,7 +6,7 @@ from pydantic.dataclasses import dataclass
|
|
|
6
6
|
from brilliance_admin.auth import UserABC
|
|
7
7
|
from brilliance_admin.schema.category import Category, CategorySchemaData
|
|
8
8
|
from brilliance_admin.translations import LanguageContext
|
|
9
|
-
from brilliance_admin.utils import DataclassBase, SupportsStr, get_logger
|
|
9
|
+
from brilliance_admin.utils import DataclassBase, SupportsStr, get_logger, humanize_field_name
|
|
10
10
|
|
|
11
11
|
logger = get_logger()
|
|
12
12
|
|
|
@@ -36,7 +36,7 @@ class Group(abc.ABC):
|
|
|
36
36
|
|
|
37
37
|
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> GroupSchemaData:
|
|
38
38
|
result = GroupSchemaData(
|
|
39
|
-
title=language_context.get_text(self.title) or self.slug,
|
|
39
|
+
title=language_context.get_text(self.title) or humanize_field_name(self.slug),
|
|
40
40
|
description=language_context.get_text(self.description),
|
|
41
41
|
icon=self.icon,
|
|
42
42
|
categories={},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import datetime
|
|
3
|
-
from
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, ClassVar
|
|
4
5
|
|
|
5
6
|
from pydantic.dataclasses import dataclass
|
|
6
7
|
|
|
@@ -52,6 +53,8 @@ class TableField(abc.ABC, FieldSchemaData):
|
|
|
52
53
|
class IntegerField(TableField):
|
|
53
54
|
_type = 'integer'
|
|
54
55
|
|
|
56
|
+
choices: Any | None = None
|
|
57
|
+
|
|
55
58
|
max_value: int | None = None
|
|
56
59
|
min_value: int | None = None
|
|
57
60
|
|
|
@@ -62,8 +65,6 @@ class IntegerField(TableField):
|
|
|
62
65
|
def generate_schema(self, user, field_slug, language_context: LanguageContext) -> FieldSchemaData:
|
|
63
66
|
schema = super().generate_schema(user, field_slug, language_context)
|
|
64
67
|
|
|
65
|
-
schema.choices = self.choices
|
|
66
|
-
|
|
67
68
|
if self.max_value is not None:
|
|
68
69
|
schema.max_value = self.max_value
|
|
69
70
|
|
|
@@ -95,13 +96,12 @@ class StringField(TableField):
|
|
|
95
96
|
min_length: int | None = None
|
|
96
97
|
max_length: int | None = None
|
|
97
98
|
|
|
98
|
-
choices:
|
|
99
|
+
choices: Any | None = None
|
|
99
100
|
|
|
100
101
|
def generate_schema(self, user, field_slug, language_context: LanguageContext) -> FieldSchemaData:
|
|
101
102
|
schema = super().generate_schema(user, field_slug, language_context)
|
|
102
103
|
|
|
103
104
|
schema.multilined = self.multilined
|
|
104
|
-
schema.choices = self.choices
|
|
105
105
|
schema.ckeditor = self.ckeditor
|
|
106
106
|
schema.tinymce = self.tinymce
|
|
107
107
|
|
|
@@ -227,23 +227,52 @@ class ImageField(TableField):
|
|
|
227
227
|
class ChoiceField(TableField):
|
|
228
228
|
_type = 'choice'
|
|
229
229
|
|
|
230
|
+
# Tag color available:
|
|
230
231
|
# https://vuetifyjs.com/en/styles/colors/#classes
|
|
231
|
-
|
|
232
|
+
choices: Any | None = None
|
|
232
233
|
|
|
233
234
|
# https://vuetifyjs.com/en/components/chips/#color-and-variants
|
|
234
235
|
variant: str = 'elevated'
|
|
235
236
|
size: str = 'default'
|
|
236
237
|
|
|
238
|
+
def __post_init__(self):
|
|
239
|
+
self.choices = self.generate_choices()
|
|
240
|
+
|
|
241
|
+
def generate_choices(self):
|
|
242
|
+
if not self.choices:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if issubclass(self.choices, Enum):
|
|
246
|
+
return [
|
|
247
|
+
{'value': c.value, 'title': c.label, 'tag_color': getattr(c, 'tag_color', None)}
|
|
248
|
+
for c in self.choices
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
msg = f'Field choices is not suppored: {self.choices}'
|
|
252
|
+
raise NotImplementedError(msg)
|
|
253
|
+
|
|
254
|
+
def find_choice(self, value):
|
|
255
|
+
if not self.choices:
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
return next((c for c in self.choices if c.get('value') == value), None)
|
|
259
|
+
|
|
237
260
|
def generate_schema(self, user, field_slug, language_context: LanguageContext) -> FieldSchemaData:
|
|
238
261
|
schema = super().generate_schema(user, field_slug, language_context)
|
|
239
262
|
|
|
240
263
|
schema.choices = self.choices
|
|
241
264
|
|
|
242
|
-
schema.tag_colors = self.tag_colors
|
|
243
265
|
schema.size = self.size
|
|
244
266
|
schema.variant = self.variant
|
|
245
267
|
|
|
246
268
|
return schema
|
|
247
269
|
|
|
248
270
|
async def serialize(self, value, extra: dict, *args, **kwargs) -> Any:
|
|
249
|
-
|
|
271
|
+
if not value:
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
choice = self.find_choice(value)
|
|
275
|
+
return {
|
|
276
|
+
'value': value,
|
|
277
|
+
'title': choice.get('title') or value if choice else value.capitalize(),
|
|
278
|
+
}
|
|
@@ -148,9 +148,10 @@ class FieldsSchema:
|
|
|
148
148
|
list_display=self.list_display,
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
+
context = {'language_context': language_context}
|
|
151
152
|
for field_slug, field in self.get_fields().items():
|
|
152
153
|
field_schema: FieldSchemaData = field.generate_schema(user, field_slug, language_context)
|
|
153
|
-
fields_schema.fields[field_slug] = field_schema.to_dict(keep_none=False)
|
|
154
|
+
fields_schema.fields[field_slug] = field_schema.to_dict(keep_none=False, context=context)
|
|
154
155
|
|
|
155
156
|
return fields_schema
|
|
156
157
|
|