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.
- 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/__init__.py +1 -0
- brilliance_admin/integrations/sqlalchemy/auth.py +3 -4
- brilliance_admin/integrations/sqlalchemy/autocomplete.py +12 -3
- brilliance_admin/integrations/sqlalchemy/fields.py +36 -19
- 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 +6 -3
- brilliance_admin/integrations/sqlalchemy/table/delete.py +2 -2
- brilliance_admin/integrations/sqlalchemy/table/list.py +44 -14
- brilliance_admin/integrations/sqlalchemy/table/retrieve.py +41 -10
- brilliance_admin/integrations/sqlalchemy/table/update.py +10 -5
- brilliance_admin/locales/en.yml +20 -10
- brilliance_admin/locales/ru.yml +20 -10
- brilliance_admin/schema/__init__.py +3 -3
- brilliance_admin/schema/admin_schema.py +31 -23
- brilliance_admin/schema/category.py +88 -14
- 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 +102 -19
- brilliance_admin/schema/table/fields_schema.py +9 -2
- brilliance_admin/static/{index-D9axz5zK.js → index-8ahvKI6W.js} +190 -190
- brilliance_admin/static/{index-vlBToOhT.css → index-B8JOx1Ps.css} +1 -1
- brilliance_admin/templates/index.html +2 -2
- brilliance_admin/translations.py +6 -3
- brilliance_admin/utils.py +41 -3
- brilliance_admin-0.44.12.dist-info/METADATA +165 -0
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/RECORD +36 -37
- {brilliance_admin-0.43.1.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.1.dist-info/METADATA +0 -214
- brilliance_admin-0.43.1.dist-info/licenses/LICENSE +0 -17
- {brilliance_admin-0.43.1.dist-info → brilliance_admin-0.44.12.dist-info}/top_level.txt +0 -0
|
@@ -2,6 +2,7 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
from brilliance_admin import auth, schema
|
|
4
4
|
from brilliance_admin.exceptions import AdminAPIException, APIError
|
|
5
|
+
from brilliance_admin.schema.admin_schema import AdminSchema
|
|
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
|
|
@@ -19,9 +20,10 @@ class SQLAlchemyAdminUpdate:
|
|
|
19
20
|
data: dict,
|
|
20
21
|
user: auth.UserABC,
|
|
21
22
|
language_context: LanguageContext,
|
|
23
|
+
admin_schema: AdminSchema,
|
|
22
24
|
) -> schema.UpdateResult:
|
|
23
25
|
if not self.has_update:
|
|
24
|
-
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
26
|
+
raise AdminAPIException(APIError(message=_('errors.method_not_allowed')), status_code=500)
|
|
25
27
|
|
|
26
28
|
# pylint: disable=import-outside-toplevel
|
|
27
29
|
from sqlalchemy import inspect
|
|
@@ -29,7 +31,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
29
31
|
|
|
30
32
|
if pk is None:
|
|
31
33
|
raise AdminAPIException(
|
|
32
|
-
APIError(message=_('pk_not_found') % {'pk_name': self.pk_name}, code='pk_not_found'),
|
|
34
|
+
APIError(message=_('errors.pk_not_found') % {'pk_name': self.pk_name}, code='pk_not_found'),
|
|
33
35
|
status_code=400,
|
|
34
36
|
)
|
|
35
37
|
|
|
@@ -42,7 +44,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
42
44
|
async with self.db_async_session() as session:
|
|
43
45
|
record = (await session.execute(stmt)).scalars().first()
|
|
44
46
|
if record is None:
|
|
45
|
-
msg = _('record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
47
|
+
msg = _('errors.record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
46
48
|
raise AdminAPIException(
|
|
47
49
|
APIError(message=msg, code='record_not_found'),
|
|
48
50
|
status_code=400,
|
|
@@ -59,7 +61,7 @@ class SQLAlchemyAdminUpdate:
|
|
|
59
61
|
type(self).__name__, self.table_schema.model.__name__, pk, e,
|
|
60
62
|
extra={'data': data},
|
|
61
63
|
)
|
|
62
|
-
msg = _('connection_refused_error') % {'error': str(e)}
|
|
64
|
+
msg = _('errors.connection_refused_error') % {'error': str(e)}
|
|
63
65
|
raise AdminAPIException(
|
|
64
66
|
APIError(message=msg, code='connection_refused_error'),
|
|
65
67
|
status_code=400,
|
|
@@ -83,8 +85,11 @@ class SQLAlchemyAdminUpdate:
|
|
|
83
85
|
type(self).__name__, self.table_schema.model.__name__, pk, e,
|
|
84
86
|
extra={'data': data}
|
|
85
87
|
)
|
|
88
|
+
msg = _('errors.db_error_update') % {
|
|
89
|
+
'error_type': str(e) if admin_schema.debug else type(e).__name__,
|
|
90
|
+
}
|
|
86
91
|
raise AdminAPIException(
|
|
87
|
-
APIError(message=
|
|
92
|
+
APIError(message=msg, code='db_error_update'), status_code=500,
|
|
88
93
|
) from e
|
|
89
94
|
|
|
90
95
|
logger.info(
|
brilliance_admin/locales/en.yml
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
|
|
5
|
+
errors:
|
|
6
|
+
pk_not_found: 'The "%(pk_name)s" field was not found in the submitted data.'
|
|
7
|
+
record_not_found: 'No record found for %(pk_name)s=%(pk)s.'
|
|
8
|
+
|
|
9
|
+
db_error_create: 'Error creating a record in the database: %(error_type)s'
|
|
10
|
+
db_error_update: 'Error updating the record in the database: %(error_type)s'
|
|
11
|
+
db_error_retrieve: 'Error retrieving the record from the database: %(error_type)s'
|
|
12
|
+
db_error_list: 'Failed to retrieve table data from the database: %(error_type)s'
|
|
13
|
+
|
|
14
|
+
connection_refused_error: 'Database connection error: %(error)s'
|
|
15
|
+
filters_exception: 'An unknown technical error occurred while filtering data: %(error_type)s'
|
|
16
|
+
method_not_allowed: 'Error, method not allowed. This action is not permitted.'
|
|
17
|
+
filter_error: 'An error occurred during filtering: {error}'
|
|
18
|
+
bad_type_error: 'Invalid data type: %(type)s but %(expected)s expected'
|
|
19
|
+
|
|
20
|
+
serialize_error:
|
|
21
|
+
field_error: 'Field %(field_slug)s serialize error: %(error)s'
|
|
22
|
+
unexpected_error: 'Unexpected serialize error: %(error)s'
|
|
23
|
+
|
|
11
24
|
search_help: 'Available search fields: %(fields)s'
|
|
12
25
|
sqlalchemy_search_help: |
|
|
13
26
|
<b>Available search fields:</b>
|
|
@@ -17,6 +30,3 @@ sqlalchemy_search_help: |
|
|
|
17
30
|
<b>""</b> - quotes for exact match
|
|
18
31
|
<b>%%</b> - any sequence of characters
|
|
19
32
|
<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,26 @@
|
|
|
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
|
+
|
|
9
|
+
db_error_create: 'Ошибка создания записи в базе данных: %(error_type)s'
|
|
10
|
+
db_error_update: 'Ошибка обновления записи в базе данных: %(error_type)s'
|
|
11
|
+
db_error_retrieve: 'Ошибка получения записи из базы данных: %(error_type)s'
|
|
12
|
+
db_error_list: 'Ошибка получения данных таблицы из базы данных: %(error_type)s'
|
|
13
|
+
|
|
14
|
+
connection_refused_error: 'Ошибка подключения к базе данных: %(error)s'
|
|
15
|
+
filters_exception: 'Произошла неизвестная техническая ошибка при фильтрации данных: %(error_type)s'
|
|
16
|
+
method_not_allowed: 'Ошибка, данный метод недоступен.'
|
|
17
|
+
filter_error: 'Проишла ошибка при фильтрации: %(error)s'
|
|
18
|
+
bad_type_error: 'Некорректный тип данных: %(type)s; ожидается %(expected)s'
|
|
19
|
+
|
|
20
|
+
serialize_error:
|
|
21
|
+
field_error: 'ошибка чтения поля %(field_slug)s: %(error)s'
|
|
22
|
+
unexpected_error: 'Ошибка обработки данных: %(error)s'
|
|
23
|
+
|
|
11
24
|
search_help: 'Доступные поля для поиска: %(fields)s'
|
|
12
25
|
sqlalchemy_search_help: |
|
|
13
26
|
<b>Доступные поля для поиска:</b>
|
|
@@ -17,6 +30,3 @@ sqlalchemy_search_help: |
|
|
|
17
30
|
<b>""</b> - кавычки для точного совпадения
|
|
18
31
|
<b>%%</b> - любая последовательность символов
|
|
19
32
|
<b>_</b> - один любой символ
|
|
20
|
-
filters_exception: 'Произошла неизвестная техническая ошибка при фильтрации данных.'
|
|
21
|
-
method_not_allowed: 'Ошибка, данный метод недоступен.'
|
|
22
|
-
filter_error: 'Проишла ошибка при фильтрации: {error}'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# pylint: disable=wildcard-import, unused-wildcard-import, unused-import
|
|
2
2
|
# flake8: noqa: F405
|
|
3
3
|
from .admin_schema import AdminSchema, AdminSchemaData
|
|
4
|
-
from .category import
|
|
5
|
-
from .
|
|
6
|
-
from .group import Group
|
|
4
|
+
from .category import CategoryGroup, CategoryLink
|
|
5
|
+
from .dashboard import *
|
|
7
6
|
from .table import *
|
|
7
|
+
from .table.category_table import CategoryTable
|
|
@@ -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
|
|
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
|
|
@@ -30,7 +33,6 @@ class FieldSchemaData(DataclassBase):
|
|
|
30
33
|
|
|
31
34
|
choices: List[dict] | None = None
|
|
32
35
|
|
|
33
|
-
tag_colors: dict | None = None
|
|
34
36
|
variant: str | None = None
|
|
35
37
|
size: str | None = None
|
|
36
38
|
|
|
@@ -93,7 +95,7 @@ class TableInfoSchemaData(DataclassBase):
|
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
@dataclass
|
|
96
|
-
class
|
|
98
|
+
class DashboardInfoSchemaData(DataclassBase):
|
|
97
99
|
search_enabled: bool
|
|
98
100
|
search_help: str | None
|
|
99
101
|
|
|
@@ -107,37 +109,56 @@ class CategorySchemaData(DataclassBase):
|
|
|
107
109
|
icon: str | None
|
|
108
110
|
type: str
|
|
109
111
|
|
|
112
|
+
categories: dict = Field(default_factory=dict)
|
|
113
|
+
|
|
110
114
|
table_info: TableInfoSchemaData | None = None
|
|
111
|
-
|
|
115
|
+
dashboard_info: DashboardInfoSchemaData | None = None
|
|
116
|
+
|
|
117
|
+
link: str | None = None
|
|
112
118
|
|
|
113
119
|
def __repr__(self):
|
|
114
120
|
return f'<CategorySchemaData type={self.type} "{self.title}">'
|
|
115
121
|
|
|
116
122
|
|
|
117
|
-
class
|
|
118
|
-
slug:
|
|
119
|
-
title:
|
|
120
|
-
description:
|
|
123
|
+
class BaseCategory(KwargsInitMixin, abc.ABC):
|
|
124
|
+
slug: str
|
|
125
|
+
title: SupportsStr | None = None
|
|
126
|
+
description: SupportsStr | None = None
|
|
121
127
|
|
|
122
128
|
# https://pictogrammers.com/library/mdi/
|
|
123
|
-
icon:
|
|
129
|
+
icon: str | None = None
|
|
124
130
|
|
|
125
131
|
_type_slug: ClassVar[str]
|
|
126
132
|
|
|
127
133
|
def generate_schema(self, user: UserABC, language_context: LanguageContext) -> CategorySchemaData:
|
|
128
|
-
|
|
129
|
-
|
|
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(
|
|
140
|
+
title=language_context.get_text(self.title) or humanize_field_name(self.slug),
|
|
130
141
|
description=language_context.get_text(self.description),
|
|
131
142
|
icon=self.icon,
|
|
132
|
-
type=
|
|
143
|
+
type=type_slug,
|
|
133
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')
|
|
134
155
|
|
|
135
156
|
@classmethod
|
|
136
157
|
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> core_schema.CoreSchema:
|
|
137
|
-
def validate(v: Any) -> "
|
|
158
|
+
def validate(v: Any) -> "BaseCategory":
|
|
138
159
|
if isinstance(v, cls):
|
|
139
160
|
return v
|
|
140
|
-
raise TypeError(f"Expected {cls.__name__} instance")
|
|
161
|
+
raise TypeError(f"Expected {cls.__name__} instance, recieved: {type(v)} {v}")
|
|
141
162
|
|
|
142
163
|
return core_schema.no_info_plain_validator_function(
|
|
143
164
|
validate,
|
|
@@ -147,3 +168,56 @@ class Category(abc.ABC):
|
|
|
147
168
|
return_schema=core_schema.str_schema(),
|
|
148
169
|
),
|
|
149
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()
|