brilliance-admin 0.42.0__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 (73) hide show
  1. admin_panel/__init__.py +4 -0
  2. admin_panel/api/__init__.py +0 -0
  3. admin_panel/api/routers.py +18 -0
  4. admin_panel/api/utils.py +28 -0
  5. admin_panel/api/views/__init__.py +0 -0
  6. admin_panel/api/views/auth.py +29 -0
  7. admin_panel/api/views/autocomplete.py +33 -0
  8. admin_panel/api/views/graphs.py +30 -0
  9. admin_panel/api/views/index.py +38 -0
  10. admin_panel/api/views/schema.py +29 -0
  11. admin_panel/api/views/settings.py +29 -0
  12. admin_panel/api/views/table.py +136 -0
  13. admin_panel/auth.py +32 -0
  14. admin_panel/docs.py +37 -0
  15. admin_panel/exceptions.py +38 -0
  16. admin_panel/integrations/__init__.py +0 -0
  17. admin_panel/integrations/sqlalchemy/__init__.py +6 -0
  18. admin_panel/integrations/sqlalchemy/auth.py +144 -0
  19. admin_panel/integrations/sqlalchemy/autocomplete.py +38 -0
  20. admin_panel/integrations/sqlalchemy/fields.py +254 -0
  21. admin_panel/integrations/sqlalchemy/fields_schema.py +316 -0
  22. admin_panel/integrations/sqlalchemy/table/__init__.py +19 -0
  23. admin_panel/integrations/sqlalchemy/table/base.py +141 -0
  24. admin_panel/integrations/sqlalchemy/table/create.py +73 -0
  25. admin_panel/integrations/sqlalchemy/table/delete.py +18 -0
  26. admin_panel/integrations/sqlalchemy/table/list.py +178 -0
  27. admin_panel/integrations/sqlalchemy/table/retrieve.py +61 -0
  28. admin_panel/integrations/sqlalchemy/table/update.py +95 -0
  29. admin_panel/schema/__init__.py +7 -0
  30. admin_panel/schema/admin_schema.py +191 -0
  31. admin_panel/schema/category.py +149 -0
  32. admin_panel/schema/graphs/__init__.py +1 -0
  33. admin_panel/schema/graphs/category_graphs.py +50 -0
  34. admin_panel/schema/group.py +67 -0
  35. admin_panel/schema/table/__init__.py +8 -0
  36. admin_panel/schema/table/admin_action.py +76 -0
  37. admin_panel/schema/table/category_table.py +175 -0
  38. admin_panel/schema/table/fields/__init__.py +5 -0
  39. admin_panel/schema/table/fields/base.py +249 -0
  40. admin_panel/schema/table/fields/function_field.py +65 -0
  41. admin_panel/schema/table/fields_schema.py +216 -0
  42. admin_panel/schema/table/table_models.py +53 -0
  43. admin_panel/static/favicon.jpg +0 -0
  44. admin_panel/static/index-BeniOHDv.js +525 -0
  45. admin_panel/static/index-vlBToOhT.css +8 -0
  46. admin_panel/static/materialdesignicons-webfont-CYDMK1kx.woff2 +0 -0
  47. admin_panel/static/materialdesignicons-webfont-CgCzGbLl.woff +0 -0
  48. admin_panel/static/materialdesignicons-webfont-D3kAzl71.ttf +0 -0
  49. admin_panel/static/materialdesignicons-webfont-DttUABo4.eot +0 -0
  50. admin_panel/static/tinymce/dark-first/content.min.css +250 -0
  51. admin_panel/static/tinymce/dark-first/skin.min.css +2820 -0
  52. admin_panel/static/tinymce/dark-slim/content.min.css +249 -0
  53. admin_panel/static/tinymce/dark-slim/skin.min.css +2821 -0
  54. admin_panel/static/tinymce/img/example.png +0 -0
  55. admin_panel/static/tinymce/img/tinymce.woff2 +0 -0
  56. admin_panel/static/tinymce/lightgray/content.min.css +1 -0
  57. admin_panel/static/tinymce/lightgray/fonts/tinymce.woff +0 -0
  58. admin_panel/static/tinymce/lightgray/skin.min.css +1 -0
  59. admin_panel/static/tinymce/plugins/accordion/css/accordion.css +17 -0
  60. admin_panel/static/tinymce/plugins/accordion/plugin.js +48 -0
  61. admin_panel/static/tinymce/plugins/codesample/css/prism.css +1 -0
  62. admin_panel/static/tinymce/plugins/customLink/css/link.css +3 -0
  63. admin_panel/static/tinymce/plugins/customLink/plugin.js +147 -0
  64. admin_panel/static/tinymce/tinymce.min.js +2 -0
  65. admin_panel/static/vanilla-picker-B6E6ObS_.js +8 -0
  66. admin_panel/templates/index.html +25 -0
  67. admin_panel/translations.py +145 -0
  68. admin_panel/utils.py +50 -0
  69. brilliance_admin-0.42.0.dist-info/METADATA +155 -0
  70. brilliance_admin-0.42.0.dist-info/RECORD +73 -0
  71. brilliance_admin-0.42.0.dist-info/WHEEL +5 -0
  72. brilliance_admin-0.42.0.dist-info/licenses/LICENSE +17 -0
  73. brilliance_admin-0.42.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,175 @@
