sqladmin 0.20.1__tar.gz → 0.21.0__tar.gz
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-0.20.1 → sqladmin-0.21.0}/.gitignore +2 -1
- {sqladmin-0.20.1 → sqladmin-0.21.0}/PKG-INFO +4 -4
- {sqladmin-0.20.1 → sqladmin-0.21.0}/README.md +1 -1
- {sqladmin-0.20.1 → sqladmin-0.21.0}/pyproject.toml +4 -4
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/__init__.py +1 -1
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/_menu.py +1 -1
- sqladmin-0.21.0/sqladmin/_types.py +25 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/application.py +22 -8
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/authentication.py +6 -2
- sqladmin-0.21.0/sqladmin/filters.py +174 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/helpers.py +4 -2
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/models.py +65 -11
- sqladmin-0.21.0/sqladmin/statics/css/main.css +10 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/_macros.html +5 -2
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/details.html +5 -1
- sqladmin-0.21.0/sqladmin/templates/sqladmin/list.html +261 -0
- sqladmin-0.20.1/sqladmin/_types.py +0 -9
- sqladmin-0.20.1/sqladmin/statics/css/main.css +0 -3
- sqladmin-0.20.1/sqladmin/templates/sqladmin/list.html +0 -222
- {sqladmin-0.20.1 → sqladmin-0.21.0}/LICENSE.md +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/_queries.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/_validators.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/ajax.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/exceptions.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/fields.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/formatters.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/forms.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/pagination.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/py.typed +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/css/select2.min.css +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/css/tabler-icons.min.css +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/css/tabler-icons.min.css.map +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/css/tabler.min.css +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/jquery.min.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/main.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/popper.min.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/select2.full.min.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/js/tabler.min.js +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/base.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/create.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/edit.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/error.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/index.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/layout.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/login.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/modals/delete.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/modals/details_action_confirmation.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/modals/list_action_confirmation.html +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templating.py +0 -0
- {sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/widgets.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: sqladmin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.21.0
|
|
4
4
|
Summary: SQLAlchemy admin for FastAPI and Starlette
|
|
5
|
-
Project-URL: Documentation, https://aminalaee.
|
|
5
|
+
Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
|
|
6
6
|
Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
|
|
7
7
|
Project-URL: Source, https://github.com/aminalaee/sqladmin
|
|
8
8
|
Author-email: Amin Alaee <me@aminalaee.dev>
|
|
@@ -72,7 +72,7 @@ Main features include:
|
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
75
|
-
**Documentation**: [https://aminalaee.
|
|
75
|
+
**Documentation**: [https://aminalaee.github.io/sqladmin](https://aminalaee.github.io/sqladmin)
|
|
76
76
|
|
|
77
77
|
**Source Code**: [https://github.com/aminalaee/sqladmin](https://github.com/aminalaee/sqladmin)
|
|
78
78
|
|
|
@@ -39,7 +39,7 @@ Main features include:
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
**Documentation**: [https://aminalaee.
|
|
42
|
+
**Documentation**: [https://aminalaee.github.io/sqladmin](https://aminalaee.github.io/sqladmin)
|
|
43
43
|
|
|
44
44
|
**Source Code**: [https://github.com/aminalaee/sqladmin](https://github.com/aminalaee/sqladmin)
|
|
45
45
|
|
|
@@ -41,7 +41,7 @@ full = [
|
|
|
41
41
|
]
|
|
42
42
|
|
|
43
43
|
[project.urls]
|
|
44
|
-
Documentation = "https://aminalaee.
|
|
44
|
+
Documentation = "https://aminalaee.github.io/sqladmin/"
|
|
45
45
|
Issues = "https://github.com/aminalaee/sqladmin/issues"
|
|
46
46
|
Source = "https://github.com/aminalaee/sqladmin"
|
|
47
47
|
|
|
@@ -93,9 +93,9 @@ dependencies = [
|
|
|
93
93
|
|
|
94
94
|
[tool.hatch.envs.docs]
|
|
95
95
|
dependencies = [
|
|
96
|
-
"mkdocs-material==9.
|
|
97
|
-
"mkdocs==1.
|
|
98
|
-
"mkdocstrings[python]==0.
|
|
96
|
+
"mkdocs-material==9.6.14",
|
|
97
|
+
"mkdocs==1.6.1",
|
|
98
|
+
"mkdocstrings[python]==0.26.1",
|
|
99
99
|
]
|
|
100
100
|
|
|
101
101
|
[tool.hatch.envs.test.scripts]
|
|
@@ -44,7 +44,7 @@ class ItemMenu:
|
|
|
44
44
|
class CategoryMenu(ItemMenu):
|
|
45
45
|
def is_active(self, request: Request) -> bool:
|
|
46
46
|
return any(
|
|
47
|
-
c.
|
|
47
|
+
c.is_active(request) and c.is_accessible(request) for c in self.children
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
@property
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Any, Callable, List, Protocol, Tuple, Union, runtime_checkable
|
|
2
|
+
|
|
3
|
+
from sqlalchemy.engine import Engine
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
5
|
+
from sqlalchemy.orm import ColumnProperty, InstrumentedAttribute, RelationshipProperty
|
|
6
|
+
from sqlalchemy.sql.expression import Select
|
|
7
|
+
from starlette.requests import Request
|
|
8
|
+
|
|
9
|
+
MODEL_PROPERTY = Union[ColumnProperty, RelationshipProperty]
|
|
10
|
+
ENGINE_TYPE = Union[Engine, AsyncEngine]
|
|
11
|
+
MODEL_ATTR = Union[str, InstrumentedAttribute]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class ColumnFilter(Protocol):
|
|
16
|
+
title: str
|
|
17
|
+
parameter_name: str
|
|
18
|
+
|
|
19
|
+
async def lookups(
|
|
20
|
+
self, request: Request, model: Any, run_query: Callable[[Select], Any]
|
|
21
|
+
) -> List[Tuple[str, str]]:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
|
|
25
|
+
...
|
|
@@ -197,16 +197,22 @@ class BaseAdmin:
|
|
|
197
197
|
view_instance: BaseView | ModelView,
|
|
198
198
|
) -> None:
|
|
199
199
|
if hasattr(func, "_exposed"):
|
|
200
|
+
if view.is_model:
|
|
201
|
+
path = f"/{view_instance.identity}" + getattr(func, "_path")
|
|
202
|
+
name = f"view-{view_instance.identity}-{func.__name__}"
|
|
203
|
+
else:
|
|
204
|
+
view.identity = getattr(func, "_identity")
|
|
205
|
+
path = getattr(func, "_path")
|
|
206
|
+
name = getattr(func, "_identity")
|
|
207
|
+
|
|
200
208
|
self.admin.add_route(
|
|
201
209
|
route=func,
|
|
202
|
-
path=
|
|
210
|
+
path=path,
|
|
203
211
|
methods=getattr(func, "_methods"),
|
|
204
|
-
name=
|
|
212
|
+
name=name,
|
|
205
213
|
include_in_schema=getattr(func, "_include_in_schema"),
|
|
206
214
|
)
|
|
207
215
|
|
|
208
|
-
view.identity = getattr(func, "_identity")
|
|
209
|
-
|
|
210
216
|
def add_model_view(self, view: type[ModelView]) -> None:
|
|
211
217
|
"""Add ModelView to the Admin.
|
|
212
218
|
|
|
@@ -233,6 +239,11 @@ class BaseAdmin:
|
|
|
233
239
|
self._find_decorated_funcs(
|
|
234
240
|
view, view_instance, self._handle_action_decorated_func
|
|
235
241
|
)
|
|
242
|
+
|
|
243
|
+
self._find_decorated_funcs(
|
|
244
|
+
view, view_instance, self._handle_expose_decorated_func
|
|
245
|
+
)
|
|
246
|
+
|
|
236
247
|
self._views.append(view_instance)
|
|
237
248
|
self._build_menu(view_instance)
|
|
238
249
|
|
|
@@ -266,7 +277,7 @@ class BaseAdmin:
|
|
|
266
277
|
|
|
267
278
|
def _build_menu(self, view: ModelView | BaseView) -> None:
|
|
268
279
|
if view.category:
|
|
269
|
-
menu = CategoryMenu(name=view.category)
|
|
280
|
+
menu = CategoryMenu(name=view.category, icon=view.category_icon)
|
|
270
281
|
menu.add_child(ViewMenu(view=view, name=view.name, icon=view.icon))
|
|
271
282
|
self._menu.add(menu)
|
|
272
283
|
else:
|
|
@@ -461,10 +472,9 @@ class Admin(BaseAdminView):
|
|
|
461
472
|
"""Details route."""
|
|
462
473
|
|
|
463
474
|
await self._details(request)
|
|
464
|
-
|
|
465
475
|
model_view = self._find_model_view(request.path_params["identity"])
|
|
476
|
+
model = await model_view.get_object_for_details(request)
|
|
466
477
|
|
|
467
|
-
model = await model_view.get_object_for_details(request.path_params["pk"])
|
|
468
478
|
if not model:
|
|
469
479
|
raise HTTPException(status_code=404)
|
|
470
480
|
|
|
@@ -638,7 +648,11 @@ class Admin(BaseAdminView):
|
|
|
638
648
|
async def logout(self, request: Request) -> Response:
|
|
639
649
|
assert self.authentication_backend is not None
|
|
640
650
|
|
|
641
|
-
await self.authentication_backend.logout(request)
|
|
651
|
+
response = await self.authentication_backend.logout(request)
|
|
652
|
+
|
|
653
|
+
if isinstance(response, Response):
|
|
654
|
+
return response
|
|
655
|
+
|
|
642
656
|
return RedirectResponse(request.url_for("admin:index"), status_code=302)
|
|
643
657
|
|
|
644
658
|
async def ajax_lookup(self, request: Request) -> Response:
|
|
@@ -29,9 +29,13 @@ class AuthenticationBackend:
|
|
|
29
29
|
"""
|
|
30
30
|
raise NotImplementedError()
|
|
31
31
|
|
|
32
|
-
async def logout(self, request: Request) -> bool:
|
|
32
|
+
async def logout(self, request: Request) -> Response | bool:
|
|
33
33
|
"""Implement logout logic here.
|
|
34
34
|
This will usually clear the session with `request.session.clear()`.
|
|
35
|
+
|
|
36
|
+
If a `Response` or `RedirectResponse` is returned,
|
|
37
|
+
that response is returned to the user,
|
|
38
|
+
otherwise the user will be redirected to the index page.
|
|
35
39
|
"""
|
|
36
40
|
raise NotImplementedError()
|
|
37
41
|
|
|
@@ -40,7 +44,7 @@ class AuthenticationBackend:
|
|
|
40
44
|
This method will be called for each incoming request
|
|
41
45
|
to validate the authentication.
|
|
42
46
|
|
|
43
|
-
If a
|
|
47
|
+
If a `Response` or `RedirectResponse` is returned,
|
|
44
48
|
that response is returned to the user,
|
|
45
49
|
otherwise a True/False is expected.
|
|
46
50
|
"""
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Callable, List, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import Integer
|
|
5
|
+
from sqlalchemy.sql.expression import Select, select
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
|
|
8
|
+
from sqladmin._types import MODEL_ATTR
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_parameter_name(column: MODEL_ATTR) -> str:
|
|
12
|
+
if isinstance(column, str):
|
|
13
|
+
return column
|
|
14
|
+
else:
|
|
15
|
+
return column.key
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def prettify_attribute_name(name: str) -> str:
|
|
19
|
+
return re.sub(r"_([A-Za-z])", r" \1", name).title()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_title(column: MODEL_ATTR) -> str:
|
|
23
|
+
name = get_parameter_name(column)
|
|
24
|
+
return prettify_attribute_name(name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_column_obj(column: MODEL_ATTR, model: Any = None) -> Any:
|
|
28
|
+
if isinstance(column, str):
|
|
29
|
+
if model is None:
|
|
30
|
+
raise ValueError("model is required for string column filters")
|
|
31
|
+
return getattr(model, column)
|
|
32
|
+
return column
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_foreign_column_name(column_obj: Any) -> str:
|
|
36
|
+
fk = next(iter(column_obj.foreign_keys))
|
|
37
|
+
return fk.column.name
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_model_from_column(column: Any) -> Any:
|
|
41
|
+
return column.parent.class_
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BooleanFilter:
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
column: MODEL_ATTR,
|
|
48
|
+
title: Optional[str] = None,
|
|
49
|
+
parameter_name: Optional[str] = None,
|
|
50
|
+
):
|
|
51
|
+
self.column = column
|
|
52
|
+
self.title = title or get_title(column)
|
|
53
|
+
self.parameter_name = parameter_name or get_parameter_name(column)
|
|
54
|
+
|
|
55
|
+
async def lookups(
|
|
56
|
+
self, request: Request, model: Any, run_query: Callable[[Select], Any]
|
|
57
|
+
) -> List[Tuple[str, str]]:
|
|
58
|
+
return [
|
|
59
|
+
("all", "All"),
|
|
60
|
+
("true", "Yes"),
|
|
61
|
+
("false", "No"),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
|
|
65
|
+
column_obj = get_column_obj(self.column, model)
|
|
66
|
+
if value == "true":
|
|
67
|
+
return query.filter(column_obj.is_(True))
|
|
68
|
+
elif value == "false":
|
|
69
|
+
return query.filter(column_obj.is_(False))
|
|
70
|
+
else:
|
|
71
|
+
return query
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class AllUniqueStringValuesFilter:
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
column: MODEL_ATTR,
|
|
78
|
+
title: Optional[str] = None,
|
|
79
|
+
parameter_name: Optional[str] = None,
|
|
80
|
+
):
|
|
81
|
+
self.column = column
|
|
82
|
+
self.title = title or get_title(column)
|
|
83
|
+
self.parameter_name = parameter_name or get_parameter_name(column)
|
|
84
|
+
|
|
85
|
+
async def lookups(
|
|
86
|
+
self, request: Request, model: Any, run_query: Callable[[Select], Any]
|
|
87
|
+
) -> List[Tuple[str, str]]:
|
|
88
|
+
column_obj = get_column_obj(self.column, model)
|
|
89
|
+
|
|
90
|
+
return [("", "All")] + [
|
|
91
|
+
(value[0], value[0])
|
|
92
|
+
for value in await run_query(select(column_obj).distinct())
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
|
|
96
|
+
if value == "":
|
|
97
|
+
return query
|
|
98
|
+
|
|
99
|
+
column_obj = get_column_obj(self.column, model)
|
|
100
|
+
return query.filter(column_obj == value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class StaticValuesFilter:
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
column: MODEL_ATTR,
|
|
107
|
+
values: List[Tuple[str, str]],
|
|
108
|
+
title: Optional[str] = None,
|
|
109
|
+
parameter_name: Optional[str] = None,
|
|
110
|
+
):
|
|
111
|
+
self.column = column
|
|
112
|
+
self.title = title or get_title(column)
|
|
113
|
+
self.parameter_name = parameter_name or get_parameter_name(column)
|
|
114
|
+
self.values = values
|
|
115
|
+
|
|
116
|
+
async def lookups(
|
|
117
|
+
self, request: Request, model: Any, run_query: Callable[[Select], Any]
|
|
118
|
+
) -> List[Tuple[str, str]]:
|
|
119
|
+
return [("", "All")] + self.values
|
|
120
|
+
|
|
121
|
+
async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
|
|
122
|
+
column_obj = get_column_obj(self.column, model)
|
|
123
|
+
if value == "":
|
|
124
|
+
return query
|
|
125
|
+
return query.filter(column_obj == value)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class ForeignKeyFilter:
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
foreign_key: MODEL_ATTR,
|
|
132
|
+
foreign_display_field: MODEL_ATTR,
|
|
133
|
+
foreign_model: Any = None,
|
|
134
|
+
title: Optional[str] = None,
|
|
135
|
+
parameter_name: Optional[str] = None,
|
|
136
|
+
):
|
|
137
|
+
self.foreign_key = foreign_key
|
|
138
|
+
self.foreign_display_field = foreign_display_field
|
|
139
|
+
self.foreign_model = foreign_model
|
|
140
|
+
self.title = title or get_title(foreign_key)
|
|
141
|
+
self.parameter_name = parameter_name or get_parameter_name(foreign_key)
|
|
142
|
+
|
|
143
|
+
async def lookups(
|
|
144
|
+
self, request: Request, model: Any, run_query: Callable[[Select], Any]
|
|
145
|
+
) -> List[Tuple[str, str]]:
|
|
146
|
+
foreign_key_obj = get_column_obj(self.foreign_key, model)
|
|
147
|
+
if self.foreign_model is None and isinstance(self.foreign_display_field, str):
|
|
148
|
+
raise ValueError("foreign_model is required for string foreign key filters")
|
|
149
|
+
if self.foreign_model is None:
|
|
150
|
+
assert not isinstance(self.foreign_display_field, str)
|
|
151
|
+
foreign_display_field_obj = self.foreign_display_field
|
|
152
|
+
else:
|
|
153
|
+
foreign_display_field_obj = get_column_obj(
|
|
154
|
+
self.foreign_display_field, self.foreign_model
|
|
155
|
+
)
|
|
156
|
+
if not self.foreign_model:
|
|
157
|
+
self.foreign_model = get_model_from_column(foreign_display_field_obj)
|
|
158
|
+
foreign_model_key_name = get_foreign_column_name(foreign_key_obj)
|
|
159
|
+
foreign_model_key_obj = getattr(self.foreign_model, foreign_model_key_name)
|
|
160
|
+
|
|
161
|
+
return [("", "All")] + [
|
|
162
|
+
(str(key), str(value))
|
|
163
|
+
for key, value in await run_query(
|
|
164
|
+
select(foreign_model_key_obj, foreign_display_field_obj).distinct()
|
|
165
|
+
)
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
|
|
169
|
+
foreign_key_obj = get_column_obj(self.foreign_key, model)
|
|
170
|
+
column_type = foreign_key_obj.type
|
|
171
|
+
if isinstance(column_type, Integer):
|
|
172
|
+
value = int(value)
|
|
173
|
+
|
|
174
|
+
return query.filter(foreign_key_obj == value)
|
|
@@ -36,7 +36,7 @@ from wtforms import Field, Form
|
|
|
36
36
|
from wtforms.fields.core import UnboundField
|
|
37
37
|
|
|
38
38
|
from sqladmin._queries import Query
|
|
39
|
-
from sqladmin._types import MODEL_ATTR
|
|
39
|
+
from sqladmin._types import MODEL_ATTR, ColumnFilter
|
|
40
40
|
from sqladmin.ajax import create_ajax_loader
|
|
41
41
|
from sqladmin.exceptions import InvalidModelError
|
|
42
42
|
from sqladmin.formatters import BASE_FORMATTERS
|
|
@@ -183,6 +183,9 @@ class BaseView(BaseModelView):
|
|
|
183
183
|
category: ClassVar[str] = ""
|
|
184
184
|
"""Category name to group views together."""
|
|
185
185
|
|
|
186
|
+
category_icon: ClassVar[str] = ""
|
|
187
|
+
"""Display icon for category in the sidebar."""
|
|
188
|
+
|
|
186
189
|
|
|
187
190
|
class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
188
191
|
"""Base class for defining admnistrative behaviour for the model.
|
|
@@ -312,6 +315,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
312
315
|
```
|
|
313
316
|
"""
|
|
314
317
|
|
|
318
|
+
column_filters: ClassVar[Sequence[ColumnFilter]] = []
|
|
319
|
+
"""Collection of the filterable columns for the list view.
|
|
320
|
+
Columns can either be string names or SQLAlchemy columns.
|
|
321
|
+
|
|
322
|
+
???+ example
|
|
323
|
+
```python
|
|
324
|
+
class UserAdmin(ModelView, model=User):
|
|
325
|
+
column_filters = [User.is_admin]
|
|
326
|
+
```
|
|
327
|
+
"""
|
|
328
|
+
|
|
315
329
|
column_sortable_list: ClassVar[Sequence[MODEL_ATTR]] = []
|
|
316
330
|
"""Collection of the sortable columns for the list view.
|
|
317
331
|
|
|
@@ -398,7 +412,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
398
412
|
"""
|
|
399
413
|
|
|
400
414
|
save_as: ClassVar[bool] = False
|
|
401
|
-
"""Set `save_as` to enable a
|
|
415
|
+
"""Set `save_as` to enable a "save as new" feature on admin change forms.
|
|
402
416
|
|
|
403
417
|
Normally, objects have three save options:
|
|
404
418
|
``Save`, `Save and continue editing` and `Save and add another`.
|
|
@@ -432,6 +446,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
432
446
|
edit_template: ClassVar[str] = "sqladmin/edit.html"
|
|
433
447
|
"""Edit view template. Default is `sqladmin/edit.html`."""
|
|
434
448
|
|
|
449
|
+
# Template configuration
|
|
450
|
+
show_compact_lists: ClassVar[bool] = True
|
|
451
|
+
"""Show compact lists. Default is `True`.
|
|
452
|
+
If False, when showing lists of objects, each object will be \
|
|
453
|
+
displayed in a separate line."""
|
|
454
|
+
|
|
435
455
|
# Export
|
|
436
456
|
column_export_list: ClassVar[List[MODEL_ATTR]] = []
|
|
437
457
|
"""List of columns to include when exporting.
|
|
@@ -645,7 +665,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
645
665
|
- None will be displayed as an empty string
|
|
646
666
|
- bool will be displayed as a checkmark if it is True otherwise as an X.
|
|
647
667
|
|
|
648
|
-
If you don
|
|
668
|
+
If you don't like the default behavior and don't want any type formatters applied,
|
|
649
669
|
just override this property with an empty dictionary:
|
|
650
670
|
|
|
651
671
|
???+ example
|
|
@@ -718,6 +738,19 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
718
738
|
self._custom_actions_in_detail: Dict[str, str] = {}
|
|
719
739
|
self._custom_actions_confirmation: Dict[str, str] = {}
|
|
720
740
|
|
|
741
|
+
def _run_arbitrary_query_sync(self, stmt: ClauseElement) -> Any:
|
|
742
|
+
with self.session_maker(expire_on_commit=False) as session:
|
|
743
|
+
result = session.execute(stmt)
|
|
744
|
+
return result.all()
|
|
745
|
+
|
|
746
|
+
async def _run_arbitrary_query(self, stmt: ClauseElement) -> Any:
|
|
747
|
+
if self.is_async:
|
|
748
|
+
async with self.session_maker(expire_on_commit=False) as session:
|
|
749
|
+
result = await session.execute(stmt)
|
|
750
|
+
return result.all()
|
|
751
|
+
else:
|
|
752
|
+
return self._run_arbitrary_query_sync(stmt)
|
|
753
|
+
|
|
721
754
|
def _run_query_sync(self, stmt: ClauseElement) -> Any:
|
|
722
755
|
with self.session_maker(expire_on_commit=False) as session:
|
|
723
756
|
result = session.execute(stmt)
|
|
@@ -803,6 +836,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
803
836
|
for relation in self._list_relations:
|
|
804
837
|
stmt = stmt.options(selectinload(relation))
|
|
805
838
|
|
|
839
|
+
for filter in self.get_filters():
|
|
840
|
+
if request.query_params.get(filter.parameter_name):
|
|
841
|
+
stmt = await filter.get_filtered_query(
|
|
842
|
+
stmt, request.query_params.get(filter.parameter_name), self.model
|
|
843
|
+
)
|
|
844
|
+
|
|
806
845
|
stmt = self.sort_query(stmt, request)
|
|
807
846
|
|
|
808
847
|
if search:
|
|
@@ -840,12 +879,8 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
840
879
|
rows = await self._run_query(stmt)
|
|
841
880
|
return rows[0] if rows else None
|
|
842
881
|
|
|
843
|
-
async def get_object_for_details(self,
|
|
844
|
-
stmt = self.
|
|
845
|
-
|
|
846
|
-
for relation in self._details_relations:
|
|
847
|
-
stmt = stmt.options(selectinload(relation))
|
|
848
|
-
|
|
882
|
+
async def get_object_for_details(self, request: Request) -> Any:
|
|
883
|
+
stmt = self.details_query(request)
|
|
849
884
|
return await self._get_object_by_pk(stmt)
|
|
850
885
|
|
|
851
886
|
async def get_object_for_edit(self, request: Request) -> Any:
|
|
@@ -973,6 +1008,15 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
973
1008
|
defaults=self._list_prop_names,
|
|
974
1009
|
)
|
|
975
1010
|
|
|
1011
|
+
def get_filters(self) -> List[ColumnFilter]:
|
|
1012
|
+
"""Get list of filters."""
|
|
1013
|
+
|
|
1014
|
+
filters = getattr(self, "column_filters", None)
|
|
1015
|
+
if not filters:
|
|
1016
|
+
return []
|
|
1017
|
+
|
|
1018
|
+
return filters
|
|
1019
|
+
|
|
976
1020
|
async def on_model_change(
|
|
977
1021
|
self, data: dict, model: Any, is_created: bool, request: Request
|
|
978
1022
|
) -> None:
|
|
@@ -1088,6 +1132,14 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1088
1132
|
|
|
1089
1133
|
return select(self.model)
|
|
1090
1134
|
|
|
1135
|
+
def details_query(self, request: Request) -> Select:
|
|
1136
|
+
"""
|
|
1137
|
+
The SQLAlchemy select expression used for the details page which can be
|
|
1138
|
+
customized. By default it will select all objects without any filters.
|
|
1139
|
+
"""
|
|
1140
|
+
|
|
1141
|
+
return self.form_edit_query(request)
|
|
1142
|
+
|
|
1091
1143
|
def edit_form_query(self, request: Request) -> Select:
|
|
1092
1144
|
msg = (
|
|
1093
1145
|
"Overriding 'edit_form_query' is deprecated. Use 'form_edit_query' instead."
|
|
@@ -1184,7 +1236,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1184
1236
|
|
|
1185
1237
|
return StreamingResponse(
|
|
1186
1238
|
content=stream_to_csv(generate),
|
|
1187
|
-
media_type="text/csv",
|
|
1239
|
+
media_type="text/csv; charset=utf-8",
|
|
1188
1240
|
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
1189
1241
|
)
|
|
1190
1242
|
|
|
@@ -1203,7 +1255,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1203
1255
|
name: str(await self.get_prop_value(row, name))
|
|
1204
1256
|
for name in self._export_prop_names
|
|
1205
1257
|
}
|
|
1206
|
-
yield json.dumps(row_dict) + (
|
|
1258
|
+
yield json.dumps(row_dict, ensure_ascii=False) + (
|
|
1259
|
+
separator if idx < last_idx else ""
|
|
1260
|
+
)
|
|
1207
1261
|
|
|
1208
1262
|
yield "]"
|
|
1209
1263
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{% macro menu_category(menu, request) %}
|
|
2
|
-
{% if menu.
|
|
2
|
+
{% if menu.is_visible(request) and menu.is_accessible(request) %}
|
|
3
3
|
<li class="nav-item dropdown">
|
|
4
4
|
<a class="nav-link dropdown-toggle {% if menu.is_active(request) %}active{% endif %}" data-bs-toggle="dropdown"
|
|
5
5
|
href="#">
|
|
@@ -56,7 +56,10 @@
|
|
|
56
56
|
|
|
57
57
|
{% macro render_field(field, kwargs={}) %}
|
|
58
58
|
<div class="mb-3 form-group row">
|
|
59
|
-
{{ field.label(
|
|
59
|
+
{{ field.label(
|
|
60
|
+
class_="form-label col-sm-2 col-form-label" + (' required-label' if field.flags.required else ''),
|
|
61
|
+
**({'title': "This is a required field"} if field.flags.required else {})
|
|
62
|
+
) }}
|
|
60
63
|
<div class="col-sm-10">
|
|
61
64
|
{% if field.errors %}
|
|
62
65
|
{{ field(class_="form-control is-invalid") }}
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
{% if is_list( value ) %}
|
|
29
29
|
<td>
|
|
30
30
|
{% for elem, formatted_elem in zip(value, formatted_value) %}
|
|
31
|
-
|
|
31
|
+
{% if model_view.show_compact_lists %}
|
|
32
|
+
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
|
|
33
|
+
{% else %}
|
|
34
|
+
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">{{ formatted_elem }}</a><br/>
|
|
35
|
+
{% endif %}
|
|
32
36
|
{% endfor %}
|
|
33
37
|
</td>
|
|
34
38
|
{% else %}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
{% extends "sqladmin/layout.html" %}
|
|
2
|
+
{% block content %}
|
|
3
|
+
<div class="container-fluid">
|
|
4
|
+
<div class="row">
|
|
5
|
+
<div class="col-12">
|
|
6
|
+
<div class="d-flex">
|
|
7
|
+
<div class="flex-grow-1 me-2">
|
|
8
|
+
<div class="card">
|
|
9
|
+
<div class="card-header">
|
|
10
|
+
<h3 class="card-title">{{ model_view.name_plural }}</h3>
|
|
11
|
+
<div class="ms-auto">
|
|
12
|
+
{% if model_view.can_export %}
|
|
13
|
+
{% if model_view.export_types | length > 1 %}
|
|
14
|
+
<div class="ms-3 d-inline-block dropdown">
|
|
15
|
+
<a href="#" class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown"
|
|
16
|
+
aria-expanded="false">
|
|
17
|
+
Export
|
|
18
|
+
</a>
|
|
19
|
+
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
|
20
|
+
{% for export_type in model_view.export_types %}
|
|
21
|
+
<li><a class="dropdown-item"
|
|
22
|
+
href="{{ url_for('admin:export', identity=model_view.identity, export_type=export_type) }}">{{
|
|
23
|
+
export_type | upper }}</a></li>
|
|
24
|
+
{% endfor %}
|
|
25
|
+
</ul>
|
|
26
|
+
</div>
|
|
27
|
+
{% elif model_view.export_types | length == 1 %}
|
|
28
|
+
<div class="ms-3 d-inline-block">
|
|
29
|
+
<a href="{{ url_for('admin:export', identity=model_view.identity, export_type=model_view.export_types[0]) }}"
|
|
30
|
+
class="btn btn-secondary">
|
|
31
|
+
Export
|
|
32
|
+
</a>
|
|
33
|
+
</div>
|
|
34
|
+
{% endif %}
|
|
35
|
+
{% endif %}
|
|
36
|
+
{% if model_view.can_create %}
|
|
37
|
+
<div class="ms-3 d-inline-block">
|
|
38
|
+
<a href="{{ url_for('admin:create', identity=model_view.identity) }}" class="btn btn-primary">
|
|
39
|
+
+ New {{ model_view.name }}
|
|
40
|
+
</a>
|
|
41
|
+
</div>
|
|
42
|
+
{% endif %}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="card-body border-bottom py-3">
|
|
46
|
+
<div class="d-flex justify-content-between">
|
|
47
|
+
<div class="dropdown col-4">
|
|
48
|
+
<button {% if not model_view.can_delete and not model_view._custom_actions_in_list %} disabled {% endif %}
|
|
49
|
+
class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
|
|
50
|
+
aria-haspopup="true" aria-expanded="false">
|
|
51
|
+
Actions
|
|
52
|
+
</button>
|
|
53
|
+
{% if model_view.can_delete or model_view._custom_actions_in_list %}
|
|
54
|
+
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
|
55
|
+
{% if model_view.can_delete %}
|
|
56
|
+
<a class="dropdown-item" id="action-delete" href="#" data-name="{{ model_view.name }}"
|
|
57
|
+
data-url="{{ url_for('admin:delete', identity=model_view.identity) }}" data-bs-toggle="modal"
|
|
58
|
+
data-bs-target="#modal-delete">Delete selected items</a>
|
|
59
|
+
{% endif %}
|
|
60
|
+
{% for custom_action, label in model_view._custom_actions_in_list.items() %}
|
|
61
|
+
{% if custom_action in model_view._custom_actions_confirmation %}
|
|
62
|
+
<a class="dropdown-item" id="action-customconfirm-{{ custom_action }}" href="#" data-bs-toggle="modal"
|
|
63
|
+
data-bs-target="#modal-confirmation-{{ custom_action }}">
|
|
64
|
+
{{ label }}
|
|
65
|
+
</a>
|
|
66
|
+
{% else %}
|
|
67
|
+
<a class="dropdown-item" id="action-custom-{{ custom_action }}" href="#"
|
|
68
|
+
data-url="{{ model_view._url_for_action(request, custom_action) }}">
|
|
69
|
+
{{ label }}
|
|
70
|
+
</a>
|
|
71
|
+
{% endif %}
|
|
72
|
+
{% endfor %}
|
|
73
|
+
</div>
|
|
74
|
+
{% endif %}
|
|
75
|
+
</div>
|
|
76
|
+
{% if model_view.column_searchable_list %}
|
|
77
|
+
<div class="col-md-4 text-muted">
|
|
78
|
+
<div class="input-group">
|
|
79
|
+
<input id="search-input" type="text" class="form-control"
|
|
80
|
+
placeholder="Search: {{ model_view.search_placeholder() }}"
|
|
81
|
+
value="{{ request.query_params.get('search', '') }}">
|
|
82
|
+
<button id="search-button" class="btn" type="button">Search</button>
|
|
83
|
+
<button id="search-reset" class="btn" type="button" {% if not request.query_params.get('search')
|
|
84
|
+
%}disabled{% endif %}><i class="fa-solid fa-times"></i></button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
{% endif %}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="table-responsive">
|
|
91
|
+
<table class="table card-table table-vcenter text-nowrap">
|
|
92
|
+
<thead>
|
|
93
|
+
<tr>
|
|
94
|
+
<th class="w-1"><input class="form-check-input m-0 align-middle" type="checkbox" aria-label="Select all"
|
|
95
|
+
id="select-all"></th>
|
|
96
|
+
<th class="w-1"></th>
|
|
97
|
+
{% for name in model_view._list_prop_names %}
|
|
98
|
+
{% set label = model_view._column_labels.get(name, name) %}
|
|
99
|
+
<th>
|
|
100
|
+
{% if name in model_view._sort_fields %}
|
|
101
|
+
{% if request.query_params.get("sortBy") == name and request.query_params.get("sort") == "asc" %}
|
|
102
|
+
<a href="{{ request.url.include_query_params(sort='desc') }}"><i class="fa-solid fa-arrow-up"></i> {{
|
|
103
|
+
label }}</a>
|
|
104
|
+
{% elif request.query_params.get("sortBy") == name and request.query_params.get("sort") == "desc" %}
|
|
105
|
+
<a href="{{ request.url.include_query_params(sort='asc') }}"><i class="fa-solid fa-arrow-down"></i> {{ label
|
|
106
|
+
}}</a>
|
|
107
|
+
{% else %}
|
|
108
|
+
<a href="{{ request.url.include_query_params(sortBy=name, sort='asc') }}">{{ label }}</a>
|
|
109
|
+
{% endif %}
|
|
110
|
+
{% else %}
|
|
111
|
+
{{ label }}
|
|
112
|
+
{% endif %}
|
|
113
|
+
</th>
|
|
114
|
+
{% endfor %}
|
|
115
|
+
</tr>
|
|
116
|
+
</thead>
|
|
117
|
+
<tbody>
|
|
118
|
+
{% for row in pagination.rows %}
|
|
119
|
+
<tr>
|
|
120
|
+
<td>
|
|
121
|
+
<input type="hidden" value="{{ get_object_identifier(row) }}">
|
|
122
|
+
<input class="form-check-input m-0 align-middle select-box" type="checkbox" aria-label="Select item">
|
|
123
|
+
</td>
|
|
124
|
+
<td class="text-end">
|
|
125
|
+
{% if model_view.can_view_details %}
|
|
126
|
+
<a href="{{ model_view._build_url_for('admin:details', request, row) }}" data-bs-toggle="tooltip"
|
|
127
|
+
data-bs-placement="top" title="View">
|
|
128
|
+
<span class="me-1"><i class="fa-solid fa-eye"></i></span>
|
|
129
|
+
</a>
|
|
130
|
+
{% endif %}
|
|
131
|
+
{% if model_view.can_edit %}
|
|
132
|
+
<a href="{{ model_view._build_url_for('admin:edit', request, row) }}" data-bs-toggle="tooltip"
|
|
133
|
+
data-bs-placement="top" title="Edit">
|
|
134
|
+
<span class="me-1"><i class="fa-solid fa-pen-to-square"></i></span>
|
|
135
|
+
</a>
|
|
136
|
+
{% endif %}
|
|
137
|
+
{% if model_view.can_delete %}
|
|
138
|
+
<a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(row) }}"
|
|
139
|
+
data-url="{{ model_view._url_for_delete(request, row) }}" data-bs-toggle="modal"
|
|
140
|
+
data-bs-target="#modal-delete" title="Delete">
|
|
141
|
+
<span class="me-1"><i class="fa-solid fa-trash"></i></span>
|
|
142
|
+
</a>
|
|
143
|
+
{% endif %}
|
|
144
|
+
</td>
|
|
145
|
+
{% for name in model_view._list_prop_names %}
|
|
146
|
+
{% set value, formatted_value = model_view.get_list_value(row, name) %}
|
|
147
|
+
{% if name in model_view._relation_names %}
|
|
148
|
+
{% if is_list( value ) %}
|
|
149
|
+
<td>
|
|
150
|
+
{% for elem, formatted_elem in zip(value, formatted_value) %}
|
|
151
|
+
{% if model_view.show_compact_lists %}
|
|
152
|
+
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
|
|
153
|
+
{% else %}
|
|
154
|
+
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">{{ formatted_elem }}</a><br/>
|
|
155
|
+
{% endif %}
|
|
156
|
+
{% endfor %}
|
|
157
|
+
</td>
|
|
158
|
+
{% else %}
|
|
159
|
+
<td><a href="{{ model_view._url_for_details_with_prop(request, row, name) }}">{{ formatted_value }}</a></td>
|
|
160
|
+
{% endif %}
|
|
161
|
+
{% else %}
|
|
162
|
+
<td>{{ formatted_value }}</td>
|
|
163
|
+
{% endif %}
|
|
164
|
+
{% endfor %}
|
|
165
|
+
</tr>
|
|
166
|
+
{% endfor %}
|
|
167
|
+
</tbody>
|
|
168
|
+
</table>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="card-footer d-flex justify-content-between align-items-center gap-2">
|
|
171
|
+
<p class="m-0 text-muted">Showing <span>{{ ((pagination.page - 1) * pagination.page_size) + 1 }}</span> to
|
|
172
|
+
<span>{{ min(pagination.page * pagination.page_size, pagination.count) }}</span> of <span>{{ pagination.count
|
|
173
|
+
}}</span> items
|
|
174
|
+
</p>
|
|
175
|
+
<ul class="pagination m-0 ms-auto">
|
|
176
|
+
<li class="page-item {% if not pagination.has_previous %}disabled{% endif %}">
|
|
177
|
+
{% if pagination.has_previous %}
|
|
178
|
+
<a class="page-link" href="{{ pagination.previous_page.url }}">
|
|
179
|
+
{% else %}
|
|
180
|
+
<a class="page-link" href="#">
|
|
181
|
+
{% endif %}
|
|
182
|
+
<i class="fa-solid fa-chevron-left"></i>
|
|
183
|
+
prev
|
|
184
|
+
</a>
|
|
185
|
+
</li>
|
|
186
|
+
{% for page_control in pagination.page_controls %}
|
|
187
|
+
<li class="page-item {% if page_control.number == pagination.page %}active{% endif %}"><a class="page-link"
|
|
188
|
+
href="{{ page_control.url }}">{{ page_control.number }}</a></li>
|
|
189
|
+
{% endfor %}
|
|
190
|
+
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
|
|
191
|
+
{% if pagination.has_next %}
|
|
192
|
+
<a class="page-link" href="{{ pagination.next_page.url }}">
|
|
193
|
+
{% else %}
|
|
194
|
+
<a class="page-link" href="#">
|
|
195
|
+
{% endif %}
|
|
196
|
+
next
|
|
197
|
+
<i class="fa-solid fa-chevron-right"></i>
|
|
198
|
+
</a>
|
|
199
|
+
</li>
|
|
200
|
+
</ul>
|
|
201
|
+
<div class="dropdown text-muted">
|
|
202
|
+
Show
|
|
203
|
+
<a href="#" class="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
|
|
204
|
+
aria-expanded="false">
|
|
205
|
+
{{ request.query_params.get("pageSize") or model_view.page_size }} / Page
|
|
206
|
+
</a>
|
|
207
|
+
<div class="dropdown-menu">
|
|
208
|
+
{% for page_size_option in model_view.page_size_options %}
|
|
209
|
+
<a class="dropdown-item" href="{{ request.url.include_query_params(pageSize=page_size_option, page=pagination.resize(page_size_option).page) }}">
|
|
210
|
+
{{ page_size_option }} / Page
|
|
211
|
+
</a>
|
|
212
|
+
{% endfor %}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
{% if model_view.get_filters() %}
|
|
219
|
+
<div class="col-md-3" style="width: 300px; flex-shrink: 0;">
|
|
220
|
+
<div id="filter-sidebar" class="card">
|
|
221
|
+
<div class="card-header">
|
|
222
|
+
<h3 class="card-title">Filters</h3>
|
|
223
|
+
</div>
|
|
224
|
+
<div class="card-body p-0">
|
|
225
|
+
<div class="list-group list-group-flush">
|
|
226
|
+
{% for filter in model_view.get_filters() %}
|
|
227
|
+
<div class="list-group-item">
|
|
228
|
+
<div class="p-2">
|
|
229
|
+
<div class="fw-bold text-truncate">{{ filter.title }}</div>
|
|
230
|
+
<div>
|
|
231
|
+
{% for lookup in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %}
|
|
232
|
+
<a href="{{ request.url.include_query_params(**{filter.parameter_name: lookup[0]}) }}" class="d-block text-decoration-none text-truncate">
|
|
233
|
+
{{ lookup[1] }}
|
|
234
|
+
</a>
|
|
235
|
+
{% endfor %}
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
{% endfor %}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
{% endif %}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
{% if model_view.can_delete %}
|
|
249
|
+
{% include 'sqladmin/modals/delete.html' %}
|
|
250
|
+
{% endif %}
|
|
251
|
+
|
|
252
|
+
{% for custom_action in model_view._custom_actions_in_list %}
|
|
253
|
+
{% if custom_action in model_view._custom_actions_confirmation %}
|
|
254
|
+
{% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action,
|
|
255
|
+
url=model_view._url_for_action(request, custom_action) %}
|
|
256
|
+
{% include 'sqladmin/modals/list_action_confirmation.html' %}
|
|
257
|
+
{% endwith %}
|
|
258
|
+
{% endif %}
|
|
259
|
+
{% endfor %}
|
|
260
|
+
</div>
|
|
261
|
+
{% endblock %}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
|
|
3
|
-
from sqlalchemy.engine import Engine
|
|
4
|
-
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
5
|
-
from sqlalchemy.orm import ColumnProperty, InstrumentedAttribute, RelationshipProperty
|
|
6
|
-
|
|
7
|
-
MODEL_PROPERTY = Union[ColumnProperty, RelationshipProperty]
|
|
8
|
-
ENGINE_TYPE = Union[Engine, AsyncEngine]
|
|
9
|
-
MODEL_ATTR = Union[str, InstrumentedAttribute]
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
{% extends "sqladmin/layout.html" %}
|
|
2
|
-
{% block content %}
|
|
3
|
-
<div class="col-12">
|
|
4
|
-
<div class="card">
|
|
5
|
-
<div class="card-header">
|
|
6
|
-
<h3 class="card-title">{{ model_view.name_plural }}</h3>
|
|
7
|
-
<div class="ms-auto">
|
|
8
|
-
{% if model_view.can_export %}
|
|
9
|
-
{% if model_view.export_types | length > 1 %}
|
|
10
|
-
<div class="ms-3 d-inline-block dropdown">
|
|
11
|
-
<a href="#" class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown"
|
|
12
|
-
aria-expanded="false">
|
|
13
|
-
Export
|
|
14
|
-
</a>
|
|
15
|
-
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
|
|
16
|
-
{% for export_type in model_view.export_types %}
|
|
17
|
-
<li><a class="dropdown-item"
|
|
18
|
-
href="{{ url_for('admin:export', identity=model_view.identity, export_type=export_type) }}">{{
|
|
19
|
-
export_type | upper }}</a></li>
|
|
20
|
-
{% endfor %}
|
|
21
|
-
</ul>
|
|
22
|
-
</div>
|
|
23
|
-
{% elif model_view.export_types | length == 1 %}
|
|
24
|
-
<div class="ms-3 d-inline-block">
|
|
25
|
-
<a href="{{ url_for('admin:export', identity=model_view.identity, export_type=model_view.export_types[0]) }}"
|
|
26
|
-
class="btn btn-secondary">
|
|
27
|
-
Export
|
|
28
|
-
</a>
|
|
29
|
-
</div>
|
|
30
|
-
{% endif %}
|
|
31
|
-
{% endif %}
|
|
32
|
-
{% if model_view.can_create %}
|
|
33
|
-
<div class="ms-3 d-inline-block">
|
|
34
|
-
<a href="{{ url_for('admin:create', identity=model_view.identity) }}" class="btn btn-primary">
|
|
35
|
-
+ New {{ model_view.name }}
|
|
36
|
-
</a>
|
|
37
|
-
</div>
|
|
38
|
-
{% endif %}
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="card-body border-bottom py-3">
|
|
42
|
-
<div class="d-flex justify-content-between">
|
|
43
|
-
<div class="dropdown col-4">
|
|
44
|
-
<button {% if not model_view.can_delete and not model_view._custom_actions_in_list %} disabled {% endif %}
|
|
45
|
-
class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
|
|
46
|
-
aria-haspopup="true" aria-expanded="false">
|
|
47
|
-
Actions
|
|
48
|
-
</button>
|
|
49
|
-
{% if model_view.can_delete or model_view._custom_actions_in_list %}
|
|
50
|
-
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
|
|
51
|
-
{% if model_view.can_delete %}
|
|
52
|
-
<a class="dropdown-item" id="action-delete" href="#" data-name="{{ model_view.name }}"
|
|
53
|
-
data-url="{{ url_for('admin:delete', identity=model_view.identity) }}" data-bs-toggle="modal"
|
|
54
|
-
data-bs-target="#modal-delete">Delete selected items</a>
|
|
55
|
-
{% endif %}
|
|
56
|
-
{% for custom_action, label in model_view._custom_actions_in_list.items() %}
|
|
57
|
-
{% if custom_action in model_view._custom_actions_confirmation %}
|
|
58
|
-
<a class="dropdown-item" id="action-customconfirm-{{ custom_action }}" href="#" data-bs-toggle="modal"
|
|
59
|
-
data-bs-target="#modal-confirmation-{{ custom_action }}">
|
|
60
|
-
{{ label }}
|
|
61
|
-
</a>
|
|
62
|
-
{% else %}
|
|
63
|
-
<a class="dropdown-item" id="action-custom-{{ custom_action }}" href="#"
|
|
64
|
-
data-url="{{ model_view._url_for_action(request, custom_action) }}">
|
|
65
|
-
{{ label }}
|
|
66
|
-
</a>
|
|
67
|
-
{% endif %}
|
|
68
|
-
{% endfor %}
|
|
69
|
-
</div>
|
|
70
|
-
{% endif %}
|
|
71
|
-
</div>
|
|
72
|
-
{% if model_view.column_searchable_list %}
|
|
73
|
-
<div class="col-md-4 text-muted">
|
|
74
|
-
<div class="input-group">
|
|
75
|
-
<input id="search-input" type="text" class="form-control"
|
|
76
|
-
placeholder="Search: {{ model_view.search_placeholder() }}"
|
|
77
|
-
value="{{ request.query_params.get('search', '') }}">
|
|
78
|
-
<button id="search-button" class="btn" type="button">Search</button>
|
|
79
|
-
<button id="search-reset" class="btn" type="button" {% if not request.query_params.get('search')
|
|
80
|
-
%}disabled{% endif %}><i class="fa-solid fa-times"></i></button>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
{% endif %}
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
<div class="table-responsive">
|
|
87
|
-
<table class="table card-table table-vcenter text-nowrap">
|
|
88
|
-
<thead>
|
|
89
|
-
<tr>
|
|
90
|
-
<th class="w-1"><input class="form-check-input m-0 align-middle" type="checkbox" aria-label="Select all"
|
|
91
|
-
id="select-all"></th>
|
|
92
|
-
<th class="w-1"></th>
|
|
93
|
-
{% for name in model_view._list_prop_names %}
|
|
94
|
-
{% set label = model_view._column_labels.get(name, name) %}
|
|
95
|
-
<th>
|
|
96
|
-
{% if name in model_view._sort_fields %}
|
|
97
|
-
{% if request.query_params.get("sortBy") == name and request.query_params.get("sort") == "asc" %}
|
|
98
|
-
<a href="{{ request.url.include_query_params(sort='desc') }}"><i class="fa-solid fa-arrow-up"></i> {{
|
|
99
|
-
label }}</a>
|
|
100
|
-
{% elif request.query_params.get("sortBy") == name and request.query_params.get("sort") == "desc" %}
|
|
101
|
-
<a href="{{ request.url.include_query_params(sort='asc') }}"><i class="fa-solid fa-arrow-down"></i> {{ label
|
|
102
|
-
}}</a>
|
|
103
|
-
{% else %}
|
|
104
|
-
<a href="{{ request.url.include_query_params(sortBy=name, sort='asc') }}">{{ label }}</a>
|
|
105
|
-
{% endif %}
|
|
106
|
-
{% else %}
|
|
107
|
-
{{ label }}
|
|
108
|
-
{% endif %}
|
|
109
|
-
</th>
|
|
110
|
-
{% endfor %}
|
|
111
|
-
</tr>
|
|
112
|
-
</thead>
|
|
113
|
-
<tbody>
|
|
114
|
-
{% for row in pagination.rows %}
|
|
115
|
-
<tr>
|
|
116
|
-
<td>
|
|
117
|
-
<input type="hidden" value="{{ get_object_identifier(row) }}">
|
|
118
|
-
<input class="form-check-input m-0 align-middle select-box" type="checkbox" aria-label="Select item">
|
|
119
|
-
</td>
|
|
120
|
-
<td class="text-end">
|
|
121
|
-
{% if model_view.can_view_details %}
|
|
122
|
-
<a href="{{ model_view._build_url_for('admin:details', request, row) }}" data-bs-toggle="tooltip"
|
|
123
|
-
data-bs-placement="top" title="View">
|
|
124
|
-
<span class="me-1"><i class="fa-solid fa-eye"></i></span>
|
|
125
|
-
</a>
|
|
126
|
-
{% endif %}
|
|
127
|
-
{% if model_view.can_edit %}
|
|
128
|
-
<a href="{{ model_view._build_url_for('admin:edit', request, row) }}" data-bs-toggle="tooltip"
|
|
129
|
-
data-bs-placement="top" title="Edit">
|
|
130
|
-
<span class="me-1"><i class="fa-solid fa-pen-to-square"></i></span>
|
|
131
|
-
</a>
|
|
132
|
-
{% endif %}
|
|
133
|
-
{% if model_view.can_delete %}
|
|
134
|
-
<a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(row) }}"
|
|
135
|
-
data-url="{{ model_view._url_for_delete(request, row) }}" data-bs-toggle="modal"
|
|
136
|
-
data-bs-target="#modal-delete" title="Delete">
|
|
137
|
-
<span class="me-1"><i class="fa-solid fa-trash"></i></span>
|
|
138
|
-
</a>
|
|
139
|
-
{% endif %}
|
|
140
|
-
</td>
|
|
141
|
-
{% for name in model_view._list_prop_names %}
|
|
142
|
-
{% set value, formatted_value = model_view.get_list_value(row, name) %}
|
|
143
|
-
{% if name in model_view._relation_names %}
|
|
144
|
-
{% if is_list( value ) %}
|
|
145
|
-
<td>
|
|
146
|
-
{% for elem, formatted_elem in zip(value, formatted_value) %}
|
|
147
|
-
<a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
|
|
148
|
-
{% endfor %}
|
|
149
|
-
</td>
|
|
150
|
-
{% else %}
|
|
151
|
-
<td><a href="{{ model_view._url_for_details_with_prop(request, row, name) }}">{{ formatted_value }}</a></td>
|
|
152
|
-
{% endif %}
|
|
153
|
-
{% else %}
|
|
154
|
-
<td>{{ formatted_value }}</td>
|
|
155
|
-
{% endif %}
|
|
156
|
-
{% endfor %}
|
|
157
|
-
</tr>
|
|
158
|
-
{% endfor %}
|
|
159
|
-
</tbody>
|
|
160
|
-
</table>
|
|
161
|
-
</div>
|
|
162
|
-
<div class="card-footer d-flex justify-content-between align-items-center gap-2">
|
|
163
|
-
<p class="m-0 text-muted">Showing <span>{{ ((pagination.page - 1) * pagination.page_size) + 1 }}</span> to
|
|
164
|
-
<span>{{ min(pagination.page * pagination.page_size, pagination.count) }}</span> of <span>{{ pagination.count
|
|
165
|
-
}}</span> items
|
|
166
|
-
</p>
|
|
167
|
-
<ul class="pagination m-0 ms-auto">
|
|
168
|
-
<li class="page-item {% if not pagination.has_previous %}disabled{% endif %}">
|
|
169
|
-
{% if pagination.has_previous %}
|
|
170
|
-
<a class="page-link" href="{{ pagination.previous_page.url }}">
|
|
171
|
-
{% else %}
|
|
172
|
-
<a class="page-link" href="#">
|
|
173
|
-
{% endif %}
|
|
174
|
-
<i class="fa-solid fa-chevron-left"></i>
|
|
175
|
-
prev
|
|
176
|
-
</a>
|
|
177
|
-
</li>
|
|
178
|
-
{% for page_control in pagination.page_controls %}
|
|
179
|
-
<li class="page-item {% if page_control.number == pagination.page %}active{% endif %}"><a class="page-link"
|
|
180
|
-
href="{{ page_control.url }}">{{ page_control.number }}</a></li>
|
|
181
|
-
{% endfor %}
|
|
182
|
-
<li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
|
|
183
|
-
{% if pagination.has_next %}
|
|
184
|
-
<a class="page-link" href="{{ pagination.next_page.url }}">
|
|
185
|
-
{% else %}
|
|
186
|
-
<a class="page-link" href="#">
|
|
187
|
-
{% endif %}
|
|
188
|
-
next
|
|
189
|
-
<i class="fa-solid fa-chevron-right"></i>
|
|
190
|
-
</a>
|
|
191
|
-
</li>
|
|
192
|
-
</ul>
|
|
193
|
-
<div class="dropdown text-muted">
|
|
194
|
-
Show
|
|
195
|
-
<a href="#" class="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
|
|
196
|
-
aria-expanded="false">
|
|
197
|
-
{{ request.query_params.get("pageSize") or model_view.page_size }} / Page
|
|
198
|
-
</a>
|
|
199
|
-
<div class="dropdown-menu">
|
|
200
|
-
{% for page_size_option in model_view.page_size_options %}
|
|
201
|
-
<a class="dropdown-item" href="{{ request.url.include_query_params(pageSize=page_size_option, page=pagination.resize(page_size_option).page) }}">
|
|
202
|
-
{{ page_size_option }} / Page
|
|
203
|
-
</a>
|
|
204
|
-
{% endfor %}
|
|
205
|
-
</div>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
{% if model_view.can_delete %}
|
|
210
|
-
{% include 'sqladmin/modals/delete.html' %}
|
|
211
|
-
{% endif %}
|
|
212
|
-
|
|
213
|
-
{% for custom_action in model_view._custom_actions_in_list %}
|
|
214
|
-
{% if custom_action in model_view._custom_actions_confirmation %}
|
|
215
|
-
{% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action,
|
|
216
|
-
url=model_view._url_for_action(request, custom_action) %}
|
|
217
|
-
{% include 'sqladmin/modals/list_action_confirmation.html' %}
|
|
218
|
-
{% endwith %}
|
|
219
|
-
{% endif %}
|
|
220
|
-
{% endfor %}
|
|
221
|
-
</div>
|
|
222
|
-
{% endblock %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqladmin-0.20.1 → sqladmin-0.21.0}/sqladmin/templates/sqladmin/modals/list_action_confirmation.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|