sqladmin 0.21.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 +36 -6
- sqladmin/_validators.py +14 -8
- sqladmin/ajax.py +2 -2
- sqladmin/application.py +35 -20
- sqladmin/fields.py +29 -20
- sqladmin/filters.py +206 -11
- sqladmin/formatters.py +1 -1
- sqladmin/forms.py +140 -51
- sqladmin/helpers.py +20 -7
- sqladmin/models.py +95 -32
- sqladmin/pretty_export.py +75 -0
- sqladmin/templates/sqladmin/details.html +7 -7
- sqladmin/templates/sqladmin/list.html +62 -13
- sqladmin/templating.py +1 -1
- sqladmin/widgets.py +20 -14
- {sqladmin-0.21.0.dist-info → sqladmin-0.23.0.dist-info}/METADATA +17 -16
- {sqladmin-0.21.0.dist-info → sqladmin-0.23.0.dist-info}/RECORD +21 -21
- {sqladmin-0.21.0.dist-info → sqladmin-0.23.0.dist-info}/WHEEL +1 -1
- sqladmin-0.21.0.dist-info/licenses/LICENSE.md +0 -27
sqladmin/filters.py
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from typing import Any, Callable, List, Optional, Tuple
|
|
3
3
|
|
|
4
|
-
from sqlalchemy import
|
|
4
|
+
from sqlalchemy import (
|
|
5
|
+
BigInteger,
|
|
6
|
+
Float,
|
|
7
|
+
Integer,
|
|
8
|
+
Numeric,
|
|
9
|
+
SmallInteger,
|
|
10
|
+
String,
|
|
11
|
+
Text,
|
|
12
|
+
)
|
|
5
13
|
from sqlalchemy.sql.expression import Select, select
|
|
14
|
+
from sqlalchemy.sql.sqltypes import _Binary
|
|
6
15
|
from starlette.requests import Request
|
|
7
16
|
|
|
8
17
|
from sqladmin._types import MODEL_ATTR
|
|
9
18
|
|
|
19
|
+
# Try to import UUID type for SQLAlchemy 2.0+
|
|
20
|
+
try:
|
|
21
|
+
import uuid
|
|
22
|
+
|
|
23
|
+
from sqlalchemy import Uuid # type: ignore[attr-defined]
|
|
24
|
+
|
|
25
|
+
HAS_UUID_SUPPORT = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
# Fallback for SQLAlchemy < 2.0
|
|
28
|
+
HAS_UUID_SUPPORT = False
|
|
29
|
+
Uuid = None # type: ignore[misc, assignment]
|
|
30
|
+
|
|
10
31
|
|
|
11
32
|
def get_parameter_name(column: MODEL_ATTR) -> str:
|
|
12
33
|
if isinstance(column, str):
|
|
13
34
|
return column
|
|
14
|
-
|
|
15
|
-
|
|
35
|
+
|
|
36
|
+
return column.key
|
|
16
37
|
|
|
17
38
|
|
|
18
39
|
def prettify_attribute_name(name: str) -> str:
|
|
@@ -42,6 +63,8 @@ def get_model_from_column(column: Any) -> Any:
|
|
|
42
63
|
|
|
43
64
|
|
|
44
65
|
class BooleanFilter:
|
|
66
|
+
has_operator = False
|
|
67
|
+
|
|
45
68
|
def __init__(
|
|
46
69
|
self,
|
|
47
70
|
column: MODEL_ATTR,
|
|
@@ -53,7 +76,10 @@ class BooleanFilter:
|
|
|
53
76
|
self.parameter_name = parameter_name or get_parameter_name(column)
|
|
54
77
|
|
|
55
78
|
async def lookups(
|
|
56
|
-
self,
|
|
79
|
+
self,
|
|
80
|
+
request: Request,
|
|
81
|
+
model: Any,
|
|
82
|
+
run_query: Callable[[Select], Any],
|
|
57
83
|
) -> List[Tuple[str, str]]:
|
|
58
84
|
return [
|
|
59
85
|
("all", "All"),
|
|
@@ -65,13 +91,16 @@ class BooleanFilter:
|
|
|
65
91
|
column_obj = get_column_obj(self.column, model)
|
|
66
92
|
if value == "true":
|
|
67
93
|
return query.filter(column_obj.is_(True))
|
|
68
|
-
|
|
94
|
+
|
|
95
|
+
if value == "false":
|
|
69
96
|
return query.filter(column_obj.is_(False))
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
|
|
98
|
+
return query
|
|
72
99
|
|
|
73
100
|
|
|
74
101
|
class AllUniqueStringValuesFilter:
|
|
102
|
+
has_operator = False
|
|
103
|
+
|
|
75
104
|
def __init__(
|
|
76
105
|
self,
|
|
77
106
|
column: MODEL_ATTR,
|
|
@@ -83,7 +112,10 @@ class AllUniqueStringValuesFilter:
|
|
|
83
112
|
self.parameter_name = parameter_name or get_parameter_name(column)
|
|
84
113
|
|
|
85
114
|
async def lookups(
|
|
86
|
-
self,
|
|
115
|
+
self,
|
|
116
|
+
request: Request,
|
|
117
|
+
model: Any,
|
|
118
|
+
run_query: Callable[[Select], Any],
|
|
87
119
|
) -> List[Tuple[str, str]]:
|
|
88
120
|
column_obj = get_column_obj(self.column, model)
|
|
89
121
|
|
|
@@ -101,6 +133,8 @@ class AllUniqueStringValuesFilter:
|
|
|
101
133
|
|
|
102
134
|
|
|
103
135
|
class StaticValuesFilter:
|
|
136
|
+
has_operator = False
|
|
137
|
+
|
|
104
138
|
def __init__(
|
|
105
139
|
self,
|
|
106
140
|
column: MODEL_ATTR,
|
|
@@ -114,7 +148,10 @@ class StaticValuesFilter:
|
|
|
114
148
|
self.values = values
|
|
115
149
|
|
|
116
150
|
async def lookups(
|
|
117
|
-
self,
|
|
151
|
+
self,
|
|
152
|
+
request: Request,
|
|
153
|
+
model: Any,
|
|
154
|
+
run_query: Callable[[Select], Any],
|
|
118
155
|
) -> List[Tuple[str, str]]:
|
|
119
156
|
return [("", "All")] + self.values
|
|
120
157
|
|
|
@@ -126,6 +163,8 @@ class StaticValuesFilter:
|
|
|
126
163
|
|
|
127
164
|
|
|
128
165
|
class ForeignKeyFilter:
|
|
166
|
+
has_operator = False
|
|
167
|
+
|
|
129
168
|
def __init__(
|
|
130
169
|
self,
|
|
131
170
|
foreign_key: MODEL_ATTR,
|
|
@@ -141,13 +180,18 @@ class ForeignKeyFilter:
|
|
|
141
180
|
self.parameter_name = parameter_name or get_parameter_name(foreign_key)
|
|
142
181
|
|
|
143
182
|
async def lookups(
|
|
144
|
-
self,
|
|
183
|
+
self,
|
|
184
|
+
request: Request,
|
|
185
|
+
model: Any,
|
|
186
|
+
run_query: Callable[[Select], Any],
|
|
145
187
|
) -> List[Tuple[str, str]]:
|
|
146
188
|
foreign_key_obj = get_column_obj(self.foreign_key, model)
|
|
147
189
|
if self.foreign_model is None and isinstance(self.foreign_display_field, str):
|
|
148
190
|
raise ValueError("foreign_model is required for string foreign key filters")
|
|
149
191
|
if self.foreign_model is None:
|
|
150
|
-
|
|
192
|
+
if isinstance(self.foreign_display_field, str):
|
|
193
|
+
raise ValueError("foreign_model should not be string")
|
|
194
|
+
|
|
151
195
|
foreign_display_field_obj = self.foreign_display_field
|
|
152
196
|
else:
|
|
153
197
|
foreign_display_field_obj = get_column_obj(
|
|
@@ -172,3 +216,154 @@ class ForeignKeyFilter:
|
|
|
172
216
|
value = int(value)
|
|
173
217
|
|
|
174
218
|
return query.filter(foreign_key_obj == value)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class OperationColumnFilter:
|
|
222
|
+
"""Universal filter that provides appropriate filter types based on column type"""
|
|
223
|
+
|
|
224
|
+
has_operator = True
|
|
225
|
+
|
|
226
|
+
def __init__(
|
|
227
|
+
self,
|
|
228
|
+
column: MODEL_ATTR,
|
|
229
|
+
title: Optional[str] = None,
|
|
230
|
+
parameter_name: Optional[str] = None,
|
|
231
|
+
):
|
|
232
|
+
self.column = column
|
|
233
|
+
self.title = title or get_title(column)
|
|
234
|
+
self.parameter_name = parameter_name or get_parameter_name(column)
|
|
235
|
+
|
|
236
|
+
def get_operation_options(self, column_obj: Any) -> List[Tuple[str, str]]:
|
|
237
|
+
"""Return operation options based on column type"""
|
|
238
|
+
if self._is_string_type(column_obj):
|
|
239
|
+
return [
|
|
240
|
+
("contains", "Contains"),
|
|
241
|
+
("equals", "Equals"),
|
|
242
|
+
("starts_with", "Starts with"),
|
|
243
|
+
("ends_with", "Ends with"),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
if self._is_numeric_type(column_obj):
|
|
247
|
+
return [
|
|
248
|
+
("equals", "Equals"),
|
|
249
|
+
("greater_than", "Greater than"),
|
|
250
|
+
("less_than", "Less than"),
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
if self._is_uuid_type(column_obj):
|
|
254
|
+
return [
|
|
255
|
+
("equals", "Equals"),
|
|
256
|
+
("contains", "Contains"),
|
|
257
|
+
("starts_with", "Starts with"),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
return [
|
|
261
|
+
("equals", "Equals"),
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
def get_operation_options_for_model(self, model: Any) -> List[Tuple[str, str]]:
|
|
265
|
+
"""Return operation options based on column type for given model"""
|
|
266
|
+
column_obj = get_column_obj(self.column, model)
|
|
267
|
+
return self.get_operation_options(column_obj)
|
|
268
|
+
|
|
269
|
+
def _is_string_type(self, column_obj: Any) -> bool:
|
|
270
|
+
return isinstance(column_obj.type, (String, Text, _Binary))
|
|
271
|
+
|
|
272
|
+
def _is_numeric_type(self, column_obj: Any) -> bool:
|
|
273
|
+
return isinstance(
|
|
274
|
+
column_obj.type, (Integer, Numeric, Float, BigInteger, SmallInteger)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def _is_uuid_type(self, column_obj: Any) -> bool:
|
|
278
|
+
# Check if UUID support is available and column is UUID type
|
|
279
|
+
return HAS_UUID_SUPPORT and isinstance(column_obj.type, Uuid)
|
|
280
|
+
|
|
281
|
+
def _convert_value_for_column(
|
|
282
|
+
self, value: str, column_obj: Any, operation: str = "equals"
|
|
283
|
+
) -> Any:
|
|
284
|
+
if not value:
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
column_type = column_obj.type
|
|
288
|
+
|
|
289
|
+
converters = [
|
|
290
|
+
((String, Text, _Binary), str),
|
|
291
|
+
((Integer, BigInteger, SmallInteger), int),
|
|
292
|
+
((Numeric, Float), float),
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
for types, converter in converters:
|
|
297
|
+
if isinstance(column_type, types):
|
|
298
|
+
return converter(value)
|
|
299
|
+
|
|
300
|
+
if HAS_UUID_SUPPORT and isinstance(column_type, Uuid):
|
|
301
|
+
return (
|
|
302
|
+
str(value.strip())
|
|
303
|
+
if operation in ("contains", "starts_with")
|
|
304
|
+
else uuid.UUID(value.strip())
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
except (ValueError, TypeError):
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
return value
|
|
311
|
+
|
|
312
|
+
async def lookups(
|
|
313
|
+
self,
|
|
314
|
+
request: Request,
|
|
315
|
+
model: Any,
|
|
316
|
+
run_query: Callable[[Select], Any],
|
|
317
|
+
) -> List[Tuple[str, str]]:
|
|
318
|
+
# This method is not used for has_operator=True filters
|
|
319
|
+
# The UI uses get_operation_options_for_model instead
|
|
320
|
+
return []
|
|
321
|
+
|
|
322
|
+
async def get_filtered_query(
|
|
323
|
+
self,
|
|
324
|
+
query: Select,
|
|
325
|
+
operation: str,
|
|
326
|
+
value: Any,
|
|
327
|
+
model: Any,
|
|
328
|
+
) -> Select:
|
|
329
|
+
"""Handle filtering with separate operation and value parameters"""
|
|
330
|
+
if not value or value == "" or not operation:
|
|
331
|
+
return query
|
|
332
|
+
|
|
333
|
+
column_obj = get_column_obj(self.column, model)
|
|
334
|
+
converted_value = self._convert_value_for_column(
|
|
335
|
+
str(value).strip(),
|
|
336
|
+
column_obj,
|
|
337
|
+
operation,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if converted_value is None:
|
|
341
|
+
return query
|
|
342
|
+
|
|
343
|
+
if operation == "contains":
|
|
344
|
+
if self._is_uuid_type(column_obj):
|
|
345
|
+
# For UUID, cast to text for LIKE operations
|
|
346
|
+
search_value = f"%{str(value).strip()}%"
|
|
347
|
+
return query.filter(column_obj.cast(String).ilike(search_value))
|
|
348
|
+
|
|
349
|
+
return query.filter(column_obj.ilike(f"%{str(value).strip()}%"))
|
|
350
|
+
|
|
351
|
+
if operation == "equals":
|
|
352
|
+
return query.filter(column_obj == converted_value)
|
|
353
|
+
|
|
354
|
+
if operation == "starts_with":
|
|
355
|
+
if self._is_uuid_type(column_obj):
|
|
356
|
+
# For UUID, cast to text for LIKE operations
|
|
357
|
+
search_value = f"{str(value).strip()}%"
|
|
358
|
+
return query.filter(column_obj.cast(String).ilike(search_value))
|
|
359
|
+
|
|
360
|
+
return query.filter(column_obj.startswith(str(value).strip()))
|
|
361
|
+
|
|
362
|
+
if operation == "ends_with":
|
|
363
|
+
return query.filter(column_obj.endswith(str(value).strip()))
|
|
364
|
+
if operation == "greater_than":
|
|
365
|
+
return query.filter(column_obj > converted_value)
|
|
366
|
+
if operation == "less_than":
|
|
367
|
+
return query.filter(column_obj < converted_value)
|
|
368
|
+
|
|
369
|
+
return query
|
sqladmin/formatters.py
CHANGED
|
@@ -11,7 +11,7 @@ def empty_formatter(value: Any) -> str:
|
|
|
11
11
|
def bool_formatter(value: bool) -> Markup:
|
|
12
12
|
"""Return check icon if value is `True` or X otherwise."""
|
|
13
13
|
icon_class = "fa-check text-success" if value else "fa-times text-danger"
|
|
14
|
-
return Markup(
|
|
14
|
+
return Markup("<i class='fa {}'></i>").format(icon_class)
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
BASE_FORMATTERS = {
|