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/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 Integer
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
- else:
15
- return column.key
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, request: Request, model: Any, run_query: Callable[[Select], Any]
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
- elif value == "false":
94
+
95
+ if value == "false":
69
96
  return query.filter(column_obj.is_(False))
70
- else:
71
- return query
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, request: Request, model: Any, run_query: Callable[[Select], Any]
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, request: Request, model: Any, run_query: Callable[[Select], Any]
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, request: Request, model: Any, run_query: Callable[[Select], Any]
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
- assert not isinstance(self.foreign_display_field, str)
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(f"<i class='fa {icon_class}'></i>")
14
+ return Markup("<i class='fa {}'></i>").format(icon_class)
15
15
 
16
16
 
17
17
  BASE_FORMATTERS = {