brilliance-admin 0.43.7__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.
- brilliance_admin/api/routers.py +2 -2
- brilliance_admin/api/views/autocomplete.py +1 -1
- brilliance_admin/api/views/{graphs.py → dashboard.py} +5 -5
- brilliance_admin/api/views/table.py +7 -6
- brilliance_admin/exceptions.py +1 -0
- brilliance_admin/integrations/sqlalchemy/auth.py +1 -2
- brilliance_admin/integrations/sqlalchemy/autocomplete.py +6 -1
- brilliance_admin/integrations/sqlalchemy/fields.py +22 -9
- brilliance_admin/integrations/sqlalchemy/table/create.py +4 -1
- brilliance_admin/integrations/sqlalchemy/table/delete.py +1 -1
- brilliance_admin/integrations/sqlalchemy/table/list.py +42 -12
- brilliance_admin/integrations/sqlalchemy/table/retrieve.py +33 -14
- brilliance_admin/integrations/sqlalchemy/table/update.py +6 -1
- brilliance_admin/locales/en.yml +12 -5
- brilliance_admin/locales/ru.yml +12 -6
- brilliance_admin/schema/__init__.py +3 -3
- brilliance_admin/schema/admin_schema.py +31 -23
- brilliance_admin/schema/category.py +87 -12
- brilliance_admin/schema/dashboard/__init__.py +1 -0
- brilliance_admin/schema/dashboard/category_dashboard.py +87 -0
- brilliance_admin/schema/table/category_table.py +13 -8
- brilliance_admin/schema/table/fields/base.py +65 -11
- brilliance_admin/schema/table/fields_schema.py +7 -1
- brilliance_admin/static/{index-BnnESruI.js → index-8ahvKI6W.js} +184 -184
- brilliance_admin/static/{index-vlBToOhT.css → index-B8JOx1Ps.css} +1 -1
- brilliance_admin/templates/index.html +2 -2
- brilliance_admin/translations.py +3 -0
- brilliance_admin/utils.py +38 -0
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.12.dist-info}/METADATA +56 -108
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.12.dist-info}/RECORD +33 -34
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.12.dist-info}/WHEEL +1 -1
- brilliance_admin-0.44.12.dist-info/licenses/LICENSE +21 -0
- brilliance_admin/schema/graphs/__init__.py +0 -1
- brilliance_admin/schema/graphs/category_graphs.py +0 -51
- brilliance_admin/schema/group.py +0 -67
- brilliance_admin-0.43.7.dist-info/licenses/LICENSE +0 -17
- {brilliance_admin-0.43.7.dist-info → brilliance_admin-0.44.12.dist-info}/top_level.txt +0 -0
|
@@ -7,11 +7,12 @@ from urllib.parse import urljoin
|
|
|
7
7
|
from fastapi import FastAPI, Request
|
|
8
8
|
from fastapi.middleware.cors import CORSMiddleware
|
|
9
9
|
from fastapi.staticfiles import StaticFiles
|
|
10
|
+
from pydantic import Field
|
|
10
11
|
from pydantic.dataclasses import dataclass
|
|
11
12
|
|
|
12
13
|
from brilliance_admin.auth import UserABC
|
|
13
14
|
from brilliance_admin.docs import build_redoc_docs, build_scalar_docs
|
|
14
|
-
from brilliance_admin.schema.
|
|
15
|
+
from brilliance_admin.schema.category import BaseCategory, CategorySchemaData
|
|
15
16
|
from brilliance_admin.translations import LanguageContext, LanguageManager
|
|
16
17
|
from brilliance_admin.utils import DataclassBase, SupportsStr
|
|
17
18
|
|
|
@@ -23,8 +24,8 @@ DEFAULT_LANGUAGES = {
|
|
|
23
24
|
|
|
24
25
|
@dataclass
|
|
25
26
|
class AdminSchemaData(DataclassBase):
|
|
26
|
-
groups: Dict[str, GroupSchemaData]
|
|
27
27
|
profile: UserABC | Any
|
|
28
|
+
categories: Dict[str, CategorySchemaData] = Field(default_factory=dict)
|
|
28
29
|
|
|
29
30
|
def __post_init__(self):
|
|
30
31
|
if not isinstance(self.profile, UserABC):
|
|
@@ -39,6 +40,7 @@ class AdminSettingsData(DataclassBase):
|
|
|
39
40
|
login_greetings_message: SupportsStr | None
|
|
40
41
|
navbar_density: str
|
|
41
42
|
languages: Dict[str, str] | None
|
|
43
|
+
main_page: str | None = None
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
@dataclass
|
|
@@ -50,9 +52,11 @@ class AdminIndexContextData(DataclassBase):
|
|
|
50
52
|
|
|
51
53
|
@dataclass
|
|
52
54
|
class AdminSchema:
|
|
53
|
-
|
|
55
|
+
categories: List[BaseCategory]
|
|
54
56
|
auth: Any
|
|
55
57
|
|
|
58
|
+
main_page: str | None = None
|
|
59
|
+
|
|
56
60
|
title: SupportsStr | None = 'Admin'
|
|
57
61
|
description: SupportsStr | None = None
|
|
58
62
|
login_greetings_message: SupportsStr | None = None
|
|
@@ -62,15 +66,17 @@ class AdminSchema:
|
|
|
62
66
|
|
|
63
67
|
navbar_density: str = 'default'
|
|
64
68
|
|
|
65
|
-
backend_prefix = None
|
|
66
|
-
static_prefix = None
|
|
69
|
+
backend_prefix: str | None = None
|
|
70
|
+
static_prefix: str | None = None
|
|
67
71
|
|
|
68
72
|
language_manager: LanguageManager | None = None
|
|
69
73
|
|
|
74
|
+
debug: bool = False
|
|
75
|
+
|
|
70
76
|
def __post_init__(self):
|
|
71
|
-
for
|
|
72
|
-
if not issubclass(
|
|
73
|
-
raise TypeError(f'
|
|
77
|
+
for category in self.categories:
|
|
78
|
+
if not issubclass(category.__class__, BaseCategory):
|
|
79
|
+
raise TypeError(f'Root category "{category}" is not instance of BaseCategory subclass')
|
|
74
80
|
|
|
75
81
|
if not self.language_manager:
|
|
76
82
|
self.language_manager = LanguageManager(DEFAULT_LANGUAGES)
|
|
@@ -81,24 +87,25 @@ class AdminSchema:
|
|
|
81
87
|
def generate_schema(self, user: UserABC, language_slug: str | None) -> AdminSchemaData:
|
|
82
88
|
language_context: LanguageContext = self.get_language_context(language_slug)
|
|
83
89
|
|
|
84
|
-
|
|
90
|
+
result = AdminSchemaData(profile=user)
|
|
85
91
|
|
|
86
|
-
for
|
|
87
|
-
if not
|
|
88
|
-
msg = f'Category
|
|
92
|
+
for category in self.categories:
|
|
93
|
+
if not category.slug:
|
|
94
|
+
msg = f'Category {type(category).__name__}.slug is empty'
|
|
89
95
|
raise AttributeError(msg)
|
|
90
96
|
|
|
91
|
-
|
|
97
|
+
try:
|
|
98
|
+
result.categories[category.slug] = category.generate_schema(user, language_context).to_dict(keep_none=False)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
msg = f'Root category "{category.slug}" generate_schema error: {e}'
|
|
101
|
+
raise Exception(msg) from e
|
|
92
102
|
|
|
93
|
-
return
|
|
94
|
-
groups=groups,
|
|
95
|
-
profile=user,
|
|
96
|
-
)
|
|
103
|
+
return result
|
|
97
104
|
|
|
98
|
-
def get_group(self, group_slug: str) -> Optional[
|
|
99
|
-
for
|
|
100
|
-
if
|
|
101
|
-
return
|
|
105
|
+
def get_group(self, group_slug: str) -> Optional[BaseCategory]:
|
|
106
|
+
for category in self.categories:
|
|
107
|
+
if category.slug == group_slug:
|
|
108
|
+
return category
|
|
102
109
|
|
|
103
110
|
return None
|
|
104
111
|
|
|
@@ -114,14 +121,13 @@ class AdminSchema:
|
|
|
114
121
|
|
|
115
122
|
return AdminSettingsData(
|
|
116
123
|
title=self.title,
|
|
124
|
+
main_page=self.main_page,
|
|
117
125
|
description=self.description,
|
|
118
126
|
login_greetings_message=self.login_greetings_message,
|
|
119
127
|
navbar_density=self.navbar_density,
|
|
120
128
|
languages=languages,
|
|
121
129
|
)
|
|
122
130
|
|
|
123
|
-
# pylint: disable=too-many-arguments
|
|
124
|
-
# pylint: disable=too-many-positional-arguments
|
|
125
131
|
def generate_app(
|
|
126
132
|
self,
|
|
127
133
|
debug=False,
|
|
@@ -131,6 +137,8 @@ class AdminSchema:
|
|
|
131
137
|
include_docs=False,
|
|
132
138
|
include_redoc=False,
|
|
133
139
|
) -> FastAPI:
|
|
140
|
+
self.debug = debug
|
|
141
|
+
|
|
134
142
|
# pylint: disable=unused-variable
|
|
135
143
|
language_context = self.get_language_context(language_slug=None)
|
|
136
144
|
|
|
@@ -4,10 +4,13 @@ from typing import Any, ClassVar, Dict, List
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
from pydantic.dataclasses import dataclass
|
|
6
6
|
from pydantic_core import core_schema
|
|
7
|
+
from structlog import get_logger
|
|
7
8
|
|
|
8
9
|
from brilliance_admin.auth import UserABC
|
|
9
10
|
from brilliance_admin.translations import LanguageContext
|
|
10
|
-
from brilliance_admin.utils import DataclassBase, SupportsStr, humanize_field_name
|
|
11
|
+
from brilliance_admin.utils import DataclassBase, KwargsInitMixin, SupportsStr, humanize_field_name
|
|
12
|
+
|
|
13
|
+
logger = get_logger()
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
# pylint: disable=too-many-instance-attributes
|
|
@@ -92,7 +95,7 @@ class TableInfoSchemaData(DataclassBase):
|
|
|
92
95
|
|
|
93
96
|
|
|
94
97
|
@dataclass
|
|
95
|
-
class
|
|
98
|
+
class DashboardInfoSchemaData(DataclassBase):
|
|
96
99
|
search_enabled: bool
|
|
97
100
|
search_help: str | None
|
|
98
101
|
|
|
@@ -106,37 +109,56 @@ class CategorySchemaData(DataclassBase):
|
|
|
106
109
|
icon: str | None
|
|
107
110
|
type: str
|
|
108
111
|
|
|
112
|
+
categories: dict = Field(default_factory=dict)
|
|
113
|
+
|
|
109
114
|
table_info: TableInfoSchemaData | None = None
|
|
110
|
-
|
|
115
|
+
dashboard_info: DashboardInfoSchemaData | None = None
|
|
116
|
+
|
|
117
|
+
link: str | None = None
|
|
111
118
|
|
|
112
119
|
def __repr__(self):
|
|
113
120
|
return f'<CategorySchemaData type={self.type} "{self.title}">'
|
|
114
121
|
|
|
115
122
|
|
|
116
|
-
class
|
|
117
|
-
slug:
|
|
118
|
-
title:
|
|
119
|
-
description:
|
|
123
|
+
class BaseCategory(KwargsInitMixin, abc.ABC):
|
|
124
|
+
slug: str
|
|
125
|
+
title: SupportsStr | None = None
|
|
126
|
+
description: SupportsStr | None = None
|
|
120
127
|
|
|
121
128
|
# https://pictogrammers.com/library/mdi/
|
|
122
|
-
icon:
|
|
129
|
+
icon: str | None = None
|
|
123
130
|
|
|
124
131
|
_type_slug: ClassVar[str]
|
|
125
132
|
|
|
126
133
|
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
127
|
-
|
|
134
|
+
type_slug = getattr(type(self), '_type_slug', None)
|
|
135
|
+
if not type_slug:
|
|
136
|
+
msg = f'{type(self).__name__}._type_slug must be set!'
|
|
137
|
+
raise AttributeError(msg)
|
|
138
|
+
|
|
139
|
+
result = CategorySchemaData(
|
|
128
140
|
title=language_context.get_text(self.title) or humanize_field_name(self.slug),
|
|
129
141
|
description=language_context.get_text(self.description),
|
|
130
142
|
icon=self.icon,
|
|
131
|
-
type=
|
|
143
|
+
type=type_slug,
|
|
132
144
|
)
|
|
145
|
+
return result
|
|
146
|
+
|
|
147
|
+
def __init_subclass__(cls, **kwargs):
|
|
148
|
+
super().__init_subclass__(**kwargs)
|
|
149
|
+
|
|
150
|
+
if cls is BaseCategory:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
if not issubclass(cls, BaseCategory):
|
|
154
|
+
raise TypeError(f'{cls.__name__} must inherit from Category')
|
|
133
155
|
|
|
134
156
|
@classmethod
|
|
135
157
|
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema:
|
|
136
|
-
def validate(v: Any) -> "
|
|
158
|
+
def validate(v: Any) -> "BaseCategory":
|
|
137
159
|
if isinstance(v, cls):
|
|
138
160
|
return v
|
|
139
|
-
raise TypeError(f"Expected {cls.__name__} instance")
|
|
161
|
+
raise TypeError(f"Expected {cls.__name__} instance, recieved: {type(v)} {v}")
|
|
140
162
|
|
|
141
163
|
return core_schema.no_info_plain_validator_function(
|
|
142
164
|
validate,
|
|
@@ -146,3 +168,56 @@ class Category(abc.ABC):
|
|
|
146
168
|
return_schema=core_schema.str_schema(),
|
|
147
169
|
),
|
|
148
170
|
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class CategoryLink(BaseCategory):
|
|
174
|
+
_type_slug: str = 'link'
|
|
175
|
+
|
|
176
|
+
link: str
|
|
177
|
+
|
|
178
|
+
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
179
|
+
result = super().generate_schema(user, language_context)
|
|
180
|
+
result.link = self.link
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CategoryGroup(BaseCategory):
|
|
185
|
+
_type_slug: str = 'group'
|
|
186
|
+
|
|
187
|
+
subcategories: list = Field(default_factory=list)
|
|
188
|
+
|
|
189
|
+
def __init__(self, *args, **kwargs):
|
|
190
|
+
super().__init__(*args, **kwargs)
|
|
191
|
+
|
|
192
|
+
for category in self.subcategories:
|
|
193
|
+
if not isinstance(category, BaseCategory):
|
|
194
|
+
raise TypeError(f'Category "{category}" is not instance of BaseCategory subclass')
|
|
195
|
+
|
|
196
|
+
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
197
|
+
result = super().generate_schema(user, language_context)
|
|
198
|
+
|
|
199
|
+
for category in self.subcategories:
|
|
200
|
+
|
|
201
|
+
if not category.slug:
|
|
202
|
+
msg = f'Category {type(category).__name__}.slug is empty'
|
|
203
|
+
raise AttributeError(msg)
|
|
204
|
+
|
|
205
|
+
if category.slug in result.categories:
|
|
206
|
+
exists = result.categories[category.slug]
|
|
207
|
+
msg = f'Category {type(category).__name__}.slug "{self.slug}" already registered by "{exists.title}"'
|
|
208
|
+
raise KeyError(msg)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
result.categories[category.slug] = category.generate_schema(user, language_context)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
msg = f'Category "{category.slug}" {type(category)} generate_schema error: {e}'
|
|
214
|
+
raise Exception(msg) from e
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
def get_category(self, category_slug: str):
|
|
219
|
+
for category in self.subcategories:
|
|
220
|
+
if category.slug == category_slug:
|
|
221
|
+
return category
|
|
222
|
+
|
|
223
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .category_dashboard import CategoryDashboard
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field, field_serializer
|
|
4
|
+
|
|
5
|
+
from brilliance_admin.schema.category import BaseCategory, DashboardInfoSchemaData
|
|
6
|
+
from brilliance_admin.schema.table.fields_schema import FieldsSchema
|
|
7
|
+
from brilliance_admin.translations import LanguageContext
|
|
8
|
+
from brilliance_admin.utils import SupportsStr
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DashboardData(BaseModel):
|
|
12
|
+
search: str | None = None
|
|
13
|
+
filters: Dict[str, Any] = Field(default_factory=dict)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChartData(BaseModel):
|
|
17
|
+
data: dict
|
|
18
|
+
options: dict
|
|
19
|
+
width: int | None = None
|
|
20
|
+
height: int = 50
|
|
21
|
+
type: str = 'line'
|
|
22
|
+
|
|
23
|
+
component_type: str = 'chart'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Subcard(BaseModel):
|
|
27
|
+
title: SupportsStr
|
|
28
|
+
value: SupportsStr
|
|
29
|
+
color: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PeriodGraph(BaseModel):
|
|
33
|
+
title: SupportsStr
|
|
34
|
+
value: SupportsStr
|
|
35
|
+
change: int | float | None = None
|
|
36
|
+
subcards: List[Subcard] = Field(default_factory=list)
|
|
37
|
+
values: List[List[int | float]] = Field(default_factory=list)
|
|
38
|
+
vertical: List[SupportsStr] = Field(default_factory=list)
|
|
39
|
+
horizontal: List[SupportsStr] = Field(default_factory=list)
|
|
40
|
+
component_type: str = 'period_graph'
|
|
41
|
+
|
|
42
|
+
@field_serializer('horizontal', 'vertical')
|
|
43
|
+
def serialize_str_list(self, val: list) -> list:
|
|
44
|
+
return [str(v) for v in val]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SmallGraph(BaseModel):
|
|
48
|
+
title: SupportsStr
|
|
49
|
+
value: SupportsStr
|
|
50
|
+
change: int | float | None = None
|
|
51
|
+
points: Dict[SupportsStr, float | int] = Field(default_factory=list)
|
|
52
|
+
component_type: str = 'small_graph'
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class DashboardContainer(BaseModel):
|
|
56
|
+
cols: int | None = None
|
|
57
|
+
md: int | None = None
|
|
58
|
+
lg: int | None = None
|
|
59
|
+
sm: int | None = None
|
|
60
|
+
|
|
61
|
+
component_type: str = 'container'
|
|
62
|
+
components: List[Any] = Field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CategoryDashboard(BaseCategory):
|
|
66
|
+
_type_slug: str = 'dashboard'
|
|
67
|
+
|
|
68
|
+
search_enabled: bool = False
|
|
69
|
+
search_help: SupportsStr | None = None
|
|
70
|
+
|
|
71
|
+
table_filters: FieldsSchema | None = None
|
|
72
|
+
|
|
73
|
+
def generate_schema(self, user, language_context: LanguageContext) -> DashboardInfoSchemaData:
|
|
74
|
+
schema = super().generate_schema(user, language_context)
|
|
75
|
+
dashboard_info = DashboardInfoSchemaData(
|
|
76
|
+
search_enabled=self.search_enabled,
|
|
77
|
+
search_help=language_context.get_text(self.search_help),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if self.table_filters:
|
|
81
|
+
dashboard_info.table_filters = self.table_filters.generate_schema(user, language_context)
|
|
82
|
+
|
|
83
|
+
schema.dashboard_info = dashboard_info
|
|
84
|
+
return schema
|
|
85
|
+
|
|
86
|
+
async def get_data(self, data: DashboardData, user) -> DashboardContainer:
|
|
87
|
+
raise NotImplementedError('get_data is not implemented')
|
|
@@ -8,8 +8,8 @@ from pydantic import Field
|
|
|
8
8
|
|
|
9
9
|
from brilliance_admin.auth import UserABC
|
|
10
10
|
from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
11
|
-
from brilliance_admin.schema import
|
|
12
|
-
from brilliance_admin.schema.category import TableInfoSchemaData
|
|
11
|
+
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
12
|
+
from brilliance_admin.schema.category import BaseCategory, TableInfoSchemaData
|
|
13
13
|
from brilliance_admin.schema.table.admin_action import ActionData, ActionResult
|
|
14
14
|
from brilliance_admin.schema.table.fields_schema import FieldsSchema
|
|
15
15
|
from brilliance_admin.schema.table.table_models import AutocompleteData, AutocompleteResult, ListData, TableListResult
|
|
@@ -17,7 +17,7 @@ from brilliance_admin.translations import LanguageContext
|
|
|
17
17
|
from brilliance_admin.utils import DeserializeAction, SupportsStr
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class CategoryTable(
|
|
20
|
+
class CategoryTable(BaseCategory):
|
|
21
21
|
_type_slug: str = 'table'
|
|
22
22
|
|
|
23
23
|
search_enabled: bool = False
|
|
@@ -32,6 +32,8 @@ class CategoryTable(Category):
|
|
|
32
32
|
pk_name: str | None = None
|
|
33
33
|
|
|
34
34
|
def __init__(self, *args, table_schema=None, table_filters=None, **kwargs):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
|
|
35
37
|
if table_schema:
|
|
36
38
|
self.table_schema = table_schema
|
|
37
39
|
|
|
@@ -128,6 +130,7 @@ class CategoryTable(Category):
|
|
|
128
130
|
action_data: ActionData,
|
|
129
131
|
language_context: LanguageContext,
|
|
130
132
|
user: UserABC,
|
|
133
|
+
admin_schema: AdminSchema,
|
|
131
134
|
) -> ActionResult:
|
|
132
135
|
action_fn = self._get_action_fn(action)
|
|
133
136
|
if action_fn is None:
|
|
@@ -154,7 +157,7 @@ class CategoryTable(Category):
|
|
|
154
157
|
|
|
155
158
|
return result
|
|
156
159
|
|
|
157
|
-
async def autocomplete(self, data: AutocompleteData, user: UserABC) -> AutocompleteResult:
|
|
160
|
+
async def autocomplete(self, data: AutocompleteData, user: UserABC, schema: AdminSchema) -> AutocompleteResult:
|
|
158
161
|
"""
|
|
159
162
|
Retrieves list of found options to select.
|
|
160
163
|
"""
|
|
@@ -162,14 +165,16 @@ class CategoryTable(Category):
|
|
|
162
165
|
|
|
163
166
|
# pylint: disable=too-many-arguments
|
|
164
167
|
@abc.abstractmethod
|
|
165
|
-
async def get_list(
|
|
168
|
+
async def get_list(
|
|
169
|
+
self, list_data: ListData, user: UserABC, language_context: LanguageContext, admin_schema: AdminSchema
|
|
170
|
+
) -> TableListResult:
|
|
166
171
|
raise NotImplementedError()
|
|
167
172
|
|
|
168
|
-
# async def retrieve(self, pk: Any, user: UserABC) -> RetrieveResult:
|
|
173
|
+
# async def retrieve(self, pk: Any, user: UserABC, language_context: LanguageContext, admin_schema: AdminSchema) -> RetrieveResult:
|
|
169
174
|
# raise NotImplementedError()
|
|
170
175
|
|
|
171
|
-
# async def create(self, data: dict, user: UserABC) -> CreateResult:
|
|
176
|
+
# async def create(self, data: dict, user: UserABC, language_context: LanguageContext, admin_schema: AdminSchema) -> CreateResult:
|
|
172
177
|
# raise NotImplementedError()
|
|
173
178
|
|
|
174
|
-
# async def update(self, pk: Any, data: dict, user: UserABC) -> UpdateResult:
|
|
179
|
+
# async def update(self, pk: Any, data: dict, user: UserABC, language_context: LanguageContext, admin_schema: AdminSchema) -> UpdateResult:
|
|
175
180
|
# raise NotImplementedError()
|
|
@@ -8,6 +8,7 @@ from pydantic.dataclasses import dataclass
|
|
|
8
8
|
from brilliance_admin.exceptions import FieldError
|
|
9
9
|
from brilliance_admin.schema.category import FieldSchemaData
|
|
10
10
|
from brilliance_admin.translations import LanguageContext
|
|
11
|
+
from brilliance_admin.translations import TranslateText as _
|
|
11
12
|
from brilliance_admin.utils import DeserializeAction, SupportsStr, humanize_field_name
|
|
12
13
|
|
|
13
14
|
|
|
@@ -80,7 +81,7 @@ class IntegerField(TableField):
|
|
|
80
81
|
async def deserialize(self, value, action: DeserializeAction, extra: dict, *args, **kwargs) -> Any:
|
|
81
82
|
value = await super().deserialize(value, action, extra, *args, **kwargs)
|
|
82
83
|
if value and not isinstance(value, int):
|
|
83
|
-
raise FieldError(
|
|
84
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'init'})
|
|
84
85
|
|
|
85
86
|
return value
|
|
86
87
|
|
|
@@ -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(
|
|
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(
|
|
167
|
+
raise FieldError(_('errors.bad_type_error') % {'type': type(value), 'expected': 'datetime'})
|
|
155
168
|
|
|
156
169
|
if isinstance(value, str):
|
|
157
|
-
return
|
|
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
|
-
|
|
162
|
-
|
|
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':
|
|
166
|
-
'to':
|
|
179
|
+
'from': _parse_iso(value['from']),
|
|
180
|
+
'to': _parse_iso(value['to']),
|
|
167
181
|
}
|
|
168
182
|
|
|
169
|
-
raise
|
|
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
|
|
194
|
-
|
|
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
|
|
|
@@ -276,3 +304,29 @@ class ChoiceField(TableField):
|
|
|
276
304
|
'value': value,
|
|
277
305
|
'title': choice.get('title') or value if choice else value.capitalize(),
|
|
278
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
|
|
@@ -159,7 +159,13 @@ class FieldsSchema:
|
|
|
159
159
|
result = {}
|
|
160
160
|
for field_slug, field in self.get_fields().items():
|
|
161
161
|
value = data.get(field_slug)
|
|
162
|
-
|
|
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
|
+
|
|
163
169
|
return result
|
|
164
170
|
|
|
165
171
|
async def deserialize(self, data: dict, action: DeserializeAction, extra) -> dict:
|