sqladmin 0.22.0__py3-none-any.whl → 0.23.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 +0 -2
- sqladmin/_menu.py +1 -1
- sqladmin/_queries.py +22 -14
- sqladmin/_types.py +6 -8
- sqladmin/_validators.py +14 -8
- sqladmin/ajax.py +2 -2
- sqladmin/application.py +35 -20
- sqladmin/fields.py +29 -20
- sqladmin/filters.py +74 -45
- sqladmin/formatters.py +1 -1
- sqladmin/forms.py +140 -51
- sqladmin/helpers.py +12 -5
- sqladmin/models.py +71 -29
- sqladmin/pretty_export.py +75 -0
- sqladmin/templates/sqladmin/details.html +7 -7
- sqladmin/templates/sqladmin/list.html +13 -2
- sqladmin/templating.py +1 -1
- sqladmin/widgets.py +20 -14
- {sqladmin-0.22.0.dist-info → sqladmin-0.23.0.dist-info}/METADATA +15 -15
- {sqladmin-0.22.0.dist-info → sqladmin-0.23.0.dist-info}/RECORD +21 -21
- {sqladmin-0.22.0.dist-info → sqladmin-0.23.0.dist-info}/WHEEL +1 -1
- sqladmin-0.22.0.dist-info/licenses/LICENSE.md +0 -27
sqladmin/helpers.py
CHANGED
|
@@ -123,7 +123,11 @@ def secure_filename(filename: str) -> str:
|
|
|
123
123
|
if (
|
|
124
124
|
os.name == "nt"
|
|
125
125
|
and filename
|
|
126
|
-
and filename.split(
|
|
126
|
+
and filename.split(
|
|
127
|
+
".",
|
|
128
|
+
maxsplit=1,
|
|
129
|
+
)[0].upper()
|
|
130
|
+
in _windows_device_files
|
|
127
131
|
):
|
|
128
132
|
filename = f"_{filename}" # pragma: no cover
|
|
129
133
|
|
|
@@ -238,7 +242,9 @@ def object_identifier_values(id_string: str, model: Any) -> tuple:
|
|
|
238
242
|
|
|
239
243
|
|
|
240
244
|
def get_direction(prop: MODEL_PROPERTY) -> str:
|
|
241
|
-
|
|
245
|
+
if not isinstance(prop, RelationshipProperty):
|
|
246
|
+
raise TypeError("Expected RelationshipProperty, got %s" % type(prop))
|
|
247
|
+
|
|
242
248
|
name = prop.direction.name
|
|
243
249
|
if name == "ONETOMANY" and not prop.uselist:
|
|
244
250
|
return "ONETOONE"
|
|
@@ -285,10 +291,11 @@ def parse_interval(value: str) -> timedelta | None:
|
|
|
285
291
|
def is_falsy_value(value: Any) -> bool:
|
|
286
292
|
if value is None:
|
|
287
293
|
return True
|
|
288
|
-
|
|
294
|
+
|
|
295
|
+
if not value and isinstance(value, str):
|
|
289
296
|
return True
|
|
290
|
-
|
|
291
|
-
|
|
297
|
+
|
|
298
|
+
return False
|
|
292
299
|
|
|
293
300
|
|
|
294
301
|
def choice_type_coerce_factory(type_: Any) -> Callable[[Any], Any]:
|
sqladmin/models.py
CHANGED
|
@@ -60,10 +60,11 @@ from sqladmin.helpers import (
|
|
|
60
60
|
|
|
61
61
|
# stream_to_csv,
|
|
62
62
|
from sqladmin.pagination import Pagination
|
|
63
|
+
from sqladmin.pretty_export import PrettyExport
|
|
63
64
|
from sqladmin.templating import Jinja2Templates
|
|
64
65
|
|
|
65
66
|
if TYPE_CHECKING:
|
|
66
|
-
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
67
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker # type: ignore[attr-defined]
|
|
67
68
|
|
|
68
69
|
from sqladmin.application import BaseAdmin
|
|
69
70
|
|
|
@@ -82,8 +83,8 @@ class ModelViewMeta(type):
|
|
|
82
83
|
"""
|
|
83
84
|
|
|
84
85
|
@no_type_check
|
|
85
|
-
def __new__(
|
|
86
|
-
cls: Type["ModelView"] = super().__new__(
|
|
86
|
+
def __new__(mcs, name, bases, attrs: dict, **kwargs: Any):
|
|
87
|
+
cls: Type["ModelView"] = super().__new__(mcs, name, bases, attrs)
|
|
87
88
|
|
|
88
89
|
model = kwargs.get("model")
|
|
89
90
|
|
|
@@ -92,10 +93,10 @@ class ModelViewMeta(type):
|
|
|
92
93
|
|
|
93
94
|
try:
|
|
94
95
|
inspect(model)
|
|
95
|
-
except NoInspectionAvailable:
|
|
96
|
+
except NoInspectionAvailable as exc:
|
|
96
97
|
raise InvalidModelError(
|
|
97
98
|
f"Class {model.__name__} is not a SQLAlchemy model."
|
|
98
|
-
)
|
|
99
|
+
) from exc
|
|
99
100
|
|
|
100
101
|
cls.pk_columns = get_primary_keys(model)
|
|
101
102
|
cls.identity = slugify_class_name(model.__name__)
|
|
@@ -104,21 +105,19 @@ class ModelViewMeta(type):
|
|
|
104
105
|
cls.name = attrs.get("name", prettify_class_name(cls.model.__name__))
|
|
105
106
|
cls.name_plural = attrs.get("name_plural", f"{cls.name}s")
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
mcls._check_conflicting_options(
|
|
108
|
+
mcs._check_conflicting_options(["column_list", "column_exclude_list"], attrs)
|
|
109
|
+
mcs._check_conflicting_options(["form_columns", "form_excluded_columns"], attrs)
|
|
110
|
+
mcs._check_conflicting_options(
|
|
112
111
|
["column_details_list", "column_details_exclude_list"], attrs
|
|
113
112
|
)
|
|
114
|
-
|
|
113
|
+
mcs._check_conflicting_options(
|
|
115
114
|
["column_export_list", "column_export_exclude_list"], attrs
|
|
116
115
|
)
|
|
117
116
|
|
|
118
117
|
return cls
|
|
119
118
|
|
|
120
119
|
@classmethod
|
|
121
|
-
def _check_conflicting_options(
|
|
120
|
+
def _check_conflicting_options(mcs, keys: List[str], attrs: dict) -> None:
|
|
122
121
|
if all(k in attrs for k in keys):
|
|
123
122
|
raise AssertionError(f"Cannot use {' and '.join(keys)} together.")
|
|
124
123
|
|
|
@@ -211,7 +210,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
211
210
|
|
|
212
211
|
# Internals
|
|
213
212
|
pk_columns: ClassVar[Tuple[Column]]
|
|
214
|
-
session_maker: ClassVar[
|
|
213
|
+
session_maker: ClassVar[ # type: ignore[no-any-unimported]
|
|
214
|
+
Union[
|
|
215
|
+
sessionmaker,
|
|
216
|
+
"async_sessionmaker",
|
|
217
|
+
]
|
|
218
|
+
]
|
|
215
219
|
is_async: ClassVar[bool] = False
|
|
216
220
|
is_model: ClassVar[bool] = True
|
|
217
221
|
ajax_lookup_url: ClassVar[str] = ""
|
|
@@ -491,6 +495,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
491
495
|
Unlimited by default.
|
|
492
496
|
"""
|
|
493
497
|
|
|
498
|
+
use_pretty_export: ClassVar[bool] = False
|
|
499
|
+
"""
|
|
500
|
+
Enable export of CSV files using column labels and column formatters.
|
|
501
|
+
|
|
502
|
+
If set to True, the export will apply column labels and formatting logic
|
|
503
|
+
used in the list template.
|
|
504
|
+
Otherwise, raw database values and field names will be used.
|
|
505
|
+
|
|
506
|
+
You can override cell formatting per column by implementing `custom_export_cell`.
|
|
507
|
+
"""
|
|
508
|
+
|
|
494
509
|
# Form
|
|
495
510
|
form: ClassVar[Optional[Type[Form]]] = None
|
|
496
511
|
"""Form class.
|
|
@@ -803,8 +818,8 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
803
818
|
return self.column_default_sort
|
|
804
819
|
if isinstance(self.column_default_sort, tuple):
|
|
805
820
|
return [self.column_default_sort]
|
|
806
|
-
|
|
807
|
-
|
|
821
|
+
|
|
822
|
+
return [(self.column_default_sort, False)]
|
|
808
823
|
|
|
809
824
|
return [(pk.name, False) for pk in self.pk_columns]
|
|
810
825
|
|
|
@@ -821,10 +836,10 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
821
836
|
|
|
822
837
|
try:
|
|
823
838
|
return int(number)
|
|
824
|
-
except ValueError:
|
|
839
|
+
except ValueError as exc:
|
|
825
840
|
raise HTTPException(
|
|
826
841
|
status_code=400, detail="Invalid page or pageSize parameter"
|
|
827
|
-
)
|
|
842
|
+
) from exc
|
|
828
843
|
|
|
829
844
|
async def count(self, request: Request, stmt: Optional[Select] = None) -> int:
|
|
830
845
|
if stmt is None:
|
|
@@ -842,14 +857,14 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
842
857
|
for relation in self._list_relations:
|
|
843
858
|
stmt = stmt.options(selectinload(relation))
|
|
844
859
|
|
|
845
|
-
for
|
|
846
|
-
filter_param_name =
|
|
860
|
+
for filter_ in self.get_filters():
|
|
861
|
+
filter_param_name = filter_.parameter_name
|
|
847
862
|
filter_value = request.query_params.get(filter_param_name)
|
|
848
863
|
|
|
849
864
|
if filter_value:
|
|
850
|
-
if hasattr(
|
|
865
|
+
if hasattr(filter_, "has_operator") and filter_.has_operator:
|
|
851
866
|
# Use operation-based filtering
|
|
852
|
-
operation_filter = typing_cast(OperationColumnFilter,
|
|
867
|
+
operation_filter = typing_cast(OperationColumnFilter, filter_)
|
|
853
868
|
operation_param = request.query_params.get(
|
|
854
869
|
f"{filter_param_name}_op"
|
|
855
870
|
)
|
|
@@ -859,7 +874,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
859
874
|
)
|
|
860
875
|
else:
|
|
861
876
|
# Use simple filtering for filters without operators
|
|
862
|
-
simple_filter = typing_cast(SimpleColumnFilter,
|
|
877
|
+
simple_filter = typing_cast(SimpleColumnFilter, filter_)
|
|
863
878
|
stmt = await simple_filter.get_filtered_query(
|
|
864
879
|
stmt, filter_value, self.model
|
|
865
880
|
)
|
|
@@ -869,7 +884,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
869
884
|
if search:
|
|
870
885
|
stmt = self.search_query(stmt=stmt, term=search)
|
|
871
886
|
|
|
872
|
-
count = await self.count(
|
|
887
|
+
count = await self.count(
|
|
888
|
+
request, select(func.count()).select_from(stmt.subquery())
|
|
889
|
+
)
|
|
873
890
|
|
|
874
891
|
stmt = stmt.limit(page_size).offset((page - 1) * page_size)
|
|
875
892
|
rows = await self._run_query(stmt)
|
|
@@ -971,14 +988,16 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
971
988
|
"""This function generalizes constructing a list of columns
|
|
972
989
|
for any sequence of inclusions or exclusions.
|
|
973
990
|
"""
|
|
974
|
-
|
|
975
991
|
if include == "__all__":
|
|
976
992
|
return self._prop_names
|
|
977
|
-
|
|
993
|
+
|
|
994
|
+
if include:
|
|
978
995
|
return [self._get_prop_name(item) for item in include]
|
|
979
|
-
|
|
996
|
+
|
|
997
|
+
if exclude:
|
|
980
998
|
exclude = [self._get_prop_name(item) for item in exclude]
|
|
981
999
|
return [prop for prop in self._prop_names if prop not in exclude]
|
|
1000
|
+
|
|
982
1001
|
return defaults
|
|
983
1002
|
|
|
984
1003
|
def get_list_columns(self) -> List[str]:
|
|
@@ -1087,7 +1106,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1087
1106
|
|
|
1088
1107
|
form = await get_model_form(
|
|
1089
1108
|
model=self.model,
|
|
1090
|
-
session_maker=self.session_maker,
|
|
1109
|
+
session_maker=self.session_maker, # type: ignore[arg-type]
|
|
1091
1110
|
only=self._form_prop_names,
|
|
1092
1111
|
column_labels=self._column_labels,
|
|
1093
1112
|
form_args=self.form_args,
|
|
@@ -1231,9 +1250,16 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1231
1250
|
export_type: str = "csv",
|
|
1232
1251
|
) -> StreamingResponse:
|
|
1233
1252
|
if export_type == "csv":
|
|
1234
|
-
|
|
1235
|
-
|
|
1253
|
+
export_method = (
|
|
1254
|
+
PrettyExport.pretty_export_csv(self, data)
|
|
1255
|
+
if self.use_pretty_export
|
|
1256
|
+
else self._export_csv(data)
|
|
1257
|
+
)
|
|
1258
|
+
return await export_method
|
|
1259
|
+
|
|
1260
|
+
if export_type == "json":
|
|
1236
1261
|
return await self._export_json(data)
|
|
1262
|
+
|
|
1237
1263
|
raise NotImplementedError("Only export_type='csv' or 'json' is implemented.")
|
|
1238
1264
|
|
|
1239
1265
|
async def _export_csv(
|
|
@@ -1289,6 +1315,22 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1289
1315
|
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
1290
1316
|
)
|
|
1291
1317
|
|
|
1318
|
+
async def custom_export_cell(
|
|
1319
|
+
self,
|
|
1320
|
+
row: Any,
|
|
1321
|
+
name: str,
|
|
1322
|
+
value: Any,
|
|
1323
|
+
) -> Optional[str]:
|
|
1324
|
+
"""
|
|
1325
|
+
Override to provide custom formatting for a specific cell in pretty export.
|
|
1326
|
+
|
|
1327
|
+
Return a string to override the default formatting for the given field,
|
|
1328
|
+
or return None to fall back to `base_export_cell`.
|
|
1329
|
+
|
|
1330
|
+
Only used when `use_pretty_export = True`.
|
|
1331
|
+
"""
|
|
1332
|
+
return None
|
|
1333
|
+
|
|
1292
1334
|
def _refresh_form_rules_cache(self) -> None:
|
|
1293
1335
|
if self.form_rules:
|
|
1294
1336
|
self._form_create_rules = self.form_rules
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, List
|
|
2
|
+
|
|
3
|
+
from starlette.responses import StreamingResponse
|
|
4
|
+
|
|
5
|
+
from sqladmin.helpers import Writer, secure_filename, stream_to_csv
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .models import ModelView
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PrettyExport:
|
|
12
|
+
@staticmethod
|
|
13
|
+
async def _base_export_cell(
|
|
14
|
+
model_view: "ModelView", name: str, value: Any, formatted_value: Any
|
|
15
|
+
) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Default formatting logic for a cell in pretty export.
|
|
18
|
+
|
|
19
|
+
Used when `custom_export_cell` returns None.
|
|
20
|
+
Applies standard rules for related fields, booleans, etc.
|
|
21
|
+
|
|
22
|
+
Only used when `use_pretty_export = True`.
|
|
23
|
+
"""
|
|
24
|
+
if name in model_view._relation_names:
|
|
25
|
+
if isinstance(value, list):
|
|
26
|
+
cell_value = ",".join(formatted_value)
|
|
27
|
+
else:
|
|
28
|
+
cell_value = formatted_value
|
|
29
|
+
else:
|
|
30
|
+
if isinstance(value, bool):
|
|
31
|
+
cell_value = "TRUE" if value else "FALSE"
|
|
32
|
+
else:
|
|
33
|
+
cell_value = formatted_value
|
|
34
|
+
return cell_value
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
async def _get_export_row_values(
|
|
38
|
+
cls, model_view: "ModelView", row: Any, column_names: List[str]
|
|
39
|
+
) -> List[Any]:
|
|
40
|
+
row_values = []
|
|
41
|
+
for name in column_names:
|
|
42
|
+
value, formatted_value = await model_view.get_list_value(row, name)
|
|
43
|
+
custom_value = await model_view.custom_export_cell(row, name, value)
|
|
44
|
+
if custom_value is None:
|
|
45
|
+
cell_value = await cls._base_export_cell(
|
|
46
|
+
model_view, name, value, formatted_value
|
|
47
|
+
)
|
|
48
|
+
else:
|
|
49
|
+
cell_value = custom_value
|
|
50
|
+
row_values.append(cell_value)
|
|
51
|
+
return row_values
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
async def pretty_export_csv(
|
|
55
|
+
cls, model_view: "ModelView", rows: List[Any]
|
|
56
|
+
) -> StreamingResponse:
|
|
57
|
+
async def generate(writer: Writer) -> AsyncGenerator[Any, None]:
|
|
58
|
+
column_names = model_view.get_export_columns()
|
|
59
|
+
headers = [
|
|
60
|
+
model_view._column_labels.get(name, name) for name in column_names
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
yield writer.writerow(headers)
|
|
64
|
+
|
|
65
|
+
for row in rows:
|
|
66
|
+
vals = await cls._get_export_row_values(model_view, row, column_names)
|
|
67
|
+
yield writer.writerow(vals)
|
|
68
|
+
|
|
69
|
+
filename = secure_filename(model_view.get_export_name(export_type="csv"))
|
|
70
|
+
|
|
71
|
+
return StreamingResponse(
|
|
72
|
+
content=stream_to_csv(generate),
|
|
73
|
+
media_type="text/csv",
|
|
74
|
+
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
75
|
+
)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<div class="card-header">
|
|
6
6
|
<h3 class="card-title">
|
|
7
7
|
{% for pk in model_view.pk_columns -%}
|
|
8
|
-
{{ pk.name }}
|
|
8
|
+
{{ pk.name | title }}
|
|
9
9
|
{%- if not loop.last %};{% endif -%}
|
|
10
10
|
{% endfor %}: {{ get_object_identifier(model) }}</h3>
|
|
11
11
|
</div>
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
</table>
|
|
49
49
|
</div>
|
|
50
50
|
<div class="card-footer container">
|
|
51
|
-
<div class="row">
|
|
52
|
-
<div class="col-
|
|
51
|
+
<div class="row row-gap-2">
|
|
52
|
+
<div class="col-auto">
|
|
53
53
|
<a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
|
|
54
54
|
Go Back
|
|
55
55
|
</a>
|
|
56
56
|
</div>
|
|
57
57
|
{% if model_view.can_delete %}
|
|
58
|
-
<div class="col-
|
|
58
|
+
<div class="col-auto">
|
|
59
59
|
<a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(model) }}"
|
|
60
60
|
data-url="{{ model_view._url_for_delete(request, model) }}" data-bs-toggle="modal"
|
|
61
61
|
data-bs-target="#modal-delete" class="btn btn-danger">
|
|
@@ -64,14 +64,14 @@
|
|
|
64
64
|
</div>
|
|
65
65
|
{% endif %}
|
|
66
66
|
{% if model_view.can_edit %}
|
|
67
|
-
<div class="col-
|
|
67
|
+
<div class="col-auto">
|
|
68
68
|
<a href="{{ model_view._build_url_for('admin:edit', request, model) }}" class="btn btn-primary">
|
|
69
69
|
Edit
|
|
70
70
|
</a>
|
|
71
71
|
</div>
|
|
72
72
|
{% endif %}
|
|
73
73
|
{% for custom_action,label in model_view._custom_actions_in_detail.items() %}
|
|
74
|
-
<div class="col-
|
|
74
|
+
<div class="col-auto">
|
|
75
75
|
{% if custom_action in model_view._custom_actions_confirmation %}
|
|
76
76
|
<a href="#" class="btn btn-secondary" data-bs-toggle="modal"
|
|
77
77
|
data-bs-target="#modal-confirmation-{{ custom_action }}">
|
|
@@ -103,4 +103,4 @@ url=model_view._url_for_action(request, custom_action) + '?pks=' + (get_object_i
|
|
|
103
103
|
{% endif %}
|
|
104
104
|
{% endfor %}
|
|
105
105
|
|
|
106
|
-
{% endblock %}
|
|
106
|
+
{% endblock %}
|
|
@@ -265,12 +265,23 @@
|
|
|
265
265
|
{% else %}
|
|
266
266
|
<!-- Fallback for other filter types -->
|
|
267
267
|
<div class="mb-3">
|
|
268
|
-
<div class="fw-bold text-truncate">{{ filter.title }}</div>
|
|
268
|
+
<div class="fw-bold text-truncate fs-3 mb-2">{{ filter.title }}</div>
|
|
269
269
|
<div>
|
|
270
270
|
{% for lookup in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %}
|
|
271
|
-
|
|
271
|
+
{% if request.query_params.get(filter.parameter_name) == lookup[0] %}
|
|
272
|
+
<div class="d-flex align-items-center justify-content-between bg-secondary-lt px-2 py-1 rounded">
|
|
273
|
+
<span class="text-truncate fw-bold text-dark">
|
|
274
|
+
{{ lookup[1] }}
|
|
275
|
+
</span>
|
|
276
|
+
<a href="{{ request.url.remove_query_params(filter.parameter_name) }}" class="text-decoration-none ms-2" title="Clear filter">
|
|
277
|
+
<i class="fa-solid fa-times"></i>
|
|
278
|
+
</a>
|
|
279
|
+
</div>
|
|
280
|
+
{% else %}
|
|
281
|
+
<a href="{{ request.url.include_query_params(**{filter.parameter_name: lookup[0]}) }}" class="d-block text-decoration-none text-truncate px-2 py-1">
|
|
272
282
|
{{ lookup[1] }}
|
|
273
283
|
</a>
|
|
284
|
+
{% endif %}
|
|
274
285
|
{% endfor %}
|
|
275
286
|
</div>
|
|
276
287
|
</div>
|
sqladmin/templating.py
CHANGED
|
@@ -45,7 +45,7 @@ class Jinja2Templates:
|
|
|
45
45
|
def __init__(self, directory: str) -> None:
|
|
46
46
|
@jinja2.pass_context
|
|
47
47
|
def url_for(context: dict, __name: str, **path_params: Any) -> URL:
|
|
48
|
-
request = context["request"]
|
|
48
|
+
request: Request = context["request"]
|
|
49
49
|
return request.url_for(__name, **path_params)
|
|
50
50
|
|
|
51
51
|
loader = jinja2.FileSystemLoader(directory)
|
sqladmin/widgets.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
# mypy: disable-error-code="override"
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
|
-
from typing import Any
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
3
5
|
|
|
4
6
|
from markupsafe import Markup
|
|
5
|
-
from wtforms import Field, widgets
|
|
7
|
+
from wtforms import Field, SelectFieldBase, widgets
|
|
6
8
|
from wtforms.widgets import html_params
|
|
7
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from sqladmin.fields import AjaxSelectField
|
|
12
|
+
|
|
8
13
|
__all__ = [
|
|
9
14
|
"AjaxSelect2Widget",
|
|
10
15
|
"DatePickerWidget",
|
|
@@ -38,7 +43,7 @@ class AjaxSelect2Widget(widgets.Select):
|
|
|
38
43
|
self.multiple = multiple
|
|
39
44
|
self.lookup_url = ""
|
|
40
45
|
|
|
41
|
-
def __call__(self, field:
|
|
46
|
+
def __call__(self, field: "AjaxSelectField", **kwargs: Any) -> Markup:
|
|
42
47
|
kwargs.setdefault("data-role", "select2-ajax")
|
|
43
48
|
kwargs.setdefault("data-url", field.loader.model_admin.ajax_lookup_url)
|
|
44
49
|
|
|
@@ -61,11 +66,11 @@ class AjaxSelect2Widget(widgets.Select):
|
|
|
61
66
|
if data:
|
|
62
67
|
kwargs["data-json"] = json.dumps([data])
|
|
63
68
|
|
|
64
|
-
return Markup(f"<select {html_params(name=field.name, **kwargs)}></select>")
|
|
69
|
+
return Markup(f"<select {html_params(name=field.name, **kwargs)}></select>") # nosec: markupsafe_markup_xss
|
|
65
70
|
|
|
66
71
|
|
|
67
72
|
class Select2TagsWidget(widgets.Select):
|
|
68
|
-
def __call__(self, field:
|
|
73
|
+
def __call__(self, field: SelectFieldBase, **kwargs: Any) -> str:
|
|
69
74
|
kwargs.setdefault("data-role", "select2-tags")
|
|
70
75
|
kwargs.setdefault("data-json", json.dumps(field.data))
|
|
71
76
|
kwargs.setdefault("multiple", "multiple")
|
|
@@ -81,20 +86,21 @@ class FileInputWidget(widgets.FileInput):
|
|
|
81
86
|
if not field.flags.required:
|
|
82
87
|
checkbox_id = f"{field.id}_checkbox"
|
|
83
88
|
checkbox_label = Markup(
|
|
84
|
-
|
|
85
|
-
)
|
|
89
|
+
'<label class="form-check-label" for="{}">Clear</label>'
|
|
90
|
+
).format(checkbox_id)
|
|
91
|
+
|
|
86
92
|
checkbox_input = Markup(
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
checkbox = Markup(
|
|
90
|
-
|
|
93
|
+
'<input class="form-check-input" type="checkbox" id="{}" name="{}">' # noqa: E501
|
|
94
|
+
).format(checkbox_id, checkbox_id)
|
|
95
|
+
checkbox = Markup('<div class="form-check">{}{}</div>').format(
|
|
96
|
+
checkbox_input, checkbox_label
|
|
91
97
|
)
|
|
92
98
|
else:
|
|
93
99
|
checkbox = Markup()
|
|
94
100
|
|
|
95
101
|
if field.data:
|
|
96
|
-
current_value = Markup(
|
|
102
|
+
current_value = Markup("<p>Currently: {}</p>").format(field.data)
|
|
97
103
|
field.flags.required = False
|
|
98
104
|
return current_value + checkbox + super().__call__(field, **kwargs)
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
|
|
106
|
+
return super().__call__(field, **kwargs)
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqladmin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.23.0
|
|
4
4
|
Summary: SQLAlchemy admin for FastAPI and Starlette
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Project-URL: Source, https://github.com/aminalaee/sqladmin
|
|
5
|
+
Keywords: sqlalchemy,fastapi,starlette,admin
|
|
6
|
+
Author: Amin Alaee
|
|
8
7
|
Author-email: Amin Alaee <me@aminalaee.dev>
|
|
9
8
|
License-Expression: BSD-3-Clause
|
|
10
|
-
License-File: LICENSE.md
|
|
11
|
-
Keywords: admin,fastapi,sqlalchemy,starlette
|
|
12
9
|
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Environment :: Web Environment
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
10
|
Classifier: Programming Language :: Python
|
|
18
11
|
Classifier: Programming Language :: Python :: 3.9
|
|
19
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -21,15 +14,22 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Environment :: Web Environment
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
24
20
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
-
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Requires-Dist: starlette
|
|
23
|
+
Requires-Dist: wtforms>=3.1,<3.2
|
|
26
24
|
Requires-Dist: jinja2
|
|
27
25
|
Requires-Dist: python-multipart
|
|
28
|
-
Requires-Dist: sqlalchemy>=
|
|
29
|
-
Requires-Dist:
|
|
30
|
-
Requires-
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
27
|
+
Requires-Dist: itsdangerous ; extra == 'full'
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
|
|
30
|
+
Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
|
|
31
|
+
Project-URL: Source, https://github.com/aminalaee/sqladmin
|
|
31
32
|
Provides-Extra: full
|
|
32
|
-
Requires-Dist: itsdangerous; extra == 'full'
|
|
33
33
|
Description-Content-Type: text/markdown
|
|
34
34
|
|
|
35
35
|
<p align="center">
|
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
sqladmin/__init__.py,sha256=
|
|
2
|
-
sqladmin/_menu.py,sha256=
|
|
3
|
-
sqladmin/_queries.py,sha256=
|
|
4
|
-
sqladmin/_types.py,sha256=
|
|
5
|
-
sqladmin/_validators.py,sha256=
|
|
6
|
-
sqladmin/ajax.py,sha256=
|
|
7
|
-
sqladmin/application.py,sha256=
|
|
1
|
+
sqladmin/__init__.py,sha256=x3o2l-V1Oqfo_M7VHWGmtU8JoHYLWBwo7NoTbzBBXwQ,192
|
|
2
|
+
sqladmin/_menu.py,sha256=fDDh1g-t2Ww-n4Zipjw3UmMRRnxK-V9AaBpZOgpbbMs,2607
|
|
3
|
+
sqladmin/_queries.py,sha256=F-CZOtVfbHmynWNp3DEyfbux5EYbk4cCnj35zAYgkqI,9853
|
|
4
|
+
sqladmin/_types.py,sha256=6lF7cuzMaG5ZzbLIxA4-y1nsnax5Kyi-_Tc7oR4UmZA,1524
|
|
5
|
+
sqladmin/_validators.py,sha256=753oc74e9BQWBCZD9sZ-akHoqwjR76oHLRIobtCk99E,1758
|
|
6
|
+
sqladmin/ajax.py,sha256=vgpR6OWYItHPyW21cBi6qaCCgbIHprXhzXdt1OjR9oI,2780
|
|
7
|
+
sqladmin/application.py,sha256=PgShcWqDylJRZT1VIZ63_6t7-hjMvilDPwf5S3szJI0,28394
|
|
8
8
|
sqladmin/authentication.py,sha256=VLNa38rzvQ774c_I1duI5dUVw3-LdcGFyIPafHgXtxg,2665
|
|
9
9
|
sqladmin/exceptions.py,sha256=6-E8m7rbWE3A7hNaSmB6CVqFzkEuwUpmU5AdGbouPCw,154
|
|
10
|
-
sqladmin/fields.py,sha256=
|
|
11
|
-
sqladmin/filters.py,sha256=
|
|
12
|
-
sqladmin/formatters.py,sha256=
|
|
13
|
-
sqladmin/forms.py,sha256=
|
|
14
|
-
sqladmin/helpers.py,sha256=
|
|
15
|
-
sqladmin/models.py,sha256=
|
|
10
|
+
sqladmin/fields.py,sha256=lDnAAsxGOVZ_7ZYdy5Eox8PjntBy9EdnLfPSEgIWKKA,12083
|
|
11
|
+
sqladmin/filters.py,sha256=oplxJkM8XlWsGlyLBkyCki8z3uOBk9WAcRJCKN4Nx7U,11465
|
|
12
|
+
sqladmin/formatters.py,sha256=ywcQU5neKdFSTFdUTpQXSmpDm4ijVl2zSfOUwdaSPhw,488
|
|
13
|
+
sqladmin/forms.py,sha256=2Z1CAKxvFrOnFjyWJsqlkejfh_ncEqI7JNSap-MSi3w,22470
|
|
14
|
+
sqladmin/helpers.py,sha256=FKVIV3SxXfyBP5FCSdnwSGveIsnlKVDpFYSB2ZkRf9A,9029
|
|
15
|
+
sqladmin/models.py,sha256=4cPSgmgtRv1yF_kKlPZSX4kA4udQ6bydet7CPK3sG2o,44765
|
|
16
16
|
sqladmin/pagination.py,sha256=zg_bAvqZd2Rf0wKJ7uiVfNV9vR0hrsilmi9Ak0SOG_U,2600
|
|
17
|
+
sqladmin/pretty_export.py,sha256=83zASMWLsY0a3g_Orj17_1hSK6XRqWorRBsnu1uPxmE,2609
|
|
17
18
|
sqladmin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
sqladmin/templating.py,sha256=o-QMikTrOEgrneLonqCWR3SpAthr-9DoMwOmobM9zq0,2252
|
|
19
|
-
sqladmin/widgets.py,sha256=xl9tGhj--KTRPmNhFn3WVbvN_tQfkYSNVBGicsFhrgM,3189
|
|
20
19
|
sqladmin/statics/css/flatpickr.min.css,sha256=GzSkJVLJbxDk36qko2cnawOGiqz_Y8GsQv_jMTUrx1Q,16166
|
|
21
20
|
sqladmin/statics/css/fontawesome.min.css,sha256=CTSx_A06dm1B063156EVh15m6Y67pAjZZaQc89LLSrU,102217
|
|
22
21
|
sqladmin/statics/css/main.css,sha256=BeLxW6X9i_hoGHiXR-AQsdTRi7QPjBG1S2XUo0iFnS4,133
|
|
@@ -38,17 +37,18 @@ sqladmin/statics/webfonts/tabler-icons.woff2,sha256=26WaCnr1NnLG_uLQ3DMhEkenPiJJ
|
|
|
38
37
|
sqladmin/templates/sqladmin/_macros.html,sha256=79GzkgJAaRbzhjm5WIRFkgl8W0U_5hH4gLteQan1rpo,2983
|
|
39
38
|
sqladmin/templates/sqladmin/base.html,sha256=u6rdmdI6Kg7JteTFmLwh7UhIo2Z1yvZwCNvPnLrHAsg,1783
|
|
40
39
|
sqladmin/templates/sqladmin/create.html,sha256=Vaj_OHLDIqnZF1HOz_g3ogTaCGZqPEfTJhaxezD1wjM,1396
|
|
41
|
-
sqladmin/templates/sqladmin/details.html,sha256=
|
|
40
|
+
sqladmin/templates/sqladmin/details.html,sha256=kh1idWR7Usi6BvQjuhoWoEN8kypW8xRiit_trJGZMsE,4195
|
|
42
41
|
sqladmin/templates/sqladmin/edit.html,sha256=geKD5j8ZLcMSciI80-qg_dQM3Sn_7g3_DeIONmSzQt8,1641
|
|
43
42
|
sqladmin/templates/sqladmin/error.html,sha256=gb-172SMuQKncv0QE8DQdQXeM-fw7oXC0LPLO3ia0IM,290
|
|
44
43
|
sqladmin/templates/sqladmin/index.html,sha256=vh_IhhYmHPOkdZNrXSEc4e9gXXeZ-nsRBCsJQ_mC7YI,71
|
|
45
44
|
sqladmin/templates/sqladmin/layout.html,sha256=iBIhypkXp6O3hAHDdMNc4pWd9yxt5mQy7o2lBQD-6Ec,1994
|
|
46
|
-
sqladmin/templates/sqladmin/list.html,sha256=
|
|
45
|
+
sqladmin/templates/sqladmin/list.html,sha256=U1O5uG8lLUKVDVWe68nErv_tYsyBouz5I8lNIsMM_AA,16581
|
|
47
46
|
sqladmin/templates/sqladmin/login.html,sha256=Y_hlcIapfVFPNbSIbCe4Tbj5DLLD46emkSlL5-RP4iY,1514
|
|
48
47
|
sqladmin/templates/sqladmin/modals/delete.html,sha256=jTuv6geT-AhK5HTgRmntrJ8CEi98-kwKrVDrzkOQWhw,1092
|
|
49
48
|
sqladmin/templates/sqladmin/modals/details_action_confirmation.html,sha256=mN8LJ5OqypxNLAg2_GYZgQmGeK4E6t7JL5RmOEYuliM,1020
|
|
50
49
|
sqladmin/templates/sqladmin/modals/list_action_confirmation.html,sha256=U52LLNmpLaMuUZSVtGK15oLXsEu6m2S3l9zj9sjN6uM,1078
|
|
51
|
-
sqladmin
|
|
52
|
-
sqladmin
|
|
53
|
-
sqladmin-0.
|
|
54
|
-
sqladmin-0.
|
|
50
|
+
sqladmin/templating.py,sha256=GcvfK54U_fypeDMySOWSg32dOCvAp8WfZUFMZqOXb9M,2261
|
|
51
|
+
sqladmin/widgets.py,sha256=YH7QquQkHviviu3GLTF0GG765MCXyk17SP1sXKC_LM0,3401
|
|
52
|
+
sqladmin-0.23.0.dist-info/WHEEL,sha256=5DEXXimM34_d4Gx1AuF9ysMr1_maoEtGKjaILM3s4w4,80
|
|
53
|
+
sqladmin-0.23.0.dist-info/METADATA,sha256=HsBgmbvjKmFLPDCnM5iL0-2qXxFV9-KR6ZK5HkJzRCc,5336
|
|
54
|
+
sqladmin-0.23.0.dist-info/RECORD,,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
Copyright © 2022, Amin Alaee.
|
|
2
|
-
All rights reserved.
|
|
3
|
-
|
|
4
|
-
Redistribution and use in source and binary forms, with or without
|
|
5
|
-
modification, are permitted provided that the following conditions are met:
|
|
6
|
-
|
|
7
|
-
* Redistributions of source code must retain the above copyright notice, this
|
|
8
|
-
list of conditions and the following disclaimer.
|
|
9
|
-
|
|
10
|
-
* Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
-
this list of conditions and the following disclaimer in the documentation
|
|
12
|
-
and/or other materials provided with the distribution.
|
|
13
|
-
|
|
14
|
-
* Neither the name of the copyright holder nor the names of its
|
|
15
|
-
contributors may be used to endorse or promote products derived from
|
|
16
|
-
this software without specific prior written permission.
|
|
17
|
-
|
|
18
|
-
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
-
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
-
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
-
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
-
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
-
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
-
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|