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
@@ -1,12 +1,14 @@
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
 
7
8
  from brilliance_admin.exceptions import FieldError
8
9
  from brilliance_admin.schema.category import FieldSchemaData
9
10
  from brilliance_admin.translations import LanguageContext
11
+ from brilliance_admin.translations import TranslateText as _
10
12
  from brilliance_admin.utils import DeserializeAction, SupportsStr, humanize_field_name
11
13
 
12
14
 
@@ -52,6 +54,8 @@ class TableField(abc.ABC, FieldSchemaData):
52
54
  class IntegerField(TableField):
53
55
  _type = 'integer'
54
56
 
57
+ choices: Any | None = None
58
+
55
59
  max_value: int | None = None
56
60
  min_value: int | None = None
57
61
 
@@ -62,8 +66,6 @@ class IntegerField(TableField):
62
66
  def generate_schema(self, user, field_slug, language_context: LanguageContext) -> FieldSchemaData:
63
67
  schema = super().generate_schema(user, field_slug, language_context)
64
68
 
65
- schema.choices = self.choices
66
-
67
69
  if self.max_value is not None:
68
70
  schema.max_value = self.max_value
69
71
 
@@ -79,7 +81,7 @@ class IntegerField(TableField):
79
81
  async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
80
82
  value = await super().deserialize(value, action, extra, *args, **kwargs)
81
83
  if value and not isinstance(value, int):
82
- raise FieldError(f'bad int type: {type(value)}')
84
+ raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'init'})
83
85
 
84
86
  return value
85
87
 
@@ -95,13 +97,12 @@ class StringField(TableField):
95
97
  min_length: int | None = None
96
98
  max_length: int | None = None
97
99
 
98
- choices: List[Tuple[str, str]] | None = None
100
+ choices: Any | None = None
99
101
 
100
102
  def generate_schema(self, user, field_slug, language_context: LanguageContext) -> FieldSchemaData:
101
103
  schema = super().generate_schema(user, field_slug, language_context)
102
104
 
103
105
  schema.multilined = self.multilined
104
- schema.choices = self.choices
105
106
  schema.ckeditor = self.ckeditor
106
107
  schema.tinymce = self.tinymce
107
108
 
@@ -116,7 +117,7 @@ class StringField(TableField):
116
117
  async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
117
118
  value = await super().deserialize(value, action, extra, *args, **kwargs)
118
119
  if value and not isinstance(value, str):
119
- raise FieldError(f'bad string type: {type(value)}')
120
+ raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'string'})
120
121
 
121
122
  return value
122
123
 
@@ -126,6 +127,18 @@ class BooleanField(TableField):
126
127
  _type = 'boolean'
127
128
 
128
129
 
130
+ def _parse_iso(value: str) -> datetime.datetime:
131
+ if value.endswith('Z'):
132
+ value = value.replace('Z', '+00:00')
133
+
134
+ dt = datetime.datetime.fromisoformat(value)
135
+
136
+ if dt.tzinfo is None:
137
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
138
+
139
+ return dt
140
+
141
+
129
142
  @dataclass
130
143
  class DateTimeField(TableField):
131
144
  _type = 'datetime'
@@ -151,28 +164,40 @@ class DateTimeField(TableField):
151
164
  return
152
165
 
153
166
  if value and not isinstance(value, (str, dict)):
154
- raise FieldError(f'bad datetime type: {type(value)}')
167
+ raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'datetime'})
155
168
 
156
169
  if isinstance(value, str):
157
- return datetime.datetime.strptime(value, self.format)
170
+ return _parse_iso(value)
158
171
 
159
172
  if isinstance(value, dict):
160
173
  if not value.get('from') or not value.get('to'):
161
- msg = f'{type(self).__name__} value must be dict with from,to values: {value}'
162
- raise FieldError(msg)
174
+ raise FieldError(
175
+ f'{type(self).__name__} value must be dict with from,to values: {value}'
176
+ )
163
177
 
164
178
  return {
165
- 'from': datetime.datetime.strptime(value.get('from'), self.format),
166
- 'to': datetime.datetime.strptime(value.get('to'), self.format),
179
+ 'from': _parse_iso(value['from']),
180
+ 'to': _parse_iso(value['to']),
167
181
  }
168
182
 
169
- raise NotImplementedError(f'Value "{value}" is not supporetd for datetime')
183
+ raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'datetime'})
170
184
 
171
185
 
