sqladmin 0.16.1__py3-none-any.whl → 0.18.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.
- sqladmin/__init__.py +1 -1
- sqladmin/_menu.py +11 -9
- sqladmin/_queries.py +10 -8
- sqladmin/ajax.py +5 -3
- sqladmin/application.py +54 -43
- sqladmin/authentication.py +4 -2
- sqladmin/fields.py +35 -33
- sqladmin/forms.py +56 -59
- sqladmin/helpers.py +13 -12
- sqladmin/models.py +98 -20
- sqladmin/pagination.py +5 -3
- sqladmin/templates/{_macros.html → sqladmin/_macros.html} +33 -0
- sqladmin/templates/{create.html → sqladmin/create.html} +3 -16
- sqladmin/templates/{details.html → sqladmin/details.html} +3 -3
- sqladmin/templates/{edit.html → sqladmin/edit.html} +5 -16
- sqladmin/templates/{error.html → sqladmin/error.html} +1 -1
- sqladmin/templates/sqladmin/index.html +3 -0
- sqladmin/templates/{layout.html → sqladmin/layout.html} +2 -2
- sqladmin/templates/{list.html → sqladmin/list.html} +6 -6
- sqladmin/templates/{login.html → sqladmin/login.html} +1 -1
- sqladmin/templating.py +9 -7
- sqladmin/widgets.py +20 -12
- {sqladmin-0.16.1.dist-info → sqladmin-0.18.0.dist-info}/METADATA +2 -2
- sqladmin-0.18.0.dist-info/RECORD +50 -0
- {sqladmin-0.16.1.dist-info → sqladmin-0.18.0.dist-info}/WHEEL +1 -1
- sqladmin/templates/index.html +0 -3
- sqladmin-0.16.1.dist-info/RECORD +0 -50
- /sqladmin/templates/{base.html → sqladmin/base.html} +0 -0
- /sqladmin/templates/{modals → sqladmin/modals}/delete.html +0 -0
- /sqladmin/templates/{modals → sqladmin/modals}/details_action_confirmation.html +0 -0
- /sqladmin/templates/{modals → sqladmin/modals}/list_action_confirmation.html +0 -0
- {sqladmin-0.16.1.dist-info → sqladmin-0.18.0.dist-info}/licenses/LICENSE.md +0 -0
sqladmin/__init__.py
CHANGED
sqladmin/_menu.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
2
4
|
|
|
3
5
|
from starlette.datastructures import URL
|
|
4
6
|
from starlette.requests import Request
|
|
@@ -8,11 +10,11 @@ if TYPE_CHECKING:
|
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class ItemMenu:
|
|
11
|
-
def __init__(self, name: str, icon:
|
|
13
|
+
def __init__(self, name: str, icon: str | None = None) -> None:
|
|
12
14
|
self.name = name
|
|
13
15
|
self.icon = icon
|
|
14
|
-
self.parent:
|
|
15
|
-
self.children:
|
|
16
|
+
self.parent: "ItemMenu" | None = None
|
|
17
|
+
self.children: list["ItemMenu"] = []
|
|
16
18
|
|
|
17
19
|
def add_child(self, item: "ItemMenu") -> None:
|
|
18
20
|
item.parent = self
|
|
@@ -27,7 +29,7 @@ class ItemMenu:
|
|
|
27
29
|
def is_active(self, request: Request) -> bool:
|
|
28
30
|
return False
|
|
29
31
|
|
|
30
|
-
def url(self, request: Request) ->
|
|
32
|
+
def url(self, request: Request) -> str | URL:
|
|
31
33
|
return "#"
|
|
32
34
|
|
|
33
35
|
@property
|
|
@@ -53,9 +55,9 @@ class CategoryMenu(ItemMenu):
|
|
|
53
55
|
class ViewMenu(ItemMenu):
|
|
54
56
|
def __init__(
|
|
55
57
|
self,
|
|
56
|
-
view:
|
|
58
|
+
view: "BaseView" | "ModelView",
|
|
57
59
|
name: str,
|
|
58
|
-
icon:
|
|
60
|
+
icon: str | None = None,
|
|
59
61
|
) -> None:
|
|
60
62
|
super().__init__(name=name, icon=icon)
|
|
61
63
|
self.view = view
|
|
@@ -69,7 +71,7 @@ class ViewMenu(ItemMenu):
|
|
|
69
71
|
def is_active(self, request: Request) -> bool:
|
|
70
72
|
return self.view.identity == request.path_params.get("identity")
|
|
71
73
|
|
|
72
|
-
def url(self, request: Request) ->
|
|
74
|
+
def url(self, request: Request) -> str | URL:
|
|
73
75
|
if self.view.is_model:
|
|
74
76
|
return request.url_for("admin:list", identity=self.view.identity)
|
|
75
77
|
return request.url_for(f"admin:{self.view.identity}")
|
|
@@ -85,7 +87,7 @@ class ViewMenu(ItemMenu):
|
|
|
85
87
|
|
|
86
88
|
class Menu:
|
|
87
89
|
def __init__(self) -> None:
|
|
88
|
-
self.items:
|
|
90
|
+
self.items: list[ItemMenu] = []
|
|
89
91
|
|
|
90
92
|
def add(self, item: ItemMenu) -> None:
|
|
91
93
|
# Only works for one-level menu
|
sqladmin/_queries.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
2
4
|
|
|
3
5
|
import anyio
|
|
4
6
|
from sqlalchemy import select
|
|
5
7
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
6
|
-
from sqlalchemy.orm import Session,
|
|
8
|
+
from sqlalchemy.orm import Session, selectinload
|
|
7
9
|
from sqlalchemy.sql.expression import Select, and_, or_
|
|
8
10
|
from starlette.requests import Request
|
|
9
11
|
|
|
@@ -24,7 +26,7 @@ class Query:
|
|
|
24
26
|
def __init__(self, model_view: "ModelView") -> None:
|
|
25
27
|
self.model_view = model_view
|
|
26
28
|
|
|
27
|
-
def _get_to_many_stmt(self, relation: MODEL_PROPERTY, values:
|
|
29
|
+
def _get_to_many_stmt(self, relation: MODEL_PROPERTY, values: list[Any]) -> Select:
|
|
28
30
|
target = relation.mapper.class_
|
|
29
31
|
|
|
30
32
|
target_pks = get_primary_keys(target)
|
|
@@ -131,7 +133,7 @@ class Query:
|
|
|
131
133
|
setattr(obj, key, value)
|
|
132
134
|
return obj
|
|
133
135
|
|
|
134
|
-
def _update_sync(self, pk: Any, data:
|
|
136
|
+
def _update_sync(self, pk: Any, data: dict[str, Any], request: Request) -> Any:
|
|
135
137
|
stmt = self.model_view._stmt_by_identifier(pk)
|
|
136
138
|
|
|
137
139
|
with self.model_view.session_maker(expire_on_commit=False) as session:
|
|
@@ -147,12 +149,12 @@ class Query:
|
|
|
147
149
|
return obj
|
|
148
150
|
|
|
149
151
|
async def _update_async(
|
|
150
|
-
self, pk: Any, data:
|
|
152
|
+
self, pk: Any, data: dict[str, Any], request: Request
|
|
151
153
|
) -> Any:
|
|
152
154
|
stmt = self.model_view._stmt_by_identifier(pk)
|
|
153
155
|
|
|
154
156
|
for relation in self.model_view._form_relations:
|
|
155
|
-
stmt = stmt.options(
|
|
157
|
+
stmt = stmt.options(selectinload(relation))
|
|
156
158
|
|
|
157
159
|
async with self.model_view.session_maker(expire_on_commit=False) as session:
|
|
158
160
|
result = await session.execute(stmt)
|
|
@@ -187,7 +189,7 @@ class Query:
|
|
|
187
189
|
await session.commit()
|
|
188
190
|
await self.model_view.after_model_delete(obj, request)
|
|
189
191
|
|
|
190
|
-
def _insert_sync(self, data:
|
|
192
|
+
def _insert_sync(self, data: dict[str, Any], request: Request) -> Any:
|
|
191
193
|
obj = self.model_view.model()
|
|
192
194
|
|
|
193
195
|
with self.model_view.session_maker(expire_on_commit=False) as session:
|
|
@@ -202,7 +204,7 @@ class Query:
|
|
|
202
204
|
)
|
|
203
205
|
return obj
|
|
204
206
|
|
|
205
|
-
async def _insert_async(self, data:
|
|
207
|
+
async def _insert_async(self, data: dict[str, Any], request: Request) -> Any:
|
|
206
208
|
obj = self.model_view.model()
|
|
207
209
|
|
|
208
210
|
async with self.model_view.session_maker(expire_on_commit=False) as session:
|
sqladmin/ajax.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
2
4
|
|
|
3
5
|
from sqlalchemy import String, cast, inspect, or_, select
|
|
4
6
|
|
|
@@ -52,13 +54,13 @@ class QueryAjaxModelLoader:
|
|
|
52
54
|
|
|
53
55
|
return remote_fields
|
|
54
56
|
|
|
55
|
-
def format(self, model: type) ->
|
|
57
|
+
def format(self, model: type) -> dict[str, Any]:
|
|
56
58
|
if not model:
|
|
57
59
|
return {}
|
|
58
60
|
|
|
59
61
|
return {"id": str(get_object_identifier(model)), "text": str(model)}
|
|
60
62
|
|
|
61
|
-
async def get_list(self, term: str, limit: int = DEFAULT_PAGE_SIZE) ->
|
|
63
|
+
async def get_list(self, term: str, limit: int = DEFAULT_PAGE_SIZE) -> list[Any]:
|
|
62
64
|
stmt = select(self.model)
|
|
63
65
|
|
|
64
66
|
# no type casting to string if a ColumnAssociationProxyInstance is given
|
sqladmin/application.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
import io
|
|
3
5
|
import logging
|
|
@@ -7,23 +9,18 @@ from typing import (
|
|
|
7
9
|
Any,
|
|
8
10
|
Awaitable,
|
|
9
11
|
Callable,
|
|
10
|
-
List,
|
|
11
|
-
Optional,
|
|
12
12
|
Sequence,
|
|
13
|
-
Tuple,
|
|
14
|
-
Type,
|
|
15
|
-
Union,
|
|
16
13
|
cast,
|
|
17
14
|
no_type_check,
|
|
18
15
|
)
|
|
19
|
-
from urllib.parse import urljoin
|
|
16
|
+
from urllib.parse import parse_qsl, urljoin
|
|
20
17
|
|
|
21
18
|
from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader
|
|
22
19
|
from sqlalchemy.engine import Engine
|
|
23
20
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
24
21
|
from sqlalchemy.orm import Session, sessionmaker
|
|
25
22
|
from starlette.applications import Starlette
|
|
26
|
-
from starlette.datastructures import URL, FormData, UploadFile
|
|
23
|
+
from starlette.datastructures import URL, FormData, MultiDict, UploadFile
|
|
27
24
|
from starlette.exceptions import HTTPException
|
|
28
25
|
from starlette.middleware import Middleware
|
|
29
26
|
from starlette.requests import Request
|
|
@@ -66,14 +63,14 @@ class BaseAdmin:
|
|
|
66
63
|
def __init__(
|
|
67
64
|
self,
|
|
68
65
|
app: Starlette,
|
|
69
|
-
engine:
|
|
70
|
-
session_maker:
|
|
66
|
+
engine: ENGINE_TYPE | None = None,
|
|
67
|
+
session_maker: sessionmaker | None = None,
|
|
71
68
|
base_url: str = "/admin",
|
|
72
69
|
title: str = "Admin",
|
|
73
|
-
logo_url:
|
|
70
|
+
logo_url: str | None = None,
|
|
74
71
|
templates_dir: str = "templates",
|
|
75
|
-
middlewares:
|
|
76
|
-
authentication_backend:
|
|
72
|
+
middlewares: Sequence[Middleware] | None = None,
|
|
73
|
+
authentication_backend: AuthenticationBackend | None = None,
|
|
77
74
|
) -> None:
|
|
78
75
|
self.app = app
|
|
79
76
|
self.engine = engine
|
|
@@ -100,7 +97,7 @@ class BaseAdmin:
|
|
|
100
97
|
|
|
101
98
|
self.admin = Starlette(middleware=middlewares)
|
|
102
99
|
self.templates = self.init_templating_engine()
|
|
103
|
-
self._views:
|
|
100
|
+
self._views: list[BaseView | ModelView] = []
|
|
104
101
|
self._menu = Menu()
|
|
105
102
|
|
|
106
103
|
def init_templating_engine(self) -> Jinja2Templates:
|
|
@@ -120,7 +117,7 @@ class BaseAdmin:
|
|
|
120
117
|
return templates
|
|
121
118
|
|
|
122
119
|
@property
|
|
123
|
-
def views(self) ->
|
|
120
|
+
def views(self) -> list[BaseView | ModelView]:
|
|
124
121
|
"""Get list of ModelView and BaseView instances lazily.
|
|
125
122
|
|
|
126
123
|
Returns:
|
|
@@ -136,7 +133,7 @@ class BaseAdmin:
|
|
|
136
133
|
|
|
137
134
|
raise HTTPException(status_code=404)
|
|
138
135
|
|
|
139
|
-
def add_view(self, view:
|
|
136
|
+
def add_view(self, view: type[ModelView] | type[BaseView]) -> None:
|
|
140
137
|
"""Add ModelView or BaseView classes to Admin.
|
|
141
138
|
This is a shortcut that will handle both `add_model_view` and `add_base_view`.
|
|
142
139
|
"""
|
|
@@ -149,10 +146,10 @@ class BaseAdmin:
|
|
|
149
146
|
|
|
150
147
|
def _find_decorated_funcs(
|
|
151
148
|
self,
|
|
152
|
-
view:
|
|
153
|
-
view_instance:
|
|
149
|
+
view: type[BaseView | ModelView],
|
|
150
|
+
view_instance: BaseView | ModelView,
|
|
154
151
|
handle_fn: Callable[
|
|
155
|
-
[MethodType,
|
|
152
|
+
[MethodType, type[BaseView | ModelView], BaseView | ModelView],
|
|
156
153
|
None,
|
|
157
154
|
],
|
|
158
155
|
) -> None:
|
|
@@ -164,8 +161,8 @@ class BaseAdmin:
|
|
|
164
161
|
def _handle_action_decorated_func(
|
|
165
162
|
self,
|
|
166
163
|
func: MethodType,
|
|
167
|
-
view:
|
|
168
|
-
view_instance:
|
|
164
|
+
view: type[BaseView | ModelView],
|
|
165
|
+
view_instance: BaseView | ModelView,
|
|
169
166
|
) -> None:
|
|
170
167
|
if hasattr(func, "_action"):
|
|
171
168
|
view_instance = cast(ModelView, view_instance)
|
|
@@ -194,8 +191,8 @@ class BaseAdmin:
|
|
|
194
191
|
def _handle_expose_decorated_func(
|
|
195
192
|
self,
|
|
196
193
|
func: MethodType,
|
|
197
|
-
view:
|
|
198
|
-
view_instance:
|
|
194
|
+
view: type[BaseView | ModelView],
|
|
195
|
+
view_instance: BaseView | ModelView,
|
|
199
196
|
) -> None:
|
|
200
197
|
if hasattr(func, "_exposed"):
|
|
201
198
|
self.admin.add_route(
|
|
@@ -208,7 +205,7 @@ class BaseAdmin:
|
|
|
208
205
|
|
|
209
206
|
view.identity = getattr(func, "_identity")
|
|
210
207
|
|
|
211
|
-
def add_model_view(self, view:
|
|
208
|
+
def add_model_view(self, view: type[ModelView]) -> None:
|
|
212
209
|
"""Add ModelView to the Admin.
|
|
213
210
|
|
|
214
211
|
???+ usage
|
|
@@ -237,7 +234,7 @@ class BaseAdmin:
|
|
|
237
234
|
self._views.append(view_instance)
|
|
238
235
|
self._build_menu(view_instance)
|
|
239
236
|
|
|
240
|
-
def add_base_view(self, view:
|
|
237
|
+
def add_base_view(self, view: type[BaseView]) -> None:
|
|
241
238
|
"""Add BaseView to the Admin.
|
|
242
239
|
|
|
243
240
|
???+ usage
|
|
@@ -265,7 +262,7 @@ class BaseAdmin:
|
|
|
265
262
|
self._views.append(view_instance)
|
|
266
263
|
self._build_menu(view_instance)
|
|
267
264
|
|
|
268
|
-
def _build_menu(self, view:
|
|
265
|
+
def _build_menu(self, view: ModelView | BaseView) -> None:
|
|
269
266
|
if view.category:
|
|
270
267
|
menu = CategoryMenu(name=view.category)
|
|
271
268
|
menu.add_child(ViewMenu(view=view, name=view.name, icon=view.icon))
|
|
@@ -338,15 +335,15 @@ class Admin(BaseAdminView):
|
|
|
338
335
|
def __init__(
|
|
339
336
|
self,
|
|
340
337
|
app: Starlette,
|
|
341
|
-
engine:
|
|
342
|
-
session_maker:
|
|
338
|
+
engine: ENGINE_TYPE | None = None,
|
|
339
|
+
session_maker: sessionmaker | "async_sessionmaker" | None = None,
|
|
343
340
|
base_url: str = "/admin",
|
|
344
341
|
title: str = "Admin",
|
|
345
|
-
logo_url:
|
|
346
|
-
middlewares:
|
|
342
|
+
logo_url: str | None = None,
|
|
343
|
+
middlewares: Sequence[Middleware] | None = None,
|
|
347
344
|
debug: bool = False,
|
|
348
345
|
templates_dir: str = "templates",
|
|
349
|
-
authentication_backend:
|
|
346
|
+
authentication_backend: AuthenticationBackend | None = None,
|
|
350
347
|
) -> None:
|
|
351
348
|
"""
|
|
352
349
|
Args:
|
|
@@ -374,14 +371,14 @@ class Admin(BaseAdminView):
|
|
|
374
371
|
|
|
375
372
|
async def http_exception(
|
|
376
373
|
request: Request, exc: Exception
|
|
377
|
-
) ->
|
|
374
|
+
) -> Response | Awaitable[Response]:
|
|
378
375
|
assert isinstance(exc, HTTPException)
|
|
379
376
|
context = {
|
|
380
377
|
"status_code": exc.status_code,
|
|
381
378
|
"message": exc.detail,
|
|
382
379
|
}
|
|
383
380
|
return await self.templates.TemplateResponse(
|
|
384
|
-
request, "error.html", context, status_code=exc.status_code
|
|
381
|
+
request, "sqladmin/error.html", context, status_code=exc.status_code
|
|
385
382
|
)
|
|
386
383
|
|
|
387
384
|
routes = [
|
|
@@ -428,7 +425,7 @@ class Admin(BaseAdminView):
|
|
|
428
425
|
async def index(self, request: Request) -> Response:
|
|
429
426
|
"""Index route which can be overridden to create dashboards."""
|
|
430
427
|
|
|
431
|
-
return await self.templates.TemplateResponse(request, "index.html")
|
|
428
|
+
return await self.templates.TemplateResponse(request, "sqladmin/index.html")
|
|
432
429
|
|
|
433
430
|
@login_required
|
|
434
431
|
async def list(self, request: Request) -> Response:
|
|
@@ -440,6 +437,14 @@ class Admin(BaseAdminView):
|
|
|
440
437
|
pagination = await model_view.list(request)
|
|
441
438
|
pagination.add_pagination_urls(request.url)
|
|
442
439
|
|
|
440
|
+
if (
|
|
441
|
+
pagination.page * pagination.page_size
|
|
442
|
+
> pagination.count + pagination.page_size
|
|
443
|
+
):
|
|
444
|
+
raise HTTPException(
|
|
445
|
+
status_code=400, detail="Invalid page or pageSize parameter"
|
|
446
|
+
)
|
|
447
|
+
|
|
443
448
|
context = {"model_view": model_view, "pagination": pagination}
|
|
444
449
|
return await self.templates.TemplateResponse(
|
|
445
450
|
request, model_view.list_template, context
|
|
@@ -485,7 +490,11 @@ class Admin(BaseAdminView):
|
|
|
485
490
|
|
|
486
491
|
await model_view.delete_model(request, pk)
|
|
487
492
|
|
|
488
|
-
|
|
493
|
+
referer_url = URL(request.headers.get("referer", ""))
|
|
494
|
+
referer_params = MultiDict(parse_qsl(referer_url.query))
|
|
495
|
+
url = URL(str(request.url_for("admin:list", identity=identity)))
|
|
496
|
+
url = url.include_query_params(**referer_params)
|
|
497
|
+
return Response(content=str(url))
|
|
489
498
|
|
|
490
499
|
@login_required
|
|
491
500
|
async def create(self, request: Request) -> Response:
|
|
@@ -497,6 +506,7 @@ class Admin(BaseAdminView):
|
|
|
497
506
|
model_view = self._find_model_view(identity)
|
|
498
507
|
|
|
499
508
|
Form = await model_view.scaffold_form()
|
|
509
|
+
model_view._validate_form_class(model_view._form_create_rules, Form)
|
|
500
510
|
form_data = await self._handle_form_data(request)
|
|
501
511
|
form = Form(form_data)
|
|
502
512
|
|
|
@@ -542,11 +552,12 @@ class Admin(BaseAdminView):
|
|
|
542
552
|
identity = request.path_params["identity"]
|
|
543
553
|
model_view = self._find_model_view(identity)
|
|
544
554
|
|
|
545
|
-
model = await model_view.get_object_for_edit(request
|
|
555
|
+
model = await model_view.get_object_for_edit(request)
|
|
546
556
|
if not model:
|
|
547
557
|
raise HTTPException(status_code=404)
|
|
548
558
|
|
|
549
559
|
Form = await model_view.scaffold_form()
|
|
560
|
+
model_view._validate_form_class(model_view._form_edit_rules, Form)
|
|
550
561
|
context = {
|
|
551
562
|
"obj": model,
|
|
552
563
|
"model_view": model_view,
|
|
@@ -609,13 +620,13 @@ class Admin(BaseAdminView):
|
|
|
609
620
|
|
|
610
621
|
context = {}
|
|
611
622
|
if request.method == "GET":
|
|
612
|
-
return await self.templates.TemplateResponse(request, "login.html")
|
|
623
|
+
return await self.templates.TemplateResponse(request, "sqladmin/login.html")
|
|
613
624
|
|
|
614
625
|
ok = await self.authentication_backend.login(request)
|
|
615
626
|
if not ok:
|
|
616
627
|
context["error"] = "Invalid credentials."
|
|
617
628
|
return await self.templates.TemplateResponse(
|
|
618
|
-
request, "login.html", context, status_code=400
|
|
629
|
+
request, "sqladmin/login.html", context, status_code=400
|
|
619
630
|
)
|
|
620
631
|
|
|
621
632
|
return RedirectResponse(request.url_for("admin:index"), status_code=302)
|
|
@@ -648,7 +659,7 @@ class Admin(BaseAdminView):
|
|
|
648
659
|
|
|
649
660
|
def get_save_redirect_url(
|
|
650
661
|
self, request: Request, form: FormData, model_view: ModelView, obj: Any
|
|
651
|
-
) ->
|
|
662
|
+
) -> str | URL:
|
|
652
663
|
"""
|
|
653
664
|
Get the redirect URL after a save action
|
|
654
665
|
which is triggered from create/edit page.
|
|
@@ -673,7 +684,7 @@ class Admin(BaseAdminView):
|
|
|
673
684
|
"""
|
|
674
685
|
|
|
675
686
|
form = await request.form()
|
|
676
|
-
form_data:
|
|
687
|
+
form_data: list[tuple[str, str | UploadFile]] = []
|
|
677
688
|
for key, value in form.multi_items():
|
|
678
689
|
if not isinstance(value, UploadFile):
|
|
679
690
|
form_data.append((key, value))
|
|
@@ -714,8 +725,8 @@ class Admin(BaseAdminView):
|
|
|
714
725
|
def expose(
|
|
715
726
|
path: str,
|
|
716
727
|
*,
|
|
717
|
-
methods:
|
|
718
|
-
identity:
|
|
728
|
+
methods: list[str] = ["GET"],
|
|
729
|
+
identity: str | None = None,
|
|
719
730
|
include_in_schema: bool = True,
|
|
720
731
|
) -> Callable[..., Any]:
|
|
721
732
|
"""Expose View with information."""
|
|
@@ -734,8 +745,8 @@ def expose(
|
|
|
734
745
|
|
|
735
746
|
def action(
|
|
736
747
|
name: str,
|
|
737
|
-
label:
|
|
738
|
-
confirmation_message:
|
|
748
|
+
label: str | None = None,
|
|
749
|
+
confirmation_message: str | None = None,
|
|
739
750
|
*,
|
|
740
751
|
include_in_schema: bool = True,
|
|
741
752
|
add_in_detail: bool = True,
|
sqladmin/authentication.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import functools
|
|
2
4
|
import inspect
|
|
3
|
-
from typing import Any, Callable
|
|
5
|
+
from typing import Any, Callable
|
|
4
6
|
|
|
5
7
|
from starlette.middleware import Middleware
|
|
6
8
|
from starlette.requests import Request
|
|
@@ -33,7 +35,7 @@ class AuthenticationBackend:
|
|
|
33
35
|
"""
|
|
34
36
|
raise NotImplementedError()
|
|
35
37
|
|
|
36
|
-
async def authenticate(self, request: Request) ->
|
|
38
|
+
async def authenticate(self, request: Request) -> Response | bool:
|
|
37
39
|
"""Implement authenticate logic here.
|
|
38
40
|
This method will be called for each incoming request
|
|
39
41
|
to validate the authentication.
|
sqladmin/fields.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import operator
|
|
3
|
-
from typing import Any, Callable,
|
|
5
|
+
from typing import Any, Callable, Generator
|
|
4
6
|
|
|
5
7
|
from wtforms import Form, ValidationError, fields, widgets
|
|
6
8
|
|
|
@@ -43,7 +45,7 @@ class IntervalField(fields.StringField):
|
|
|
43
45
|
A text field which stores a `datetime.timedelta` object.
|
|
44
46
|
"""
|
|
45
47
|
|
|
46
|
-
def process_formdata(self, valuelist:
|
|
48
|
+
def process_formdata(self, valuelist: list[str]) -> None:
|
|
47
49
|
if not valuelist:
|
|
48
50
|
return
|
|
49
51
|
|
|
@@ -57,19 +59,19 @@ class IntervalField(fields.StringField):
|
|
|
57
59
|
class SelectField(fields.SelectField):
|
|
58
60
|
def __init__(
|
|
59
61
|
self,
|
|
60
|
-
label:
|
|
61
|
-
validators:
|
|
62
|
+
label: str | None = None,
|
|
63
|
+
validators: list | None = None,
|
|
62
64
|
coerce: type = str,
|
|
63
|
-
choices:
|
|
65
|
+
choices: list | Callable | None = None,
|
|
64
66
|
allow_blank: bool = False,
|
|
65
|
-
blank_text:
|
|
67
|
+
blank_text: str | None = None,
|
|
66
68
|
**kwargs: Any,
|
|
67
69
|
) -> None:
|
|
68
70
|
super().__init__(label, validators, coerce, choices, **kwargs)
|
|
69
71
|
self.allow_blank = allow_blank
|
|
70
72
|
self.blank_text = blank_text or " "
|
|
71
73
|
|
|
72
|
-
def iter_choices(self) -> Generator[
|
|
74
|
+
def iter_choices(self) -> Generator[tuple[str, str, bool, dict], None, None]:
|
|
73
75
|
choices = self.choices or []
|
|
74
76
|
|
|
75
77
|
if self.allow_blank:
|
|
@@ -86,7 +88,7 @@ class SelectField(fields.SelectField):
|
|
|
86
88
|
{},
|
|
87
89
|
)
|
|
88
90
|
|
|
89
|
-
def process_formdata(self, valuelist:
|
|
91
|
+
def process_formdata(self, valuelist: list[str]) -> None:
|
|
90
92
|
if valuelist:
|
|
91
93
|
if valuelist[0] == "__None":
|
|
92
94
|
self.data = None
|
|
@@ -112,7 +114,7 @@ class JSONField(fields.TextAreaField):
|
|
|
112
114
|
else:
|
|
113
115
|
return "{}"
|
|
114
116
|
|
|
115
|
-
def process_formdata(self, valuelist:
|
|
117
|
+
def process_formdata(self, valuelist: list[str]) -> None:
|
|
116
118
|
if valuelist:
|
|
117
119
|
value = valuelist[0]
|
|
118
120
|
|
|
@@ -132,10 +134,10 @@ class QuerySelectField(fields.SelectFieldBase):
|
|
|
132
134
|
|
|
133
135
|
def __init__(
|
|
134
136
|
self,
|
|
135
|
-
data:
|
|
136
|
-
label:
|
|
137
|
-
validators:
|
|
138
|
-
get_label:
|
|
137
|
+
data: list | None = None,
|
|
138
|
+
label: str | None = None,
|
|
139
|
+
validators: list | None = None,
|
|
140
|
+
get_label: Callable | str | None = None,
|
|
139
141
|
allow_blank: bool = False,
|
|
140
142
|
blank_text: str = "",
|
|
141
143
|
**kwargs: Any,
|
|
@@ -153,11 +155,11 @@ class QuerySelectField(fields.SelectFieldBase):
|
|
|
153
155
|
|
|
154
156
|
self.allow_blank = allow_blank
|
|
155
157
|
self.blank_text = blank_text
|
|
156
|
-
self._data:
|
|
157
|
-
self._formdata:
|
|
158
|
+
self._data: tuple | None
|
|
159
|
+
self._formdata: str | list[str] | None
|
|
158
160
|
|
|
159
161
|
@property
|
|
160
|
-
def data(self) ->
|
|
162
|
+
def data(self) -> tuple | None:
|
|
161
163
|
if self._formdata is not None:
|
|
162
164
|
for pk, _ in self._select_data:
|
|
163
165
|
if pk == self._formdata:
|
|
@@ -170,7 +172,7 @@ class QuerySelectField(fields.SelectFieldBase):
|
|
|
170
172
|
self._data = data
|
|
171
173
|
self._formdata = None
|
|
172
174
|
|
|
173
|
-
def iter_choices(self) -> Generator[
|
|
175
|
+
def iter_choices(self) -> Generator[tuple[str, str, bool, dict], None, None]:
|
|
174
176
|
if self.allow_blank:
|
|
175
177
|
yield ("__None", self.blank_text, self.data is None, {})
|
|
176
178
|
|
|
@@ -186,7 +188,7 @@ class QuerySelectField(fields.SelectFieldBase):
|
|
|
186
188
|
for pk, label in self._select_data:
|
|
187
189
|
yield (pk, self.get_label(label), str(pk) == primary_key, {})
|
|
188
190
|
|
|
189
|
-
def process_formdata(self, valuelist:
|
|
191
|
+
def process_formdata(self, valuelist: list[str]) -> None:
|
|
190
192
|
if valuelist:
|
|
191
193
|
if self.allow_blank and valuelist[0] == "__None":
|
|
192
194
|
self.data = None
|
|
@@ -220,9 +222,9 @@ class QuerySelectMultipleField(QuerySelectField):
|
|
|
220
222
|
|
|
221
223
|
def __init__(
|
|
222
224
|
self,
|
|
223
|
-
data:
|
|
224
|
-
label:
|
|
225
|
-
validators:
|
|
225
|
+
data: list | None = None,
|
|
226
|
+
label: str | None = None,
|
|
227
|
+
validators: list | None = None,
|
|
226
228
|
default: Any = None,
|
|
227
229
|
**kwargs: Any,
|
|
228
230
|
) -> None:
|
|
@@ -238,11 +240,11 @@ class QuerySelectMultipleField(QuerySelectField):
|
|
|
238
240
|
"allow_blank=True does not do anything for QuerySelectMultipleField."
|
|
239
241
|
)
|
|
240
242
|
self._invalid_formdata = False
|
|
241
|
-
self._formdata:
|
|
242
|
-
self._data:
|
|
243
|
+
self._formdata: list[str] | None = None
|
|
244
|
+
self._data: tuple | None = None
|
|
243
245
|
|
|
244
246
|
@property
|
|
245
|
-
def data(self) ->
|
|
247
|
+
def data(self) -> tuple | None:
|
|
246
248
|
formdata = self._formdata
|
|
247
249
|
if formdata is not None:
|
|
248
250
|
data = []
|
|
@@ -262,7 +264,7 @@ class QuerySelectMultipleField(QuerySelectField):
|
|
|
262
264
|
self._data = data
|
|
263
265
|
self._formdata = None
|
|
264
266
|
|
|
265
|
-
def iter_choices(self) -> Generator[
|
|
267
|
+
def iter_choices(self) -> Generator[tuple[str, Any, bool, dict], None, None]:
|
|
266
268
|
if self.data is not None:
|
|
267
269
|
primary_keys = (
|
|
268
270
|
self.data
|
|
@@ -272,7 +274,7 @@ class QuerySelectMultipleField(QuerySelectField):
|
|
|
272
274
|
for pk, label in self._select_data:
|
|
273
275
|
yield (pk, self.get_label(label), pk in primary_keys, {})
|
|
274
276
|
|
|
275
|
-
def process_formdata(self, valuelist:
|
|
277
|
+
def process_formdata(self, valuelist: list[str]) -> None:
|
|
276
278
|
self._formdata = list(set(valuelist))
|
|
277
279
|
|
|
278
280
|
def pre_validate(self, form: Form) -> None:
|
|
@@ -292,8 +294,8 @@ class AjaxSelectField(fields.SelectFieldBase):
|
|
|
292
294
|
def __init__(
|
|
293
295
|
self,
|
|
294
296
|
loader: QueryAjaxModelLoader,
|
|
295
|
-
label:
|
|
296
|
-
validators:
|
|
297
|
+
label: str | None = None,
|
|
298
|
+
validators: list | None = None,
|
|
297
299
|
allow_blank: bool = False,
|
|
298
300
|
**kwargs: Any,
|
|
299
301
|
) -> None:
|
|
@@ -334,9 +336,9 @@ class AjaxSelectMultipleField(fields.SelectFieldBase):
|
|
|
334
336
|
def __init__(
|
|
335
337
|
self,
|
|
336
338
|
loader: QueryAjaxModelLoader,
|
|
337
|
-
label:
|
|
338
|
-
validators:
|
|
339
|
-
default:
|
|
339
|
+
label: str | None = None,
|
|
340
|
+
validators: list | None = None,
|
|
341
|
+
default: list | None = None,
|
|
340
342
|
allow_blank: bool = False,
|
|
341
343
|
**kwargs: Any,
|
|
342
344
|
) -> None:
|
|
@@ -344,7 +346,7 @@ class AjaxSelectMultipleField(fields.SelectFieldBase):
|
|
|
344
346
|
self.loader = loader
|
|
345
347
|
self.allow_blank = allow_blank
|
|
346
348
|
default = default or []
|
|
347
|
-
self._formdata:
|
|
349
|
+
self._formdata: set[Any] = set()
|
|
348
350
|
|
|
349
351
|
super().__init__(label, validators, default=default, **kwargs)
|
|
350
352
|
|
|
@@ -377,7 +379,7 @@ class Select2TagsField(fields.SelectField):
|
|
|
377
379
|
def process_formdata(self, valuelist: list) -> None:
|
|
378
380
|
self.data = valuelist
|
|
379
381
|
|
|
380
|
-
def process_data(self, value:
|
|
382
|
+
def process_data(self, value: list | None) -> None:
|
|
381
383
|
self.data = value or []
|
|
382
384
|
|
|
383
385
|
|