1
+ import abc
2
+ import asyncio
3
+ import copy
4
+ from typing import Awaitable, List
5
+
6
+ from fastapi import HTTPException, Request
7
+ from pydantic import Field
8
+
9
+ from admin_panel.auth import UserABC
10
+ from admin_panel.exceptions import AdminAPIException, APIError
11
+ from admin_panel.schema import Category
12
+ from admin_panel.schema.category import TableInfoSchemaData
13
+ from admin_panel.schema.table.admin_action import ActionData, ActionResult
14
+ from admin_panel.schema.table.fields_schema import FieldsSchema
15
+ from admin_panel.schema.table.table_models import AutocompleteData, AutocompleteResult, ListData, TableListResult
16
+ from admin_panel.translations import LanguageManager, TranslateText
17
+ from admin_panel.utils import DeserializeAction
18
+
19
+
20
+ class CategoryTable(Category):
21
+ _type_slug: str = 'table'
22
+
23
+ search_enabled: bool = False
24
+ search_help: str | TranslateText | None = None
25
+
26
+ table_schema: FieldsSchema = None
27
+ table_filters: FieldsSchema | None = None
28
+
29
+ ordering_fields: List[str] = Field(default_factory=list)
30
+ default_ordering: str | None = None
31
+
32
+ pk_name: str | None = None
33
+
34
+ def __init__(self, *args, table_schema=None, table_filters=None, **kwargs):
35
+ if table_schema:
36
+ self.table_schema = table_schema
37
+
38
+ if table_filters:
39
+ self.table_filters = table_filters
40
+
41
+ if self.slug is None:
42
+ msg = f'Category table attribute {type(self).__name__}.slug must be set'
43
+ raise Exception(msg)
44
+
45
+ @property
46
+ def has_retrieve(self):
47
+ if not self.pk_name:
48
+ return False
49
+
50
+ fn = getattr(self, 'retrieve', None)
51
+ return asyncio.iscoroutinefunction(fn)
52
+
53
+ @property
54
+ def has_create(self):
55
+ fn = getattr(self, 'create', None)
56
+ return asyncio.iscoroutinefunction(fn)
57
+
58
+ @property
59
+ def has_update(self):
60
+ fn = getattr(self, 'update', None)
61
+ return asyncio.iscoroutinefunction(fn)
62
+
63
+ def generate_schema(self, user, language_manager: LanguageManager) -> dict:
64
+ schema = super().generate_schema(user, language_manager)
65
+
66
+ table_schema = getattr(self, 'table_schema', None)
67
+ if not table_schema or not issubclass(table_schema.__class__, FieldsSchema):
68
+ raise AttributeError(f'Admin category {self.__class__} must have table_schema instance of FieldsSchema')
69
+
70
+ table = TableInfoSchemaData(
71
+ table_schema=self.table_schema.generate_schema(user, language_manager),
72
+ ordering_fields=self.ordering_fields,
73
+ default_ordering=self.default_ordering,
74
+
75
+ search_enabled=self.search_enabled,
76
+ search_help=language_manager.get_text(self.search_help),
77
+
78
+ pk_name=self.pk_name,
79
+ can_retrieve=self.has_retrieve,
80
+
81
+ can_create=self.has_create,
82
+ can_update=self.has_update,
83
+ )
84
+
85
+ if self.table_filters:
86
+ table.table_filters = self.table_filters.generate_schema(user, language_manager)
87
+
88
+ actions = {}
89
+ for attribute_name in dir(self):
90
+ if '__' in attribute_name:
91
+ continue
92
+
93
+ attribute = getattr(self, attribute_name)
94
+ if asyncio.iscoroutinefunction(attribute) and getattr(attribute, '__action__', False):
95
+ action = copy.copy(attribute.action_info)
96
+
97
+ action['title'] = language_manager.get_text(action.get('title'))
98
+ action['description'] = language_manager.get_text(action.get('description'))
99
+ action['confirmation_text'] = language_manager.get_text(action.get('confirmation_text'))
100
+
101
+ form_schema = action['form_schema']
102
+ if form_schema:
103
+ try:
104
+ action['form_schema'] = form_schema.generate_schema(user, language_manager)
105
+ except Exception as e:
106
+ msg = f'Action {attribute} form schema {form_schema} error: {e}'
107
+ raise Exception(msg) from e
108
+
109
+ actions[attribute_name] = action
110
+
111
+ table.actions = actions
112
+ schema.table_info = table
113
+ return schema
114
+
115
+ def _get_action_fn(self, action: str) -> Awaitable | None:
116
+ attribute = getattr(self, action)
117
+ if not asyncio.iscoroutinefunction(attribute) or not getattr(attribute, '__action__', False):
118
+ return None
119
+
120
+ return attribute
121
+
122
+ # pylint: disable=too-many-arguments
123
+ # pylint: disable=too-many-positional-arguments
124
+ async def _perform_action(
125
+ self,
126
+ request: Request,
127
+ action: str,
128
+ action_data: ActionData,
129
+ language_manager: LanguageManager,
130
+ user: UserABC,
131
+ ) -> ActionResult:
132
+ action_fn = self._get_action_fn(action)
133
+ if action_fn is None:
134
+ raise HTTPException(status_code=404, detail=f'Action "{action}" is not found')
135
+
136
+ try:
137
+ form_schema = action_fn.action_info['form_schema']
138
+ if form_schema:
139
+ deserialized_data = await form_schema.deserialize(
140
+ action_data.form_data,
141
+ action=DeserializeAction.TABLE_ACTION,
142
+ extra={'user': user, 'request': request}
143
+ )
144
+ action_data.form_data = deserialized_data
145
+
146
+ result: ActionResult = await action_fn(action_data)
147
+ except AdminAPIException as e:
148
+ raise e
149
+ except Exception as e:
150
+ raise AdminAPIException(
151
+ APIError(message=str(e), code='user_action_error'),
152
+ status_code=500,
153
+ ) from e
154
+
155
+ return result
156
+
157
+ async def autocomplete(self, data: AutocompleteData, user: UserABC) -> AutocompleteResult:
158
+ """
159
+ Retrieves list of found options to select.
160
+ """
161
+ raise NotImplementedError('autocomplete is not implemented')
162
+
163
+ # pylint: disable=too-many-arguments
164
+ @abc.abstractmethod
165
+ async def get_list(self, list_data: ListData, user: UserABC, language_manager: LanguageManager) -> TableListResult:
166
+ raise NotImplementedError()
167
+
168
+ # async def retrieve(self, pk: Any, user: UserABC) -> RetrieveResult:
169
+ # raise NotImplementedError()
170
+
171
+ # async def create(self, data: dict, user: UserABC) -> CreateResult:
172
+ # raise NotImplementedError()
173
+
174
+ # async def update(self, pk: Any, data: dict, user: UserABC) -> UpdateResult:
175
+ # raise NotImplementedError()
@@ -0,0 +1,5 @@
1
+ # pylint: disable=wildcard-import, unused-wildcard-import, unused-import
2
+ # flake8: noqa: F405
3
+ from .base import (
4
+ ArrayField, BooleanField, ChoiceField, DateTimeField, FileField, ImageField, IntegerField, JSONField, StringField)
5
+ from .function_field import FunctionField, function_field
@@ -0,0 +1,249 @@
1
+ import abc
2
+ import datetime
3
+ from typing import Any, ClassVar, List, Tuple
4
+
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from admin_panel.exceptions import FieldError
8
+ from admin_panel.schema.category import FieldSchemaData
9
+ from admin_panel.translations import LanguageManager, TranslateText
10
+ from admin_panel.utils import DeserializeAction, humanize_field_name
11
+
12
+
13
+ @dataclass
14
+ class TableField(abc.ABC, FieldSchemaData):
15
+ _type: ClassVar[str]
16
+
17
+ label: str | TranslateText | None = None
18
+ help_text: str | TranslateText | None = None
19
+
20
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
21
+ schema = FieldSchemaData(
22
+ type=self._type,
23
+ label=language_manager.get_text(self.label) or humanize_field_name(field_slug),
24
+ help_text=language_manager.get_text(self.help_text),
25
+ header=self.header,
26
+ read_only=self.read_only,
27
+ default=self.default,
28
+ required=self.required,
29
+ )
30
+
31
+ return schema
32
+
33
+ async def serialize(self, value, extra: dict, *args, **kwargs) -> Any:
34
+ return value
35
+
36
+ async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
37
+ if self.required and value is None:
38
+ raise FieldError('Field is required', 'field_required')
39
+
40
+ return value
41
+
42
+ async def autocomplete(self, model, data, user):
43
+ raise NotImplementedError('autocomplete is not implemented')
44
+
45
+ # pylint: disable=too-many-arguments
46
+ # pylint: disable=too-many-positional-arguments
47
+ def set_deserialized_value(self, result: dict, field_slug, deserialized_value, action, extra):
48
+ result[field_slug] = deserialized_value
49
+
50
+
51
+ @dataclass
52
+ class IntegerField(TableField):
53
+ _type = 'integer'
54
+
55
+ max_value: int | None = None
56
+ min_value: int | None = None
57
+
58
+ inputmode: str | None = None
59
+ precision: int | None = None
60
+ scale: int | None = None
61
+
62
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
63
+ schema = super().generate_schema(user, field_slug, language_manager)
64
+
65
+ schema.choices = self.choices
66
+
67
+ if self.max_value is not None:
68
+ schema.max_value = self.max_value
69
+
70
+ if self.min_value is not None:
71
+ schema.min_value = self.min_value
72
+
73
+ schema.inputmode = self.inputmode
74
+ schema.precision = self.precision
75
+ schema.scale = self.scale
76
+
77
+ return schema
78
+
79
+ async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
80
+ value = await super().deserialize(value, action, extra, *args, **kwargs)
81
+ if value and not isinstance(value, int):
82
+ raise FieldError(f'bad int type: {type(value)}')
83
+
84
+ return value
85
+
86
+
87
+ @dataclass
88
+ class StringField(TableField):
89
+ _type = 'string'
90
+
91
+ multilined: bool | None = None
92
+ ckeditor: bool | None = None
93
+ tinymce: bool | None = None
94
+
95
+ min_length: int | None = None
96
+ max_length: int | None = None
97
+
98
+ choices: List[Tuple[str, str]] | None = None
99
+
100
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
101
+ schema = super().generate_schema(user, field_slug, language_manager)
102
+
103
+ schema.multilined = self.multilined
104
+ schema.choices = self.choices
105
+ schema.ckeditor = self.ckeditor
106
+ schema.tinymce = self.tinymce
107
+
108
+ if self.min_length is not None:
109
+ schema.min_length = self.min_length
110
+
111
+ if self.max_length is not None:
112
+ schema.max_length = self.max_length
113
+
114
+ return schema
115
+
116
+ async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
117
+ value = await super().deserialize(value, action, extra, *args, **kwargs)
118
+ if value and not isinstance(value, str):
119
+ raise FieldError(f'bad string type: {type(value)}')
120
+
121
+ return value
122
+
123
+
124
+ @dataclass
125
+ class BooleanField(TableField):
126
+ _type = 'boolean'
127
+
128
+
129
+ @dataclass
130
+ class DateTimeField(TableField):
131
+ _type = 'datetime'
132
+
133
+ format: str = '%Y-%m-%dT%H:%M:%S'
134
+ range: bool | None = None
135
+ include_date: bool | None = True
136
+ include_time: bool | None = True
137
+
138
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
139
+ schema = super().generate_schema(user, field_slug, language_manager)
140
+
141
+ schema.range = self.range
142
+ schema.include_date = self.include_date
143
+ schema.include_time = self.include_time
144
+
145
+ return schema
146
+
147
+ async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
148
+ value = await super().deserialize(value, action, extra, *args, **kwargs)
149
+
150
+ if not value:
151
+ return
152
+
153
+ if value and not isinstance(value, (str, dict)):
154
+ raise FieldError(f'bad datetime type: {type(value)}')
155
+
156
+ if isinstance(value, str):
157
+ return datetime.datetime.strptime(value, self.format)
158
+
159
+ if isinstance(value, dict):
160
+ 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)
163
+
164
+ return {
165
+ 'from': datetime.datetime.strptime(value.get('from'), self.format),
166
+ 'to': datetime.datetime.strptime(value.get('to'), self.format),
167
+ }
168
+
169
+ raise NotImplementedError(f'Value "{value}" is not supporetd for datetime')
170
+
171
+
172
+ @dataclass
173
+ class JSONField(TableField):
174
+ _type = 'json'
175
+
176
+
177
+ @dataclass
178
+ class ArrayField(TableField):
179
+ _type = 'array'
180
+
181
+ array_type: str | None
182
+
183
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
184
+ schema = super().generate_schema(user, field_slug, language_manager)
185
+
186
+ schema.array_type = self.array_type
187
+
188
+ return schema
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 and not isinstance(value, list):
194
+ raise FieldError(f'bad array type: {type(value)}')
195
+
196
+ return value
197
+
198
+
199
+ @dataclass
200
+ class FileField(TableField):
201
+ _type = 'file'
202
+
203
+
204
+ @dataclass
205
+ class ImageField(TableField):
206
+ _type = 'image'
207
+
208
+ preview_max_height: int = 100
209
+ preview_max_width: int = 100
210
+
211
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
212
+ schema = super().generate_schema(user, field_slug, language_manager)
213
+
214
+ if self.preview_max_height is not None:
215
+ schema.preview_max_height = self.preview_max_height
216
+
217
+ if self.preview_max_width is not None:
218
+ schema.preview_max_width = self.preview_max_width
219
+
220
+ return schema
221
+
222
+ async def serialize(self, value, extra: dict, *args, **kwargs) -> Any:
223
+ return {'url': value}
224
+
225
+
226
+ @dataclass
227
+ class ChoiceField(TableField):
228
+ _type = 'choice'
229
+
230
+ # https://vuetifyjs.com/en/styles/colors/#classes
231
+ tag_colors: dict | None = None
232
+
233
+ # https://vuetifyjs.com/en/components/chips/#color-and-variants
234
+ variant: str = 'elevated'
235
+ size: str = 'default'
236
+
237
+ def generate_schema(self, user, field_slug, language_manager: LanguageManager) -> FieldSchemaData:
238
+ schema = super().generate_schema(user, field_slug, language_manager)
239
+
240
+ schema.choices = self.choices
241
+
242
+ schema.tag_colors = self.tag_colors
243
+ schema.size = self.size
244
+ schema.variant = self.variant
245
+
246
+ return schema
247
+
248
+ async def serialize(self, value, extra: dict, *args, **kwargs) -> Any:
249
+ return {'value': value, 'title': value.capitalize() if value else value}
@@ -0,0 +1,65 @@
1
+ import asyncio
2
+ import functools
3
+ from typing import Any
4
+
5
+ from pydantic.dataclasses import dataclass
6
+
7
+ from admin_panel.exceptions import AdminAPIException, APIError
8
+ from admin_panel.schema.table.fields.base import TableField
9
+ from admin_panel.utils import get_logger
10
+
11
+ logger = get_logger()
12
+
13
+
14
+ def function_field(**kwargs):
15
+ '''
16
+ The same as decaring:
17
+ field = FunctionField(fn=attribute)
18
+
19
+ but available directly and converted after to the FunctionField
20
+ '''
21
+ def wrapper(func):
22
+ func.__function_field__ = True
23
+
24
+ field_type = kwargs.pop('type', None)
25
+ if field_type:
26
+ # pylint: disable=protected-access
27
+ kwargs['_type'] = field_type._type
28
+
29
+ func.__kwargs__ = kwargs
30
+
31
+ @functools.wraps(func)
32
+ async def wrapped(*args, **kwargs):
33
+ return await func(*args, **kwargs)
34
+
35
+ return wrapped
36
+
37
+ return wrapper
38
+
39
+
40
+ @dataclass
41
+ class FunctionField(TableField):
42
+ _type: str = 'function_field'
43
+ read_only = True
44
+
45
+ fn: Any = None
46
+
47
+ def __post_init__(self):
48
+ if not asyncio.iscoroutinefunction(self.fn):
49
+ msg = f'{type(self).__name__}.fn {self.fn} must be coroutine function'
50
+ raise AttributeError(msg)
51
+
52
+ async def serialize(self, value, extra: dict, *args, **kwargs) -> Any:
53
+ try:
54
+ return await self.fn(**extra)
55
+ except Exception as e:
56
+ logger.exception(
57
+ 'Function field %s label=%s error from function="%s": %s',
58
+ type(self).__name__,
59
+ self.label,
60
+ self.fn,
61
+ e,
62
+ )
63
+ raise AdminAPIException(
64
+ APIError(message=f'Error: {e}', code='function_field_error'), status_code=400,
65
+ ) from e