sqladmin 0.18.0__py3-none-any.whl → 0.20.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/ajax.py +8 -3
- sqladmin/application.py +14 -10
- sqladmin/forms.py +11 -3
- sqladmin/models.py +38 -5
- sqladmin/pagination.py +9 -0
- sqladmin/statics/css/tabler-icons.min.css +4 -0
- sqladmin/statics/css/tabler-icons.min.css.map +1 -0
- sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
- sqladmin/templates/sqladmin/base.html +4 -0
- sqladmin/templates/sqladmin/list.html +1 -1
- sqladmin/widgets.py +4 -1
- {sqladmin-0.18.0.dist-info → sqladmin-0.20.0.dist-info}/METADATA +1 -1
- {sqladmin-0.18.0.dist-info → sqladmin-0.20.0.dist-info}/RECORD +16 -13
- {sqladmin-0.18.0.dist-info → sqladmin-0.20.0.dist-info}/WHEEL +0 -0
- {sqladmin-0.18.0.dist-info → sqladmin-0.20.0.dist-info}/licenses/LICENSE.md +0 -0
sqladmin/__init__.py
CHANGED
sqladmin/ajax.py
CHANGED
|
@@ -26,6 +26,7 @@ class QueryAjaxModelLoader:
|
|
|
26
26
|
self.model_admin = model_admin
|
|
27
27
|
self.fields = options.get("fields", {})
|
|
28
28
|
self.order_by = options.get("order_by")
|
|
29
|
+
self.limit = options.get("limit", DEFAULT_PAGE_SIZE)
|
|
29
30
|
|
|
30
31
|
pks = get_primary_keys(self.model)
|
|
31
32
|
self.pk = pks[0] if len(pks) == 1 else None
|
|
@@ -60,7 +61,7 @@ class QueryAjaxModelLoader:
|
|
|
60
61
|
|
|
61
62
|
return {"id": str(get_object_identifier(model)), "text": str(model)}
|
|
62
63
|
|
|
63
|
-
async def get_list(self, term: str
|
|
64
|
+
async def get_list(self, term: str) -> list[Any]:
|
|
64
65
|
stmt = select(self.model)
|
|
65
66
|
|
|
66
67
|
# no type casting to string if a ColumnAssociationProxyInstance is given
|
|
@@ -71,9 +72,13 @@ class QueryAjaxModelLoader:
|
|
|
71
72
|
stmt = stmt.filter(or_(*filters))
|
|
72
73
|
|
|
73
74
|
if self.order_by:
|
|
74
|
-
|
|
75
|
+
if isinstance(self.order_by, list):
|
|
76
|
+
for o in self.order_by:
|
|
77
|
+
stmt = stmt.order_by(o)
|
|
78
|
+
else:
|
|
79
|
+
stmt = stmt.order_by(self.order_by)
|
|
75
80
|
|
|
76
|
-
stmt = stmt.limit(limit)
|
|
81
|
+
stmt = stmt.limit(self.limit)
|
|
77
82
|
result = await self.model_admin._run_query(stmt)
|
|
78
83
|
return result
|
|
79
84
|
|
sqladmin/application.py
CHANGED
|
@@ -68,6 +68,7 @@ class BaseAdmin:
|
|
|
68
68
|
base_url: str = "/admin",
|
|
69
69
|
title: str = "Admin",
|
|
70
70
|
logo_url: str | None = None,
|
|
71
|
+
favicon_url: str | None = None,
|
|
71
72
|
templates_dir: str = "templates",
|
|
72
73
|
middlewares: Sequence[Middleware] | None = None,
|
|
73
74
|
authentication_backend: AuthenticationBackend | None = None,
|
|
@@ -78,6 +79,7 @@ class BaseAdmin:
|
|
|
78
79
|
self.templates_dir = templates_dir
|
|
79
80
|
self.title = title
|
|
80
81
|
self.logo_url = logo_url
|
|
82
|
+
self.favicon_url = favicon_url
|
|
81
83
|
|
|
82
84
|
if session_maker:
|
|
83
85
|
self.session_maker = session_maker
|
|
@@ -340,6 +342,7 @@ class Admin(BaseAdminView):
|
|
|
340
342
|
base_url: str = "/admin",
|
|
341
343
|
title: str = "Admin",
|
|
342
344
|
logo_url: str | None = None,
|
|
345
|
+
favicon_url: str | None = None,
|
|
343
346
|
middlewares: Sequence[Middleware] | None = None,
|
|
344
347
|
debug: bool = False,
|
|
345
348
|
templates_dir: str = "templates",
|
|
@@ -353,6 +356,7 @@ class Admin(BaseAdminView):
|
|
|
353
356
|
base_url: Base URL for Admin interface.
|
|
354
357
|
title: Admin title.
|
|
355
358
|
logo_url: URL of logo to be displayed instead of title.
|
|
359
|
+
favicon_url: URL of favicon to be displayed.
|
|
356
360
|
"""
|
|
357
361
|
|
|
358
362
|
super().__init__(
|
|
@@ -362,6 +366,7 @@ class Admin(BaseAdminView):
|
|
|
362
366
|
base_url=base_url,
|
|
363
367
|
title=title,
|
|
364
368
|
logo_url=logo_url,
|
|
369
|
+
favicon_url=favicon_url,
|
|
365
370
|
templates_dir=templates_dir,
|
|
366
371
|
middlewares=middlewares,
|
|
367
372
|
authentication_backend=authentication_backend,
|
|
@@ -437,12 +442,13 @@ class Admin(BaseAdminView):
|
|
|
437
442
|
pagination = await model_view.list(request)
|
|
438
443
|
pagination.add_pagination_urls(request.url)
|
|
439
444
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
445
|
+
request_page = model_view.validate_page_number(
|
|
446
|
+
request.query_params.get("page"), 1
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
if request_page > pagination.page:
|
|
450
|
+
return RedirectResponse(
|
|
451
|
+
request.url.include_query_params(page=pagination.page), status_code=302
|
|
446
452
|
)
|
|
447
453
|
|
|
448
454
|
context = {"model_view": model_view, "pagination": pagination}
|
|
@@ -505,8 +511,7 @@ class Admin(BaseAdminView):
|
|
|
505
511
|
identity = request.path_params["identity"]
|
|
506
512
|
model_view = self._find_model_view(identity)
|
|
507
513
|
|
|
508
|
-
Form = await model_view.scaffold_form()
|
|
509
|
-
model_view._validate_form_class(model_view._form_create_rules, Form)
|
|
514
|
+
Form = await model_view.scaffold_form(model_view._form_create_rules)
|
|
510
515
|
form_data = await self._handle_form_data(request)
|
|
511
516
|
form = Form(form_data)
|
|
512
517
|
|
|
@@ -556,8 +561,7 @@ class Admin(BaseAdminView):
|
|
|
556
561
|
if not model:
|
|
557
562
|
raise HTTPException(status_code=404)
|
|
558
563
|
|
|
559
|
-
Form = await model_view.scaffold_form()
|
|
560
|
-
model_view._validate_form_class(model_view._form_edit_rules, Form)
|
|
564
|
+
Form = await model_view.scaffold_form(model_view._form_edit_rules)
|
|
561
565
|
context = {
|
|
562
566
|
"obj": model,
|
|
563
567
|
"model_view": model_view,
|
sqladmin/forms.py
CHANGED
|
@@ -18,7 +18,12 @@ from typing import (
|
|
|
18
18
|
import anyio
|
|
19
19
|
from sqlalchemy import Boolean, select
|
|
20
20
|
from sqlalchemy import inspect as sqlalchemy_inspect
|
|
21
|
-
from sqlalchemy.orm import
|
|
21
|
+
from sqlalchemy.orm import (
|
|
22
|
+
ColumnProperty,
|
|
23
|
+
RelationshipProperty,
|
|
24
|
+
sessionmaker,
|
|
25
|
+
)
|
|
26
|
+
from sqlalchemy.sql.elements import Label
|
|
22
27
|
from sqlalchemy.sql.schema import Column
|
|
23
28
|
from wtforms import (
|
|
24
29
|
BooleanField,
|
|
@@ -612,9 +617,12 @@ async def get_model_form(
|
|
|
612
617
|
attributes = []
|
|
613
618
|
names = only or mapper.attrs.keys()
|
|
614
619
|
for name in names:
|
|
615
|
-
|
|
620
|
+
attr = mapper.attrs[name]
|
|
621
|
+
if (exclude and name in exclude) or (
|
|
622
|
+
isinstance(attr, ColumnProperty) and isinstance(attr.expression, Label)
|
|
623
|
+
):
|
|
616
624
|
continue
|
|
617
|
-
attributes.append((name,
|
|
625
|
+
attributes.append((name, attr))
|
|
618
626
|
|
|
619
627
|
field_dict = {}
|
|
620
628
|
for name, attr in attributes:
|
sqladmin/models.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import time
|
|
4
5
|
import warnings
|
|
5
6
|
from enum import Enum
|
|
@@ -176,7 +177,7 @@ class BaseView(BaseModelView):
|
|
|
176
177
|
|
|
177
178
|
icon: ClassVar[str] = ""
|
|
178
179
|
"""Display icon for ModelAdmin in the sidebar.
|
|
179
|
-
Currently only supports FontAwesome icons.
|
|
180
|
+
Currently only supports FontAwesome and Tabler icons.
|
|
180
181
|
"""
|
|
181
182
|
|
|
182
183
|
category: ClassVar[str] = ""
|
|
@@ -454,7 +455,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
454
455
|
```
|
|
455
456
|
"""
|
|
456
457
|
|
|
457
|
-
export_types: ClassVar[List[str]] = ["csv"]
|
|
458
|
+
export_types: ClassVar[List[str]] = ["csv", "json"]
|
|
458
459
|
"""A list of available export filetypes.
|
|
459
460
|
Currently only `csv` is supported.
|
|
460
461
|
"""
|
|
@@ -1015,10 +1016,11 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1015
1016
|
By default do nothing.
|
|
1016
1017
|
"""
|
|
1017
1018
|
|
|
1018
|
-
async def scaffold_form(self) -> Type[Form]:
|
|
1019
|
+
async def scaffold_form(self, rules: List[str] | None = None) -> Type[Form]:
|
|
1019
1020
|
if self.form is not None:
|
|
1020
1021
|
return self.form
|
|
1021
|
-
|
|
1022
|
+
|
|
1023
|
+
form = await get_model_form(
|
|
1022
1024
|
model=self.model,
|
|
1023
1025
|
session_maker=self.session_maker,
|
|
1024
1026
|
only=self._form_prop_names,
|
|
@@ -1032,6 +1034,11 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1032
1034
|
form_converter=self.form_converter,
|
|
1033
1035
|
)
|
|
1034
1036
|
|
|
1037
|
+
if rules:
|
|
1038
|
+
self._validate_form_class(rules, form)
|
|
1039
|
+
|
|
1040
|
+
return form
|
|
1041
|
+
|
|
1035
1042
|
def search_placeholder(self) -> str:
|
|
1036
1043
|
"""Return search placeholder text.
|
|
1037
1044
|
|
|
@@ -1152,7 +1159,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1152
1159
|
) -> StreamingResponse:
|
|
1153
1160
|
if export_type == "csv":
|
|
1154
1161
|
return await self._export_csv(data)
|
|
1155
|
-
|
|
1162
|
+
elif export_type == "json":
|
|
1163
|
+
return await self._export_json(data)
|
|
1164
|
+
raise NotImplementedError("Only export_type='csv' or 'json' is implemented.")
|
|
1156
1165
|
|
|
1157
1166
|
async def _export_csv(
|
|
1158
1167
|
self,
|
|
@@ -1179,6 +1188,30 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1179
1188
|
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
1180
1189
|
)
|
|
1181
1190
|
|
|
1191
|
+
async def _export_json(
|
|
1192
|
+
self,
|
|
1193
|
+
data: List[Any],
|
|
1194
|
+
) -> StreamingResponse:
|
|
1195
|
+
async def generate() -> AsyncGenerator[str, None]:
|
|
1196
|
+
yield "["
|
|
1197
|
+
separator = "," if len(data) > 1 else ""
|
|
1198
|
+
|
|
1199
|
+
for row in data:
|
|
1200
|
+
row_dict = {
|
|
1201
|
+
name: await self.get_prop_value(row, name)
|
|
1202
|
+
for name in self._export_prop_names
|
|
1203
|
+
}
|
|
1204
|
+
yield json.dumps(row_dict) + separator
|
|
1205
|
+
|
|
1206
|
+
yield "]"
|
|
1207
|
+
|
|
1208
|
+
filename = secure_filename(self.get_export_name(export_type="json"))
|
|
1209
|
+
return StreamingResponse(
|
|
1210
|
+
content=generate(),
|
|
1211
|
+
media_type="application/json",
|
|
1212
|
+
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1182
1215
|
def _refresh_form_rules_cache(self) -> None:
|
|
1183
1216
|
if self.form_rules:
|
|
1184
1217
|
self._form_create_rules = self.form_rules
|
sqladmin/pagination.py
CHANGED
|
@@ -46,6 +46,15 @@ class Pagination:
|
|
|
46
46
|
|
|
47
47
|
raise RuntimeError("Next page not found.")
|
|
48
48
|
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
# Clamp page
|
|
51
|
+
self.page = min(self.page, max(1, self.count // self.page_size + 1))
|
|
52
|
+
|
|
53
|
+
def resize(self, page_size: int) -> Pagination:
|
|
54
|
+
self.page = (self.page - 1) * self.page_size // page_size + 1
|
|
55
|
+
self.page_size = page_size
|
|
56
|
+
return self
|
|
57
|
+
|
|
49
58
|
def add_pagination_urls(self, base_url: URL) -> None:
|
|
50
59
|
# Previous pages
|
|
51
60
|
for p in range(self.page - min(self.max_page_controls, 3), self.page):
|