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.
Files changed (26) hide show
  1. brilliance_admin/integrations/sqlalchemy/__init__.py +1 -0
  2. brilliance_admin/integrations/sqlalchemy/auth.py +2 -2
  3. brilliance_admin/integrations/sqlalchemy/autocomplete.py +6 -2
  4. brilliance_admin/integrations/sqlalchemy/fields.py +14 -10
  5. brilliance_admin/integrations/sqlalchemy/fields_schema.py +21 -12
  6. brilliance_admin/integrations/sqlalchemy/table/base.py +4 -4
  7. brilliance_admin/integrations/sqlalchemy/table/create.py +3 -3
  8. brilliance_admin/integrations/sqlalchemy/table/delete.py +1 -1
  9. brilliance_admin/integrations/sqlalchemy/table/list.py +4 -4
  10. brilliance_admin/integrations/sqlalchemy/table/retrieve.py +17 -5
  11. brilliance_admin/integrations/sqlalchemy/table/update.py +5 -5
  12. brilliance_admin/locales/en.yml +13 -10
  13. brilliance_admin/locales/ru.yml +14 -10
  14. brilliance_admin/schema/category.py +2 -3
  15. brilliance_admin/schema/group.py +2 -2
  16. brilliance_admin/schema/table/fields/base.py +37 -8
  17. brilliance_admin/schema/table/fields_schema.py +2 -1
  18. brilliance_admin/static/{index-D9axz5zK.js → index-BnnESruI.js} +130 -130
  19. brilliance_admin/templates/index.html +1 -1
  20. brilliance_admin/translations.py +3 -3
  21. brilliance_admin/utils.py +3 -3
  22. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/METADATA +18 -15
  23. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/RECORD +26 -26
  24. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/WHEEL +0 -0
  25. {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.43.7.dist-info}/licenses/LICENSE +0 -0
  26. {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
- async with self.db_async_session() as session:
36
- results = await field.autocomplete(self.model, data, user, extra={'db_session': session})
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 = 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('db_session') is None:
105
- msg = f'SQLAlchemyRelatedField.autocomplete {type(self).__name__} requires extra["db_session"] (AsyncSession)'
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
- session = extra['db_session']
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
- records = (await session.execute(stmt)).scalars().all()
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} (many=True)')
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
- raise FieldError(f'Related field "{self.rel_name}" is missing on record (many=False)')
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
- 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__,
@@ -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', type(self).__name__, e,
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(
@@ -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
- pk_not_found: 'The "%(pk_name)s" field was not found in the submitted data.'
5
- record_not_found: 'No record found for %(pk_name)s=%(pk)s.'
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
- connection_refused_error: 'Database connection error: %(error)s'
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}'
@@ -1,13 +1,20 @@
1
1
  delete: 'Удалить'
2
2
  delete_confirmation_text: "Вы уверены, что хотите удалить данные записи?\nДанное действие нельзя отменить."
3
3
  deleted_successfully: 'Записи успешно удалены.'
4
- pk_not_found: 'Поле "%(pk_name)s" не найдено среди переданных данных.'
5
- record_not_found: 'Запись по ключу %(pk_name)s=%(pk)s не найдена.'
6
- db_error_create: 'Ошибка создания записи в базе данных.'
7
- db_error_update: 'Ошибка обновления записи в базе данных.'
8
- db_error_retrieve: 'Ошибка получения записи из базы данных.'
9
- db_error_list: 'Ошибка получения данных таблицы из базы данных.'
10
- connection_refused_error: 'Ошибка подключения к базе данных: %(error)s'
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,
@@ -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 typing import Any, ClassVar, List, Tuple
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: List[Tuple[str, str]] | None = None
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
- tag_colors: dict | None = None
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
- return {'value': value, 'title': value.capitalize() if value else value}
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