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.
- admin_panel/__init__.py +4 -0
- admin_panel/api/__init__.py +0 -0
- admin_panel/api/routers.py +18 -0
- admin_panel/api/utils.py +28 -0
- admin_panel/api/views/__init__.py +0 -0
- admin_panel/api/views/auth.py +29 -0
- admin_panel/api/views/autocomplete.py +33 -0
- admin_panel/api/views/graphs.py +30 -0
- admin_panel/api/views/index.py +38 -0
- admin_panel/api/views/schema.py +29 -0
- admin_panel/api/views/settings.py +29 -0
- admin_panel/api/views/table.py +136 -0
- admin_panel/auth.py +32 -0
- admin_panel/docs.py +37 -0
- admin_panel/exceptions.py +38 -0
- admin_panel/integrations/__init__.py +0 -0
- admin_panel/integrations/sqlalchemy/__init__.py +6 -0
- admin_panel/integrations/sqlalchemy/auth.py +144 -0
- admin_panel/integrations/sqlalchemy/autocomplete.py +38 -0
- admin_panel/integrations/sqlalchemy/fields.py +254 -0
- admin_panel/integrations/sqlalchemy/fields_schema.py +316 -0
- admin_panel/integrations/sqlalchemy/table/__init__.py +19 -0
- admin_panel/integrations/sqlalchemy/table/base.py +141 -0
- admin_panel/integrations/sqlalchemy/table/create.py +73 -0
- admin_panel/integrations/sqlalchemy/table/delete.py +18 -0
- admin_panel/integrations/sqlalchemy/table/list.py +178 -0
- admin_panel/integrations/sqlalchemy/table/retrieve.py +61 -0
- admin_panel/integrations/sqlalchemy/table/update.py +95 -0
- admin_panel/schema/__init__.py +7 -0
- admin_panel/schema/admin_schema.py +191 -0
- admin_panel/schema/category.py +149 -0
- admin_panel/schema/graphs/__init__.py +1 -0
- admin_panel/schema/graphs/category_graphs.py +50 -0
- admin_panel/schema/group.py +67 -0
- admin_panel/schema/table/__init__.py +8 -0
- admin_panel/schema/table/admin_action.py +76 -0
- admin_panel/schema/table/category_table.py +175 -0
- admin_panel/schema/table/fields/__init__.py +5 -0
- admin_panel/schema/table/fields/base.py +249 -0
- admin_panel/schema/table/fields/function_field.py +65 -0
- admin_panel/schema/table/fields_schema.py +216 -0
- admin_panel/schema/table/table_models.py +53 -0
- admin_panel/static/favicon.jpg +0 -0
- admin_panel/static/index-BeniOHDv.js +525 -0
- admin_panel/static/index-vlBToOhT.css +8 -0
- admin_panel/static/materialdesignicons-webfont-CYDMK1kx.woff2 +0 -0
- admin_panel/static/materialdesignicons-webfont-CgCzGbLl.woff +0 -0
- admin_panel/static/materialdesignicons-webfont-D3kAzl71.ttf +0 -0
- admin_panel/static/materialdesignicons-webfont-DttUABo4.eot +0 -0
- admin_panel/static/tinymce/dark-first/content.min.css +250 -0
- admin_panel/static/tinymce/dark-first/skin.min.css +2820 -0
- admin_panel/static/tinymce/dark-slim/content.min.css +249 -0
- admin_panel/static/tinymce/dark-slim/skin.min.css +2821 -0
- admin_panel/static/tinymce/img/example.png +0 -0
- admin_panel/static/tinymce/img/tinymce.woff2 +0 -0
- admin_panel/static/tinymce/lightgray/content.min.css +1 -0
- admin_panel/static/tinymce/lightgray/fonts/tinymce.woff +0 -0
- admin_panel/static/tinymce/lightgray/skin.min.css +1 -0
- admin_panel/static/tinymce/plugins/accordion/css/accordion.css +17 -0
- admin_panel/static/tinymce/plugins/accordion/plugin.js +48 -0
- admin_panel/static/tinymce/plugins/codesample/css/prism.css +1 -0
- admin_panel/static/tinymce/plugins/customLink/css/link.css +3 -0
- admin_panel/static/tinymce/plugins/customLink/plugin.js +147 -0
- admin_panel/static/tinymce/tinymce.min.js +2 -0
- admin_panel/static/vanilla-picker-B6E6ObS_.js +8 -0
- admin_panel/templates/index.html +25 -0
- admin_panel/translations.py +145 -0
- admin_panel/utils.py +50 -0
- brilliance_admin-0.42.0.dist-info/METADATA +155 -0
- brilliance_admin-0.42.0.dist-info/RECORD +73 -0
- brilliance_admin-0.42.0.dist-info/WHEEL +5 -0
- brilliance_admin-0.42.0.dist-info/licenses/LICENSE +17 -0
- brilliance_admin-0.42.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from admin_panel.integrations.sqlalchemy.autocomplete import SQLAlchemyAdminAutocompleteMixin
|
|
4
|
+
from admin_panel.integrations.sqlalchemy.fields_schema import SQLAlchemyFieldsSchema
|
|
5
|
+
from admin_panel.schema.table.category_table import CategoryTable
|
|
6
|
+
from admin_panel.translations import TranslateText as _
|
|
7
|
+
|
|
8
|
+
EXCEPTION_REL_NAME = '''
|
|
9
|
+
Model "{model_name}" doesn\'t contain rel_name:"{rel_name}" for field "{slug}"
|
|
10
|
+
Model fields = {model_attrs}
|
|
11
|
+
'''
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SQLAlchemyAdminBase(SQLAlchemyAdminAutocompleteMixin, CategoryTable):
|
|
15
|
+
model: Any
|
|
16
|
+
slug = None
|
|
17
|
+
ordering_fields = []
|
|
18
|
+
|
|
19
|
+
search_fields = []
|
|
20
|
+
|
|
21
|
+
table_schema: SQLAlchemyFieldsSchema
|
|
22
|
+
|
|
23
|
+
db_async_session: Any = None
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*args,
|
|
28
|
+
model=None,
|
|
29
|
+
table_schema=None,
|
|
30
|
+
db_async_session=None,
|
|
31
|
+
ordering_fields=None,
|
|
32
|
+
default_ordering=None,
|
|
33
|
+
search_fields=None,
|
|
34
|
+
**kwargs,
|
|
35
|
+
):
|
|
36
|
+
if model:
|
|
37
|
+
self.model = model
|
|
38
|
+
|
|
39
|
+
if search_fields:
|
|
40
|
+
self.search_fields = search_fields
|
|
41
|
+
|
|
42
|
+
if self.search_fields:
|
|
43
|
+
self.search_enabled = True
|
|
44
|
+
self.search_help = _('sqlalchemy_search_help') % {'fields': ', '.join(self.search_fields)}
|
|
45
|
+
|
|
46
|
+
if default_ordering:
|
|
47
|
+
self.default_ordering = default_ordering
|
|
48
|
+
|
|
49
|
+
if ordering_fields:
|
|
50
|
+
self.ordering_fields = ordering_fields
|
|
51
|
+
|
|
52
|
+
self.validate_fields()
|
|
53
|
+
|
|
54
|
+
if table_schema:
|
|
55
|
+
self.table_schema = table_schema
|
|
56
|
+
|
|
57
|
+
if not self.table_schema:
|
|
58
|
+
self.table_schema = SQLAlchemyFieldsSchema(model=self.model)
|
|
59
|
+
|
|
60
|
+
if not issubclass(type(self.table_schema), SQLAlchemyFieldsSchema):
|
|
61
|
+
msg = f'{type(self).__name__}.table_schema {self.table_schema} must be subclass of SQLAlchemyFieldsSchema'
|
|
62
|
+
raise AttributeError(msg)
|
|
63
|
+
|
|
64
|
+
if not self.model:
|
|
65
|
+
msg = f'{type(self).__name__}.model is required for SQLAlchemy'
|
|
66
|
+
raise AttributeError(msg)
|
|
67
|
+
|
|
68
|
+
if not self.slug:
|
|
69
|
+
self.slug = self.model.__name__.lower()
|
|
70
|
+
|
|
71
|
+
if db_async_session:
|
|
72
|
+
self.db_async_session = db_async_session
|
|
73
|
+
|
|
74
|
+
if not self.db_async_session:
|
|
75
|
+
msg = f'{type(self).__name__}.db_async_session is required for SQLAlchemy'
|
|
76
|
+
raise AttributeError(msg)
|
|
77
|
+
|
|
78
|
+
# pylint: disable=import-outside-toplevel
|
|
79
|
+
from sqlalchemy import inspect
|
|
80
|
+
from sqlalchemy.sql.schema import Column
|
|
81
|
+
|
|
82
|
+
for attr in inspect(self.model).mapper.column_attrs:
|
|
83
|
+
col: Column = attr.columns[0]
|
|
84
|
+
if col.primary_key and not self.pk_name:
|
|
85
|
+
self.pk_name = attr.key
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
if not self.default_ordering and self.pk_name:
|
|
89
|
+
self.default_ordering = f'-{self.pk_name}'
|
|
90
|
+
|
|
91
|
+
super().__init__(*args, **kwargs)
|
|
92
|
+
|
|
93
|
+
def validate_fields(self):
|
|
94
|
+
# pylint: disable=import-outside-toplevel
|
|
95
|
+
from sqlalchemy.orm import InstrumentedAttribute
|
|
96
|
+
|
|
97
|
+
if self.search_fields:
|
|
98
|
+
for field in self.search_fields:
|
|
99
|
+
column = getattr(self.model, field, None)
|
|
100
|
+
if not isinstance(column, InstrumentedAttribute):
|
|
101
|
+
raise AttributeError(
|
|
102
|
+
f'{type(self).__name__}: search field "{field}" not found in model {self.model.__name__}'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if self.ordering_fields:
|
|
106
|
+
for field in self.ordering_fields:
|
|
107
|
+
column = getattr(self.model, field, None)
|
|
108
|
+
if not isinstance(column, InstrumentedAttribute):
|
|
109
|
+
raise AttributeError(
|
|
110
|
+
f'{type(self).__name__}: ordering field "{field}" not found in model {self.model.__name__}'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def get_queryset(self):
|
|
114
|
+
# pylint: disable=import-outside-toplevel
|
|
115
|
+
from sqlalchemy import select
|
|
116
|
+
from sqlalchemy.orm import selectinload
|
|
117
|
+
|
|
118
|
+
stmt = select(self.model).options(selectinload('*'))
|
|
119
|
+
|
|
120
|
+
# Eager-load related fields
|
|
121
|
+
for slug, field in self.table_schema.get_fields().items():
|
|
122
|
+
|
|
123
|
+
# pylint: disable=protected-access
|
|
124
|
+
if field._type == "related":
|
|
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
|
+
if not hasattr(self.model, field.rel_name):
|
|
131
|
+
msg = EXCEPTION_REL_NAME.format(
|
|
132
|
+
slug=slug,
|
|
133
|
+
model_name=self.model.__name__,
|
|
134
|
+
rel_name=field.rel_name,
|
|
135
|
+
model_attrs=model_attrs,
|
|
136
|
+
)
|
|
137
|
+
raise AttributeError(msg)
|
|
138
|
+
|
|
139
|
+
stmt = stmt.options(selectinload(getattr(self.model, field.rel_name)))
|
|
140
|
+
|
|
141
|
+
return stmt
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from admin_panel import schema
|
|
2
|
+
from admin_panel.auth import UserABC
|
|
3
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
4
|
+
from admin_panel.translations import LanguageManager
|
|
5
|
+
from admin_panel.translations import TranslateText as _
|
|
6
|
+
from admin_panel.utils import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQLAlchemyAdminCreate:
|
|
12
|
+
has_create: bool = True
|
|
13
|
+
|
|
14
|
+
async def create(
|
|
15
|
+
self,
|
|
16
|
+
data: dict,
|
|
17
|
+
user: UserABC,
|
|
18
|
+
language_manager: LanguageManager,
|
|
19
|
+
) -> schema.CreateResult:
|
|
20
|
+
if not self.has_create:
|
|
21
|
+
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
22
|
+
|
|
23
|
+
# pylint: disable=import-outside-toplevel
|
|
24
|
+
from sqlalchemy.exc import IntegrityError
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
async with self.db_async_session() as session:
|
|
28
|
+
record = await self.table_schema.create(user, data, session)
|
|
29
|
+
pk_value = getattr(record, self.pk_name, None)
|
|
30
|
+
|
|
31
|
+
except AdminAPIException as e:
|
|
32
|
+
raise e
|
|
33
|
+
|
|
34
|
+
except ConnectionRefusedError as e:
|
|
35
|
+
logger.exception(
|
|
36
|
+
'SQLAlchemy %s create %s db error: %s',
|
|
37
|
+
type(self).__name__, self.table_schema.model.__name__, e,
|
|
38
|
+
extra={'data': data},
|
|
39
|
+
)
|
|
40
|
+
msg = _('connection_refused_error') % {'error': str(e)}
|
|
41
|
+
raise AdminAPIException(
|
|
42
|
+
APIError(message=msg, code='connection_refused_error'),
|
|
43
|
+
status_code=500,
|
|
44
|
+
) from e
|
|
45
|
+
|
|
46
|
+
except IntegrityError as e:
|
|
47
|
+
logger.warning(
|
|
48
|
+
'SQLAlchemy %s create %s db error: %s',
|
|
49
|
+
type(self).__name__, self.table_schema.model.__name__, e,
|
|
50
|
+
extra={'data': data},
|
|
51
|
+
)
|
|
52
|
+
orig = e.orig
|
|
53
|
+
message = orig.args[0] if orig.args else type(orig).__name__
|
|
54
|
+
raise AdminAPIException(
|
|
55
|
+
APIError(message=message, code='db_integrity_error'), status_code=500,
|
|
56
|
+
) from e
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.exception(
|
|
60
|
+
'SQLAlchemy %s create %s db error: %s',
|
|
61
|
+
type(self).__name__, self.table_schema.model.__name__, e,
|
|
62
|
+
extra={'data': data},
|
|
63
|
+
)
|
|
64
|
+
raise AdminAPIException(
|
|
65
|
+
APIError(message=_('db_error_create'), code='db_error_create'), status_code=500,
|
|
66
|
+
) from e
|
|
67
|
+
|
|
68
|
+
logger.info(
|
|
69
|
+
'%s model %s #%s created by %s',
|
|
70
|
+
type(self).__name__, self.table_schema.model.__name__, pk_value, user.username,
|
|
71
|
+
extra={'data': data},
|
|
72
|
+
)
|
|
73
|
+
return schema.CreateResult(pk=pk_value)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from admin_panel.exceptions import APIError, AdminAPIException
|
|
2
|
+
from admin_panel.schema.table.admin_action import ActionData, ActionMessage, ActionResult, admin_action
|
|
3
|
+
from admin_panel.translations import TranslateText as _
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SQLAlchemyDeleteAction:
|
|
7
|
+
has_delete: bool = True
|
|
8
|
+
|
|
9
|
+
@admin_action(
|
|
10
|
+
title=_('delete'),
|
|
11
|
+
confirmation_text=_('delete_confirmation_text'),
|
|
12
|
+
base_color='red-lighten-2',
|
|
13
|
+
variant='outlined',
|
|
14
|
+
)
|
|
15
|
+
async def delete(self, action_data: ActionData):
|
|
16
|
+
if not self.has_delete:
|
|
17
|
+
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
18
|
+
return ActionResult(message=ActionMessage(_('deleted_successfully')))
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from admin_panel import auth, schema
|
|
2
|
+
from admin_panel.exceptions import AdminAPIException, APIError, FieldError
|
|
3
|
+
from admin_panel.integrations.sqlalchemy.fields_schema import SQLAlchemyFieldsSchema
|
|
4
|
+
from admin_panel.translations import LanguageManager
|
|
5
|
+
from admin_panel.translations import TranslateText as _
|
|
6
|
+
from admin_panel.utils import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQLAlchemyAdminListMixin:
|
|
12
|
+
table_schema: SQLAlchemyFieldsSchema
|
|
13
|
+
table_filters: SQLAlchemyFieldsSchema | None
|
|
14
|
+
|
|
15
|
+
def apply_ordering(self, stmt, list_data):
|
|
16
|
+
# pylint: disable=import-outside-toplevel
|
|
17
|
+
from sqlalchemy import asc, desc
|
|
18
|
+
from sqlalchemy.orm import InstrumentedAttribute
|
|
19
|
+
|
|
20
|
+
ordering = list_data.ordering or self.default_ordering
|
|
21
|
+
|
|
22
|
+
if not ordering:
|
|
23
|
+
return stmt
|
|
24
|
+
|
|
25
|
+
direction = asc
|
|
26
|
+
|
|
27
|
+
if ordering.startswith("-"):
|
|
28
|
+
ordering = ordering[1:]
|
|
29
|
+
direction = desc
|
|
30
|
+
|
|
31
|
+
if list_data.ordering and ordering not in self.ordering_fields:
|
|
32
|
+
msg = f'Ordering "{ordering}" is not allowed; available options: {self.ordering_fields} default_ordering: {self.default_ordering}'
|
|
33
|
+
raise FieldError(message=msg)
|
|
34
|
+
|
|
35
|
+
column = getattr(self.model, ordering, None)
|
|
36
|
+
if not isinstance(column, InstrumentedAttribute):
|
|
37
|
+
msg = f'{type(self).__name__} ordering field "{ordering}" not found in model {self.model}'
|
|
38
|
+
raise FieldError(message=msg)
|
|
39
|
+
|
|
40
|
+
return stmt.order_by(direction(column))
|
|
41
|
+
|
|
42
|
+
def apply_search(self, stmt, list_data: schema.ListData):
|
|
43
|
+
# pylint: disable=import-outside-toplevel
|
|
44
|
+
from sqlalchemy import String, cast, or_
|
|
45
|
+
from sqlalchemy.orm import InstrumentedAttribute
|
|
46
|
+
|
|
47
|
+
if not self.search_fields or not list_data.search:
|
|
48
|
+
return stmt
|
|
49
|
+
|
|
50
|
+
search = list_data.search
|
|
51
|
+
conditions = []
|
|
52
|
+
|
|
53
|
+
for field_slug in self.search_fields:
|
|
54
|
+
column = getattr(self.model, field_slug, None)
|
|
55
|
+
if not isinstance(column, InstrumentedAttribute):
|
|
56
|
+
msg = f'{type(self).__name__} filter "{field_slug}" not found as field inside model {self.model}'
|
|
57
|
+
raise AttributeError(msg)
|
|
58
|
+
|
|
59
|
+
conditions.append(cast(column, String).ilike(search))
|
|
60
|
+
|
|
61
|
+
if conditions:
|
|
62
|
+
stmt = stmt.where(or_(*conditions))
|
|
63
|
+
|
|
64
|
+
return stmt
|
|
65
|
+
|
|
66
|
+
async def apply_filters(self, stmt, list_data: schema.ListData):
|
|
67
|
+
if not self.table_filters or not list_data.filters:
|
|
68
|
+
return stmt
|
|
69
|
+
|
|
70
|
+
if not issubclass(type(self.table_filters), SQLAlchemyFieldsSchema):
|
|
71
|
+
msg = f'{type(self).__name__}.table_filters {type(self.table_filters)} must be SQLAlchemyFieldsSchema subclass'
|
|
72
|
+
raise AttributeError(msg)
|
|
73
|
+
|
|
74
|
+
return await self.table_filters.apply_filters(stmt, list_data.filters)
|
|
75
|
+
|
|
76
|
+
def apply_pagination(self, stmt, list_data: schema.ListData):
|
|
77
|
+
page = max(1, list_data.page or 1)
|
|
78
|
+
limit = min(150, max(1, list_data.limit or 25))
|
|
79
|
+
|
|
80
|
+
offset = (page - 1) * limit
|
|
81
|
+
|
|
82
|
+
return stmt.limit(limit).offset(offset)
|
|
83
|
+
|
|
84
|
+
# pylint: disable=too-many-arguments
|
|
85
|
+
# pylint: disable=too-many-locals
|
|
86
|
+
async def get_list(
|
|
87
|
+
self,
|
|
88
|
+
list_data: schema.ListData,
|
|
89
|
+
user: auth.UserABC,
|
|
90
|
+
language_manager: LanguageManager,
|
|
91
|
+
) -> schema.TableListResult:
|
|
92
|
+
# pylint: disable=import-outside-toplevel
|
|
93
|
+
from sqlalchemy import exc, func, select
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
stmt = self.get_queryset()
|
|
97
|
+
stmt = await self.apply_filters(stmt, list_data)
|
|
98
|
+
stmt = self.apply_search(stmt, list_data)
|
|
99
|
+
|
|
100
|
+
count_stmt = select(func.count()).select_from(stmt.subquery())
|
|
101
|
+
stmt = self.apply_pagination(stmt, list_data)
|
|
102
|
+
stmt = self.apply_ordering(stmt, list_data)
|
|
103
|
+
|
|
104
|
+
except FieldError as e:
|
|
105
|
+
logger.exception(
|
|
106
|
+
'SQLAlchemy %s list filters for %s field error: %s',
|
|
107
|
+
type(self).__name__, self.model.__name__, e,
|
|
108
|
+
extra={
|
|
109
|
+
'list_data': list_data,
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
msg = _('filter_error') % {'error': e.message}
|
|
113
|
+
raise AdminAPIException(APIError(message=msg, code='filters_exception'), status_code=500) from e
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.exception(
|
|
117
|
+
'SQLAlchemy %s list filters for %s error: %s',
|
|
118
|
+
type(self).__name__, self.model.__name__, e,
|
|
119
|
+
extra={
|
|
120
|
+
'list_data': list_data,
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
raise AdminAPIException(APIError(message=_('filters_exception'), code='filters_exception'), status_code=500) from e
|
|
124
|
+
|
|
125
|
+
data = []
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
async with self.db_async_session() as session:
|
|
129
|
+
total_count = await session.scalar(count_stmt)
|
|
130
|
+
records = (await session.execute(stmt)).scalars().all()
|
|
131
|
+
for record in records:
|
|
132
|
+
line = await self.table_schema.serialize(
|
|
133
|
+
record,
|
|
134
|
+
extra={"record": record, "user": user},
|
|
135
|
+
)
|
|
136
|
+
data.append(line)
|
|
137
|
+
|
|
138
|
+
except ConnectionRefusedError as e:
|
|
139
|
+
logger.exception(
|
|
140
|
+
'SQLAlchemy %s get_list db error: %s',
|
|
141
|
+
type(self).__name__, e,
|
|
142
|
+
extra={
|
|
143
|
+
'list_data': list_data,
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
msg = _('connection_refused_error') % {'error': str(e)}
|
|
147
|
+
raise AdminAPIException(
|
|
148
|
+
APIError(message=msg, code='connection_refused_error'),
|
|
149
|
+
status_code=500,
|
|
150
|
+
) from e
|
|
151
|
+
|
|
152
|
+
except (exc.IntegrityError, exc.StatementError) as e:
|
|
153
|
+
logger.exception(
|
|
154
|
+
'SQLAlchemy %s get_list db error: %s',
|
|
155
|
+
type(self).__name__, e,
|
|
156
|
+
extra={
|
|
157
|
+
'list_data': list_data,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
orig = e.orig
|
|
161
|
+
message = orig.args[0] if orig.args else type(orig).__name__
|
|
162
|
+
raise AdminAPIException(
|
|
163
|
+
APIError(message=message, code='db_exception'), status_code=500,
|
|
164
|
+
) from e
|
|
165
|
+
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.exception(
|
|
168
|
+
'SQLAlchemy %s get_list db error: %s',
|
|
169
|
+
type(self).__name__, e,
|
|
170
|
+
extra={
|
|
171
|
+
'list_data': list_data,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
raise AdminAPIException(
|
|
175
|
+
APIError(message=_('db_error_list'), code='db_error_list'), status_code=500,
|
|
176
|
+
) from e
|
|
177
|
+
|
|
178
|
+
return schema.TableListResult(data=data, total_count=int(total_count or 0))
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from admin_panel import auth, schema
|
|
4
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
5
|
+
from admin_panel.translations import LanguageManager
|
|
6
|
+
from admin_panel.translations import TranslateText as _
|
|
7
|
+
from admin_panel.utils import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLAlchemyAdminRetrieveMixin:
|
|
13
|
+
has_retrieve: bool = True
|
|
14
|
+
|
|
15
|
+
async def retrieve(
|
|
16
|
+
self,
|
|
17
|
+
pk: Any,
|
|
18
|
+
user: auth.UserABC,
|
|
19
|
+
language_manager: LanguageManager,
|
|
20
|
+
) -> schema.RetrieveResult:
|
|
21
|
+
if not self.has_delete:
|
|
22
|
+
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
23
|
+
|
|
24
|
+
# pylint: disable=import-outside-toplevel
|
|
25
|
+
from sqlalchemy import inspect
|
|
26
|
+
|
|
27
|
+
col = inspect(self.model).mapper.columns[self.pk_name]
|
|
28
|
+
python_type = col.type.python_type
|
|
29
|
+
|
|
30
|
+
assert self.pk_name
|
|
31
|
+
stmt = self.get_queryset().where(getattr(self.model, self.pk_name) == python_type(pk))
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
async with self.db_async_session() as session:
|
|
35
|
+
record = (await session.execute(stmt)).scalars().first()
|
|
36
|
+
data = await self.table_schema.serialize(
|
|
37
|
+
record,
|
|
38
|
+
extra={"record": record, "user": user},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.exception(
|
|
43
|
+
'SQLAlchemy %s retrieve db error: %s', type(self).__name__, e,
|
|
44
|
+
)
|
|
45
|
+
raise AdminAPIException(
|
|
46
|
+
APIError(message=_('db_error_retrieve'), code='db_error_retrieve'), status_code=500,
|
|
47
|
+
) from e
|
|
48
|
+
|
|
49
|
+
if record is None:
|
|
50
|
+
msg = _('record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
51
|
+
raise AdminAPIException(
|
|
52
|
+
APIError(message=msg, code='record_not_found'),
|
|
53
|
+
status_code=400,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
logger.debug(
|
|
57
|
+
'%s model %s #%s retrieved by %s',
|
|
58
|
+
type(self).__name__, self.table_schema.model.__name__, pk, user.username,
|
|
59
|
+
extra={'data': data},
|
|
60
|
+
)
|
|
61
|
+
return schema.RetrieveResult(data=data)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from admin_panel import auth, schema
|
|
4
|
+
from admin_panel.exceptions import AdminAPIException, APIError
|
|
5
|
+
from admin_panel.translations import LanguageManager
|
|
6
|
+
from admin_panel.translations import TranslateText as _
|
|
7
|
+
from admin_panel.utils import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SQLAlchemyAdminUpdate:
|
|
13
|
+
has_update: bool = True
|
|
14
|
+
|
|
15
|
+
# pylint: disable=too-many-locals
|
|
16
|
+
async def update(
|
|
17
|
+
self,
|
|
18
|
+
pk: Any,
|
|
19
|
+
data: dict,
|
|
20
|
+
user: auth.UserABC,
|
|
21
|
+
language_manager: LanguageManager,
|
|
22
|
+
) -> schema.UpdateResult:
|
|
23
|
+
if not self.has_update:
|
|
24
|
+
raise AdminAPIException(APIError(message=_('method_not_allowed')), status_code=500)
|
|
25
|
+
|
|
26
|
+
# pylint: disable=import-outside-toplevel
|
|
27
|
+
from sqlalchemy import inspect
|
|
28
|
+
from sqlalchemy.exc import IntegrityError
|
|
29
|
+
|
|
30
|
+
if pk is None:
|
|
31
|
+
raise AdminAPIException(
|
|
32
|
+
APIError(message=_('pk_not_found') % {'pk_name': self.pk_name}, code='pk_not_found'),
|
|
33
|
+
status_code=400,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
col = inspect(self.table_schema.model).mapper.columns[self.pk_name]
|
|
37
|
+
python_type = col.type.python_type
|
|
38
|
+
|
|
39
|
+
stmt = self.get_queryset().where(getattr(self.model, self.pk_name) == python_type(pk))
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
async with self.db_async_session() as session:
|
|
43
|
+
record = (await session.execute(stmt)).scalars().first()
|
|
44
|
+
if record is None:
|
|
45
|
+
msg = _('record_not_found') % {'pk_name': self.pk_name, 'pk': pk}
|
|
46
|
+
raise AdminAPIException(
|
|
47
|
+
APIError(message=msg, code='record_not_found'),
|
|
48
|
+
status_code=400,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await self.table_schema.update(record, user, data, session)
|
|
52
|
+
|
|
53
|
+
except AdminAPIException as e:
|
|
54
|
+
raise e
|
|
55
|
+
|
|
56
|
+
except ConnectionRefusedError as e:
|
|
57
|
+
logger.exception(
|
|
58
|
+
'SQLAlchemy %s update %s #%s db connection error: %s',
|
|
59
|
+
type(self).__name__, self.table_schema.model.__name__, pk, e,
|
|
60
|
+
extra={'data': data},
|
|
61
|
+
)
|
|
62
|
+
msg = _('connection_refused_error') % {'error': str(e)}
|
|
63
|
+
raise AdminAPIException(
|
|
64
|
+
APIError(message=msg, code='connection_refused_error'),
|
|
65
|
+
status_code=400,
|
|
66
|
+
) from e
|
|
67
|
+
|
|
68
|
+
except IntegrityError as e:
|
|
69
|
+
logger.warning(
|
|
70
|
+
'SQLAlchemy %s update %s #%s db error: %s',
|
|
71
|
+
type(self).__name__, self.table_schema.model.__name__, pk, e,
|
|
72
|
+
extra={'data': data},
|
|
73
|
+
)
|
|
74
|
+
orig = e.orig
|
|
75
|
+
message = orig.args[0] if orig.args else type(orig).__name__
|
|
76
|
+
raise AdminAPIException(
|
|
77
|
+
APIError(message=message, code='db_integrity_error'), status_code=500,
|
|
78
|
+
) from e
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.exception(
|
|
82
|
+
'SQLAlchemy %s update %s #%s db error: %s',
|
|
83
|
+
type(self).__name__, self.table_schema.model.__name__, pk, e,
|
|
84
|
+
extra={'data': data}
|
|
85
|
+
)
|
|
86
|
+
raise AdminAPIException(
|
|
87
|
+
APIError(message=_('db_error_update'), code='db_error_update'), status_code=500,
|
|
88
|
+
) from e
|
|
89
|
+
|
|
90
|
+
logger.info(
|
|
91
|
+
'%s model %s #%s updated by %s',
|
|
92
|
+
type(self).__name__, self.table_schema.model.__name__, pk, user.username,
|
|
93
|
+
extra={'data': data},
|
|
94
|
+
)
|
|
95
|
+
return schema.UpdateResult(pk=pk)
|