172
186
  @dataclass
173
187
  class JSONField(TableField):
174
188
  _type = 'json'
175
189
 
190
+ async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
191
+ value = await super().deserialize(value, action, extra, *args, **kwargs)
192
+
193
+ if value is None:
194
+ return
195
+
196
+ if not isinstance(value, (dict, list)):
197
+ raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'JSON'})
198
+
199
+ return value
200
+
176
201
 
177
202
  @dataclass
178
203
  class ArrayField(TableField):
@@ -190,8 +215,11 @@ class ArrayField(TableField):
190
215
  async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
191
216
  value = await super().deserialize(value, action, extra, *args, **kwargs)
192
217
 
193
- if value and not isinstance(value, list):
194
- raise FieldError(f'bad array type: {type(value)}')
218
+ if value is None:
219
+ return
220
+
221
+ if not isinstance(value, list):
222
+ raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'Array'})
195
223
 
196
224
  return value
197
225
 
@@ -227,23 +255,78 @@ class ImageField(TableField):
227
255
  class ChoiceField(TableField):
228
256
  _type = 'choice'
229
257
 
258
+ # Tag color available:
230
259
  # https://vuetifyjs.com/en/styles/colors/#classes
231
- tag_colors: dict | None = None
260
+ choices: Any | None = None
232
261
 
233
262
  # https://vuetifyjs.com/en/components/chips/#color-and-variants
234
263
  variant: str = 'elevated'
235
264
  size: str = 'default'
236
265
 
266
+ def __post_init__(self):
267
+ self.choices = self.generate_choices()
268
+
269
+ def generate_choices(self):
270
+ if not self.choices:
271
+ return None
272
+
273
+ if issubclass(self.choices, Enum):
274
+ return [
275
+ {'value': c.value, 'title': c.label, 'tag_color': getattr(c, 'tag_color', None)}
276
+ for c in self.choices
277
+ ]
278
+
279
+ msg = f'Field choices is not suppored: {self.choices}'
280
+ raise NotImplementedError(msg)
281
+
282
+ def find_choice(self, value):
283
+ if not self.choices:
284
+ return None
285
+
286
+ return next((c for c in self.choices if c.get('value') == value), None)
287
+
237
288
  def generate_schema(self, user, field_slug, language_context: LanguageContext) -> FieldSchemaData:
238
289
  schema = super().generate_schema(user, field_slug, language_context)
239
290
 
240
291
  schema.choices = self.choices
241
292
 
242
- schema.tag_colors = self.tag_colors
243
293
  schema.size = self.size
244
294
  schema.variant = self.variant
245
295
 
246
296
  return schema
247
297
 
248
298
  async def serialize(self, value, extra: dict, *args, **kwargs) -> Any:
249
- return {'value': value, 'title': value.capitalize() if value else value}
299
+ if not value:
300
+ return
301
+
302
+ choice = self.find_choice(value)
303
+ return {
304
+ 'value': value,
305
+ 'title': choice.get('title') or value if choice else value.capitalize(),
306
+ }
307
+
308
+ async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
309
+ value = await super().deserialize(value, action, extra, *args, **kwargs)
310
+
311
+ if value is None:
312
+ return
313
+
314
+ if isinstance(value, dict):
315
+ if 'value' not in value:
316
+ raise FieldError(
317
+ f'{type(self).__name__} dict value must contain "value": {value}'
318
+ )
319
+ value = value['value']
320
+
321
+ if not isinstance(value, str):
322
+ raise FieldError(
323
+ f'{type(self).__name__} value must be str, got {type(value)}'
324
+ )
325
+
326
+ choice = self.find_choice(value)
327
+ if not choice:
328
+ raise FieldError(
329
+ f'Invalid choice value "{value}", allowed: {[c["value"] for c in self.choices or []]}'
330
+ )
331
+
332
+ return value
@@ -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
 
@@ -158,7 +159,13 @@ class FieldsSchema:
158
159
  result = {}
159
160
  for field_slug, field in self.get_fields().items():
160
161
  value = data.get(field_slug)
161
- result[field_slug] = await field.serialize(value, extra)
162
+
163
+ try:
164
+ result[field_slug] = await field.serialize(value, extra)
165
+ except FieldError as e:
166
+ e.field_slug = field_slug
167
+ raise e
168
+
162
169
  return result
163
170
 
164
171
  async def deserialize(self, data: dict, action: DeserializeAction, extra) -> dict: