sqladmin 0.20.1__py3-none-any.whl → 0.22.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 +1 -1
- sqladmin/_types.py +49 -1
- sqladmin/application.py +22 -8
- sqladmin/authentication.py +6 -2
- sqladmin/filters.py +340 -0
- sqladmin/helpers.py +12 -4
- sqladmin/models.py +89 -14
- sqladmin/statics/css/main.css +8 -1
- sqladmin/templates/sqladmin/_macros.html +5 -2
- sqladmin/templates/sqladmin/details.html +5 -1
- sqladmin/templates/sqladmin/list.html +274 -197
- {sqladmin-0.20.1.dist-info → sqladmin-0.22.0.dist-info}/METADATA +7 -6
- {sqladmin-0.20.1.dist-info → sqladmin-0.22.0.dist-info}/RECORD +16 -15
- {sqladmin-0.20.1.dist-info → sqladmin-0.22.0.dist-info}/WHEEL +1 -1
- {sqladmin-0.20.1.dist-info → sqladmin-0.22.0.dist-info}/licenses/LICENSE.md +0 -0
sqladmin/models.py
CHANGED
|
@@ -19,6 +19,7 @@ from typing import (
|
|
|
19
19
|
Union,
|
|
20
20
|
no_type_check,
|
|
21
21
|
)
|
|
22
|
+
from typing import cast as typing_cast
|
|
22
23
|
from urllib.parse import urlencode
|
|
23
24
|
|
|
24
25
|
import anyio
|
|
@@ -36,7 +37,12 @@ from wtforms import Field, Form
|
|
|
36
37
|
from wtforms.fields.core import UnboundField
|
|
37
38
|
|
|
38
39
|
from sqladmin._queries import Query
|
|
39
|
-
from sqladmin._types import
|
|
40
|
+
from sqladmin._types import (
|
|
41
|
+
MODEL_ATTR,
|
|
42
|
+
ColumnFilter,
|
|
43
|
+
OperationColumnFilter,
|
|
44
|
+
SimpleColumnFilter,
|
|
45
|
+
)
|
|
40
46
|
from sqladmin.ajax import create_ajax_loader
|
|
41
47
|
from sqladmin.exceptions import InvalidModelError
|
|
42
48
|
from sqladmin.formatters import BASE_FORMATTERS
|
|
@@ -183,6 +189,9 @@ class BaseView(BaseModelView):
|
|
|
183
189
|
category: ClassVar[str] = ""
|
|
184
190
|
"""Category name to group views together."""
|
|
185
191
|
|
|
192
|
+
category_icon: ClassVar[str] = ""
|
|
193
|
+
"""Display icon for category in the sidebar."""
|
|
194
|
+
|
|
186
195
|
|
|
187
196
|
class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
188
197
|
"""Base class for defining admnistrative behaviour for the model.
|
|
@@ -312,6 +321,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
312
321
|
```
|
|
313
322
|
"""
|
|
314
323
|
|
|
324
|
+
column_filters: ClassVar[Sequence[ColumnFilter]] = []
|
|
325
|
+
"""Collection of the filterable columns for the list view.
|
|
326
|
+
Columns can either be string names or SQLAlchemy columns.
|
|
327
|
+
|
|
328
|
+
???+ example
|
|
329
|
+
```python
|
|
330
|
+
class UserAdmin(ModelView, model=User):
|
|
331
|
+
column_filters = [User.is_admin]
|
|
332
|
+
```
|
|
333
|
+
"""
|
|
334
|
+
|
|
315
335
|
column_sortable_list: ClassVar[Sequence[MODEL_ATTR]] = []
|
|
316
336
|
"""Collection of the sortable columns for the list view.
|
|
317
337
|
|
|
@@ -398,7 +418,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
398
418
|
"""
|
|
399
419
|
|
|
400
420
|
save_as: ClassVar[bool] = False
|
|
401
|
-
"""Set `save_as` to enable a
|
|
421
|
+
"""Set `save_as` to enable a "save as new" feature on admin change forms.
|
|
402
422
|
|
|
403
423
|
Normally, objects have three save options:
|
|
404
424
|
``Save`, `Save and continue editing` and `Save and add another`.
|
|
@@ -432,6 +452,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
432
452
|
edit_template: ClassVar[str] = "sqladmin/edit.html"
|
|
433
453
|
"""Edit view template. Default is `sqladmin/edit.html`."""
|
|
434
454
|
|
|
455
|
+
# Template configuration
|
|
456
|
+
show_compact_lists: ClassVar[bool] = True
|
|
457
|
+
"""Show compact lists. Default is `True`.
|
|
458
|
+
If False, when showing lists of objects, each object will be \
|
|
459
|
+
displayed in a separate line."""
|
|
460
|
+
|
|
435
461
|
# Export
|
|
436
462
|
column_export_list: ClassVar[List[MODEL_ATTR]] = []
|
|
437
463
|
"""List of columns to include when exporting.
|
|
@@ -645,7 +671,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
645
671
|
- None will be displayed as an empty string
|
|
646
672
|
- bool will be displayed as a checkmark if it is True otherwise as an X.
|
|
647
673
|
|
|
648
|
-
If you don
|
|
674
|
+
If you don't like the default behavior and don't want any type formatters applied,
|
|
649
675
|
just override this property with an empty dictionary:
|
|
650
676
|
|
|
651
677
|
???+ example
|
|
@@ -718,6 +744,19 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
718
744
|
self._custom_actions_in_detail: Dict[str, str] = {}
|
|
719
745
|
self._custom_actions_confirmation: Dict[str, str] = {}
|
|
720
746
|
|
|
747
|
+
def _run_arbitrary_query_sync(self, stmt: ClauseElement) -> Any:
|
|
748
|
+
with self.session_maker(expire_on_commit=False) as session:
|
|
749
|
+
result = session.execute(stmt)
|
|
750
|
+
return result.all()
|
|
751
|
+
|
|
752
|
+
async def _run_arbitrary_query(self, stmt: ClauseElement) -> Any:
|
|
753
|
+
if self.is_async:
|
|
754
|
+
async with self.session_maker(expire_on_commit=False) as session:
|
|
755
|
+
result = await session.execute(stmt)
|
|
756
|
+
return result.all()
|
|
757
|
+
else:
|
|
758
|
+
return self._run_arbitrary_query_sync(stmt)
|
|
759
|
+
|
|
721
760
|
def _run_query_sync(self, stmt: ClauseElement) -> Any:
|
|
722
761
|
with self.session_maker(expire_on_commit=False) as session:
|
|
723
762
|
result = session.execute(stmt)
|
|
@@ -803,13 +842,34 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
803
842
|
for relation in self._list_relations:
|
|
804
843
|
stmt = stmt.options(selectinload(relation))
|
|
805
844
|
|
|
845
|
+
for filter in self.get_filters():
|
|
846
|
+
filter_param_name = filter.parameter_name
|
|
847
|
+
filter_value = request.query_params.get(filter_param_name)
|
|
848
|
+
|
|
849
|
+
if filter_value:
|
|
850
|
+
if hasattr(filter, "has_operator") and filter.has_operator:
|
|
851
|
+
# Use operation-based filtering
|
|
852
|
+
operation_filter = typing_cast(OperationColumnFilter, filter)
|
|
853
|
+
operation_param = request.query_params.get(
|
|
854
|
+
f"{filter_param_name}_op"
|
|
855
|
+
)
|
|
856
|
+
if operation_param:
|
|
857
|
+
stmt = await operation_filter.get_filtered_query(
|
|
858
|
+
stmt, operation_param, filter_value, self.model
|
|
859
|
+
)
|
|
860
|
+
else:
|
|
861
|
+
# Use simple filtering for filters without operators
|
|
862
|
+
simple_filter = typing_cast(SimpleColumnFilter, filter)
|
|
863
|
+
stmt = await simple_filter.get_filtered_query(
|
|
864
|
+
stmt, filter_value, self.model
|
|
865
|
+
)
|
|
866
|
+
|
|
806
867
|
stmt = self.sort_query(stmt, request)
|
|
807
868
|
|
|
808
869
|
if search:
|
|
809
870
|
stmt = self.search_query(stmt=stmt, term=search)
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
count = await self.count(request)
|
|
871
|
+
|
|
872
|
+
count = await self.count(request, select(func.count()).select_from(stmt))
|
|
813
873
|
|
|
814
874
|
stmt = stmt.limit(page_size).offset((page - 1) * page_size)
|
|
815
875
|
rows = await self._run_query(stmt)
|
|
@@ -840,12 +900,8 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
840
900
|
rows = await self._run_query(stmt)
|
|
841
901
|
return rows[0] if rows else None
|
|
842
902
|
|
|
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
|
-
|
|
903
|
+
async def get_object_for_details(self, request: Request) -> Any:
|
|
904
|
+
stmt = self.details_query(request)
|
|
849
905
|
return await self._get_object_by_pk(stmt)
|
|
850
906
|
|
|
851
907
|
async def get_object_for_edit(self, request: Request) -> Any:
|
|
@@ -973,6 +1029,15 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
973
1029
|
defaults=self._list_prop_names,
|
|
974
1030
|
)
|
|
975
1031
|
|
|
1032
|
+
def get_filters(self) -> List[ColumnFilter]:
|
|
1033
|
+
"""Get list of filters."""
|
|
1034
|
+
|
|
1035
|
+
filters = getattr(self, "column_filters", None)
|
|
1036
|
+
if not filters:
|
|
1037
|
+
return []
|
|
1038
|
+
|
|
1039
|
+
return filters
|
|
1040
|
+
|
|
976
1041
|
async def on_model_change(
|
|
977
1042
|
self, data: dict, model: Any, is_created: bool, request: Request
|
|
978
1043
|
) -> None:
|
|
@@ -1088,6 +1153,14 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1088
1153
|
|
|
1089
1154
|
return select(self.model)
|
|
1090
1155
|
|
|
1156
|
+
def details_query(self, request: Request) -> Select:
|
|
1157
|
+
"""
|
|
1158
|
+
The SQLAlchemy select expression used for the details page which can be
|
|
1159
|
+
customized. By default it will select all objects without any filters.
|
|
1160
|
+
"""
|
|
1161
|
+
|
|
1162
|
+
return self.form_edit_query(request)
|
|
1163
|
+
|
|
1091
1164
|
def edit_form_query(self, request: Request) -> Select:
|
|
1092
1165
|
msg = (
|
|
1093
1166
|
"Overriding 'edit_form_query' is deprecated. Use 'form_edit_query' instead."
|
|
@@ -1184,7 +1257,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1184
1257
|
|
|
1185
1258
|
return StreamingResponse(
|
|
1186
1259
|
content=stream_to_csv(generate),
|
|
1187
|
-
media_type="text/csv",
|
|
1260
|
+
media_type="text/csv; charset=utf-8",
|
|
1188
1261
|
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
1189
1262
|
)
|
|
1190
1263
|
|
|
@@ -1203,7 +1276,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1203
1276
|
name: str(await self.get_prop_value(row, name))
|
|
1204
1277
|
for name in self._export_prop_names
|
|
1205
1278
|
}
|
|
1206
|
-
yield json.dumps(row_dict) + (
|
|
1279
|
+
yield json.dumps(row_dict, ensure_ascii=False) + (
|
|
1280
|
+
separator if idx < last_idx else ""
|
|
1281
|
+
)
|
|
1207
1282
|
|
|
1208
1283
|
yield "]"
|
|
1209
1284
|
|
sqladmin/statics/css/main.css
CHANGED
|
@@ -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 %}
|