sqladmin 0.8.0__py3-none-any.whl → 0.10.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/_queries.py +18 -9
- sqladmin/_types.py +1 -1
- sqladmin/_validators.py +24 -0
- sqladmin/ajax.py +1 -1
- sqladmin/application.py +46 -7
- sqladmin/authentication.py +10 -6
- sqladmin/fields.py +52 -5
- sqladmin/forms.py +61 -16
- sqladmin/helpers.py +99 -16
- sqladmin/models.py +119 -118
- sqladmin/statics/js/main.js +14 -0
- sqladmin/templates/create.html +1 -1
- sqladmin/templates/details.html +6 -6
- sqladmin/templates/edit.html +1 -1
- sqladmin/templates/list.html +10 -10
- sqladmin/widgets.py +30 -1
- {sqladmin-0.8.0.dist-info → sqladmin-0.10.0.dist-info}/METADATA +9 -3
- {sqladmin-0.8.0.dist-info → sqladmin-0.10.0.dist-info}/RECORD +21 -21
- {sqladmin-0.8.0.dist-info → sqladmin-0.10.0.dist-info}/WHEEL +1 -1
- {sqladmin-0.8.0.dist-info → sqladmin-0.10.0.dist-info}/licenses/LICENSE.md +0 -0
sqladmin/__init__.py
CHANGED
sqladmin/_queries.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from re import L
|
|
2
1
|
from typing import TYPE_CHECKING, Any, Dict, List
|
|
3
2
|
|
|
4
3
|
import anyio
|
|
@@ -7,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
7
6
|
from sqlalchemy.orm import Session, joinedload
|
|
8
7
|
from sqlalchemy.sql.expression import Select
|
|
9
8
|
|
|
10
|
-
from sqladmin._types import
|
|
9
|
+
from sqladmin._types import MODEL_PROPERTY
|
|
11
10
|
from sqladmin.helpers import get_column_python_type, get_direction, get_primary_key
|
|
12
11
|
|
|
13
12
|
if TYPE_CHECKING:
|
|
@@ -18,7 +17,7 @@ class Query:
|
|
|
18
17
|
def __init__(self, model_view: "ModelView") -> None:
|
|
19
18
|
self.model_view = model_view
|
|
20
19
|
|
|
21
|
-
def _get_to_many_stmt(self, relation:
|
|
20
|
+
def _get_to_many_stmt(self, relation: MODEL_PROPERTY, values: List[Any]) -> Select:
|
|
22
21
|
target = relation.mapper.class_
|
|
23
22
|
target_pk = get_primary_key(target)
|
|
24
23
|
target_pk_type = get_column_python_type(target_pk)
|
|
@@ -26,14 +25,14 @@ class Query:
|
|
|
26
25
|
related_stmt = select(target).where(target_pk.in_(pk_values))
|
|
27
26
|
return related_stmt
|
|
28
27
|
|
|
29
|
-
def _get_to_one_stmt(self, relation:
|
|
28
|
+
def _get_to_one_stmt(self, relation: MODEL_PROPERTY, value: Any) -> Select:
|
|
30
29
|
target = relation.mapper.class_
|
|
31
30
|
target_pk = get_primary_key(target)
|
|
32
31
|
target_pk_type = get_column_python_type(target_pk)
|
|
33
32
|
related_stmt = select(target).where(target_pk == target_pk_type(value))
|
|
34
33
|
return related_stmt
|
|
35
34
|
|
|
36
|
-
def _set_many_to_one(self, obj: Any, relation:
|
|
35
|
+
def _set_many_to_one(self, obj: Any, relation: MODEL_PROPERTY, value: Any) -> Any:
|
|
37
36
|
fk = relation.local_remote_pairs[0][0]
|
|
38
37
|
fk_type = get_column_python_type(fk)
|
|
39
38
|
setattr(obj, fk.name, fk_type(value))
|
|
@@ -46,7 +45,12 @@ class Query:
|
|
|
46
45
|
|
|
47
46
|
if not value:
|
|
48
47
|
# Set falsy values to None, if column is Nullable
|
|
49
|
-
if
|
|
48
|
+
if (
|
|
49
|
+
not relation
|
|
50
|
+
and column.nullable
|
|
51
|
+
and isinstance(value, bool)
|
|
52
|
+
and value is not False
|
|
53
|
+
):
|
|
50
54
|
value = None
|
|
51
55
|
|
|
52
56
|
setattr(obj, key, value)
|
|
@@ -79,7 +83,12 @@ class Query:
|
|
|
79
83
|
|
|
80
84
|
if not value:
|
|
81
85
|
# Set falsy values to None, if column is Nullable
|
|
82
|
-
if
|
|
86
|
+
if (
|
|
87
|
+
not relation
|
|
88
|
+
and column.nullable
|
|
89
|
+
and isinstance(value, bool)
|
|
90
|
+
and value is not False
|
|
91
|
+
):
|
|
83
92
|
value = None
|
|
84
93
|
|
|
85
94
|
setattr(obj, key, value)
|
|
@@ -119,8 +128,8 @@ class Query:
|
|
|
119
128
|
pk = get_column_python_type(self.model_view.pk_column)(pk)
|
|
120
129
|
stmt = select(self.model_view.model).where(self.model_view.pk_column == pk)
|
|
121
130
|
|
|
122
|
-
for relation in self.model_view.
|
|
123
|
-
stmt = stmt.options(joinedload(relation
|
|
131
|
+
for relation in self.model_view._relation_attrs:
|
|
132
|
+
stmt = stmt.options(joinedload(relation))
|
|
124
133
|
|
|
125
134
|
async with self.model_view.sessionmaker(expire_on_commit=False) as session:
|
|
126
135
|
result = await session.execute(stmt)
|
sqladmin/_types.py
CHANGED
|
@@ -4,5 +4,5 @@ from sqlalchemy.engine import Engine
|
|
|
4
4
|
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
5
5
|
from sqlalchemy.orm import ColumnProperty, RelationshipProperty
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
MODEL_PROPERTY = Union[ColumnProperty, RelationshipProperty]
|
|
8
8
|
ENGINE_TYPE = Union[Engine, AsyncEngine]
|
sqladmin/_validators.py
CHANGED
|
@@ -15,6 +15,30 @@ class CurrencyValidator:
|
|
|
15
15
|
raise ValidationError("Not a valid ISO currency code (e.g. USD, EUR, CNY).")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class PhoneNumberValidator:
|
|
19
|
+
"""Form validator for sqlalchemy_utils PhoneNumberType."""
|
|
20
|
+
|
|
21
|
+
def __call__(self, form: Form, field: Field) -> None:
|
|
22
|
+
from sqlalchemy_utils import PhoneNumber, PhoneNumberParseException
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
PhoneNumber(field.data)
|
|
26
|
+
except PhoneNumberParseException:
|
|
27
|
+
raise ValidationError("Not a valid phone number.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ColorValidator:
|
|
31
|
+
"""General Color validator using `colour` package."""
|
|
32
|
+
|
|
33
|
+
def __call__(self, form: Form, field: Field) -> None:
|
|
34
|
+
from colour import Color
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
Color(field.data)
|
|
38
|
+
except ValueError:
|
|
39
|
+
raise ValidationError('Not a valid color (e.g. "red", "#f00", "#ff0000").')
|
|
40
|
+
|
|
41
|
+
|
|
18
42
|
class TimezoneValidator:
|
|
19
43
|
"""Form validator for sqlalchemy_utils TimezoneType."""
|
|
20
44
|
|
sqladmin/ajax.py
CHANGED
sqladmin/application.py
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
Callable,
|
|
5
|
+
Dict,
|
|
6
|
+
List,
|
|
7
|
+
Optional,
|
|
8
|
+
Sequence,
|
|
9
|
+
Type,
|
|
10
|
+
Union,
|
|
11
|
+
no_type_check,
|
|
12
|
+
)
|
|
3
13
|
|
|
4
14
|
from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader
|
|
5
15
|
from sqlalchemy.engine import Engine
|
|
6
16
|
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
|
|
7
17
|
from sqlalchemy.orm import Session, sessionmaker
|
|
8
18
|
from starlette.applications import Starlette
|
|
9
|
-
from starlette.datastructures import FormData
|
|
19
|
+
from starlette.datastructures import URL, FormData, UploadFile
|
|
10
20
|
from starlette.exceptions import HTTPException
|
|
11
21
|
from starlette.middleware import Middleware
|
|
12
22
|
from starlette.requests import Request
|
|
@@ -404,7 +414,7 @@ class Admin(BaseAdminView):
|
|
|
404
414
|
|
|
405
415
|
await model_view.delete_model(model)
|
|
406
416
|
|
|
407
|
-
return Response(content=request.url_for("admin:list", identity=identity))
|
|
417
|
+
return Response(content=str(request.url_for("admin:list", identity=identity)))
|
|
408
418
|
|
|
409
419
|
@login_required
|
|
410
420
|
async def create(self, request: Request) -> Response:
|
|
@@ -416,7 +426,7 @@ class Admin(BaseAdminView):
|
|
|
416
426
|
model_view = self._find_model_view(identity)
|
|
417
427
|
|
|
418
428
|
Form = await model_view.scaffold_form()
|
|
419
|
-
form_data = await
|
|
429
|
+
form_data = await self._handle_form_data(request)
|
|
420
430
|
form = Form(form_data)
|
|
421
431
|
|
|
422
432
|
context = {
|
|
@@ -472,7 +482,7 @@ class Admin(BaseAdminView):
|
|
|
472
482
|
if request.method == "GET":
|
|
473
483
|
return self.templates.TemplateResponse(model_view.edit_template, context)
|
|
474
484
|
|
|
475
|
-
form_data = await
|
|
485
|
+
form_data = await self._handle_form_data(request, model)
|
|
476
486
|
form = Form(form_data)
|
|
477
487
|
if not form.validate():
|
|
478
488
|
context["form"] = form
|
|
@@ -559,8 +569,11 @@ class Admin(BaseAdminView):
|
|
|
559
569
|
|
|
560
570
|
def get_save_redirect_url(
|
|
561
571
|
self, request: Request, form: FormData, model_view: ModelView, obj: Any
|
|
562
|
-
) -> str:
|
|
563
|
-
"""
|
|
572
|
+
) -> Union[str, URL]:
|
|
573
|
+
"""
|
|
574
|
+
Get the redirect URL after a save action
|
|
575
|
+
which is triggered from create/edit page.
|
|
576
|
+
"""
|
|
564
577
|
|
|
565
578
|
identity = request.path_params["identity"]
|
|
566
579
|
pk = getattr(obj, model_view.pk_column.name)
|
|
@@ -573,6 +586,32 @@ class Admin(BaseAdminView):
|
|
|
573
586
|
return request.url_for("admin:edit", identity=identity, pk=pk)
|
|
574
587
|
return request.url_for("admin:create", identity=identity)
|
|
575
588
|
|
|
589
|
+
async def _handle_form_data(self, request: Request, obj: Any = None) -> FormData:
|
|
590
|
+
"""
|
|
591
|
+
Handle form data and modify in case of UplaodFile.
|
|
592
|
+
This is needed since in edit page
|
|
593
|
+
there's no way to show current file of object.
|
|
594
|
+
"""
|
|
595
|
+
|
|
596
|
+
form = await request.form()
|
|
597
|
+
form_data: Dict[str, Any] = {}
|
|
598
|
+
|
|
599
|
+
for key, value in form.items():
|
|
600
|
+
if not isinstance(value, UploadFile):
|
|
601
|
+
form_data[key] = value
|
|
602
|
+
continue
|
|
603
|
+
|
|
604
|
+
should_clear = form.get(key + "_checkbox")
|
|
605
|
+
empty_upload = len(await value.read(1)) != 1
|
|
606
|
+
if should_clear:
|
|
607
|
+
form_data[key] = None
|
|
608
|
+
elif empty_upload and getattr(obj, key):
|
|
609
|
+
f = getattr(obj, key) # In case of update, imitate UploadFile
|
|
610
|
+
form_data[key] = UploadFile(filename=f.name, file=f.open())
|
|
611
|
+
else:
|
|
612
|
+
form_data[key] = value
|
|
613
|
+
return FormData(form_data)
|
|
614
|
+
|
|
576
615
|
|
|
577
616
|
def expose(
|
|
578
617
|
path: str,
|
sqladmin/authentication.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import functools
|
|
2
|
-
from typing import Any, Callable
|
|
2
|
+
from typing import Any, Callable, Optional
|
|
3
3
|
|
|
4
4
|
from starlette.middleware import Middleware
|
|
5
5
|
from starlette.middleware.sessions import SessionMiddleware
|
|
6
6
|
from starlette.requests import Request
|
|
7
|
-
from starlette.responses import
|
|
7
|
+
from starlette.responses import Response
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class AuthenticationBackend:
|
|
@@ -31,10 +31,14 @@ class AuthenticationBackend:
|
|
|
31
31
|
"""
|
|
32
32
|
raise NotImplementedError()
|
|
33
33
|
|
|
34
|
-
async def authenticate(self, request: Request) ->
|
|
34
|
+
async def authenticate(self, request: Request) -> Optional[Response]:
|
|
35
35
|
"""Implement authenticate logic here.
|
|
36
36
|
This method will be called for each incoming request
|
|
37
37
|
to validate the authentication.
|
|
38
|
+
|
|
39
|
+
If the request is authenticated, this method should return `None` or do nothing.
|
|
40
|
+
Otherwise it should return a `Response` object,
|
|
41
|
+
like a redirect to the login page or SSO page.
|
|
38
42
|
"""
|
|
39
43
|
raise NotImplementedError()
|
|
40
44
|
|
|
@@ -49,9 +53,9 @@ def login_required(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
49
53
|
admin, request = args[0], args[1]
|
|
50
54
|
auth_backend = admin.authentication_backend
|
|
51
55
|
if auth_backend is not None:
|
|
52
|
-
|
|
53
|
-
if
|
|
54
|
-
return
|
|
56
|
+
response = await auth_backend.authenticate(request)
|
|
57
|
+
if response and isinstance(response, Response):
|
|
58
|
+
return response
|
|
55
59
|
|
|
56
60
|
return await func(*args, **kwargs)
|
|
57
61
|
|
sqladmin/fields.py
CHANGED
|
@@ -3,16 +3,18 @@ import operator
|
|
|
3
3
|
from typing import Any, Callable, Generator, List, Optional, Set, Tuple, Union
|
|
4
4
|
|
|
5
5
|
from sqlalchemy import inspect
|
|
6
|
-
from wtforms import Form,
|
|
6
|
+
from wtforms import Form, ValidationError, fields, widgets
|
|
7
7
|
|
|
8
8
|
from sqladmin import widgets as sqladmin_widgets
|
|
9
9
|
from sqladmin.ajax import QueryAjaxModelLoader
|
|
10
|
+
from sqladmin.helpers import parse_interval
|
|
10
11
|
|
|
11
12
|
__all__ = [
|
|
12
13
|
"AjaxSelectField",
|
|
13
14
|
"AjaxSelectMultipleField",
|
|
14
15
|
"DateField",
|
|
15
16
|
"DateTimeField",
|
|
17
|
+
"IntervalField",
|
|
16
18
|
"JSONField",
|
|
17
19
|
"QuerySelectField",
|
|
18
20
|
"QuerySelectMultipleField",
|
|
@@ -46,6 +48,22 @@ class TimeField(fields.TimeField):
|
|
|
46
48
|
widget = sqladmin_widgets.TimePickerWidget()
|
|
47
49
|
|
|
48
50
|
|
|
51
|
+
class IntervalField(fields.StringField):
|
|
52
|
+
"""
|
|
53
|
+
A text field which stores a `datetime.timedelta` object.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def process_formdata(self, valuelist: List[str]) -> None:
|
|
57
|
+
if not valuelist:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
interval = parse_interval(valuelist[0])
|
|
61
|
+
if not interval:
|
|
62
|
+
raise ValueError("Invalide timedelta format.")
|
|
63
|
+
|
|
64
|
+
self.data = interval
|
|
65
|
+
|
|
66
|
+
|
|
49
67
|
class SelectField(fields.SelectField):
|
|
50
68
|
def __init__(
|
|
51
69
|
self,
|
|
@@ -166,7 +184,11 @@ class QuerySelectField(fields.SelectFieldBase):
|
|
|
166
184
|
yield ("__None", self.blank_text, self.data is None)
|
|
167
185
|
|
|
168
186
|
if self.data:
|
|
169
|
-
primary_key =
|
|
187
|
+
primary_key = (
|
|
188
|
+
self.data
|
|
189
|
+
if isinstance(self.data, str)
|
|
190
|
+
else str(inspect(self.data).identity[0])
|
|
191
|
+
)
|
|
170
192
|
else:
|
|
171
193
|
primary_key = None
|
|
172
194
|
|
|
@@ -251,7 +273,11 @@ class QuerySelectMultipleField(QuerySelectField):
|
|
|
251
273
|
|
|
252
274
|
def iter_choices(self) -> Generator[Tuple[str, Any, bool], None, None]:
|
|
253
275
|
if self.data is not None:
|
|
254
|
-
primary_keys =
|
|
276
|
+
primary_keys = (
|
|
277
|
+
self.data
|
|
278
|
+
if all(isinstance(d, str) for d in self.data)
|
|
279
|
+
else [str(inspect(m).identity[0]) for m in self.data]
|
|
280
|
+
)
|
|
255
281
|
for pk, label in self._select_data:
|
|
256
282
|
yield (pk, self.get_label(label), pk in primary_keys)
|
|
257
283
|
|
|
@@ -268,7 +294,7 @@ class QuerySelectMultipleField(QuerySelectField):
|
|
|
268
294
|
raise ValidationError(self.gettext("Not a valid choice"))
|
|
269
295
|
|
|
270
296
|
|
|
271
|
-
class AjaxSelectField(SelectFieldBase):
|
|
297
|
+
class AjaxSelectField(fields.SelectFieldBase):
|
|
272
298
|
widget = sqladmin_widgets.AjaxSelect2Widget()
|
|
273
299
|
separator = ","
|
|
274
300
|
|
|
@@ -310,7 +336,7 @@ class AjaxSelectField(SelectFieldBase):
|
|
|
310
336
|
raise ValidationError("Not a valid choice")
|
|
311
337
|
|
|
312
338
|
|
|
313
|
-
class AjaxSelectMultipleField(SelectFieldBase):
|
|
339
|
+
class AjaxSelectMultipleField(fields.SelectFieldBase):
|
|
314
340
|
widget = sqladmin_widgets.AjaxSelect2Widget(multiple=True)
|
|
315
341
|
separator = ","
|
|
316
342
|
|
|
@@ -349,3 +375,24 @@ class AjaxSelectMultipleField(SelectFieldBase):
|
|
|
349
375
|
for field in valuelist:
|
|
350
376
|
for n in field.split(self.separator):
|
|
351
377
|
self._formdata.add(n)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class Select2TagsField(fields.SelectField):
|
|
381
|
+
widget = sqladmin_widgets.Select2TagsWidget()
|
|
382
|
+
|
|
383
|
+
def pre_validate(self, form: Form) -> None:
|
|
384
|
+
...
|
|
385
|
+
|
|
386
|
+
def process_formdata(self, valuelist: list) -> None:
|
|
387
|
+
self.data = valuelist
|
|
388
|
+
|
|
389
|
+
def process_data(self, value: Optional[list]) -> None:
|
|
390
|
+
self.data = value or []
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class FileField(fields.FileField):
|
|
394
|
+
"""
|
|
395
|
+
File field which is clearable.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
widget = sqladmin_widgets.FileInputWidget()
|
sqladmin/forms.py
CHANGED
|
@@ -33,8 +33,13 @@ from wtforms import (
|
|
|
33
33
|
)
|
|
34
34
|
from wtforms.fields.core import UnboundField
|
|
35
35
|
|
|
36
|
-
from sqladmin._types import ENGINE_TYPE,
|
|
37
|
-
from sqladmin._validators import
|
|
36
|
+
from sqladmin._types import ENGINE_TYPE, MODEL_PROPERTY
|
|
37
|
+
from sqladmin._validators import (
|
|
38
|
+
ColorValidator,
|
|
39
|
+
CurrencyValidator,
|
|
40
|
+
PhoneNumberValidator,
|
|
41
|
+
TimezoneValidator,
|
|
42
|
+
)
|
|
38
43
|
from sqladmin.ajax import QueryAjaxModelLoader
|
|
39
44
|
from sqladmin.exceptions import NoConverterFound
|
|
40
45
|
from sqladmin.fields import (
|
|
@@ -42,9 +47,12 @@ from sqladmin.fields import (
|
|
|
42
47
|
AjaxSelectMultipleField,
|
|
43
48
|
DateField,
|
|
44
49
|
DateTimeField,
|
|
50
|
+
FileField,
|
|
51
|
+
IntervalField,
|
|
45
52
|
JSONField,
|
|
46
53
|
QuerySelectField,
|
|
47
54
|
QuerySelectMultipleField,
|
|
55
|
+
Select2TagsField,
|
|
48
56
|
SelectField,
|
|
49
57
|
TimeField,
|
|
50
58
|
)
|
|
@@ -68,7 +76,7 @@ class ConverterCallable(Protocol):
|
|
|
68
76
|
def __call__(
|
|
69
77
|
self,
|
|
70
78
|
model: type,
|
|
71
|
-
prop:
|
|
79
|
+
prop: MODEL_PROPERTY,
|
|
72
80
|
kwargs: Dict[str, Any],
|
|
73
81
|
) -> UnboundField:
|
|
74
82
|
... # pragma: no cover
|
|
@@ -106,7 +114,7 @@ class ModelConverterBase:
|
|
|
106
114
|
|
|
107
115
|
async def _prepare_kwargs(
|
|
108
116
|
self,
|
|
109
|
-
prop:
|
|
117
|
+
prop: MODEL_PROPERTY,
|
|
110
118
|
engine: ENGINE_TYPE,
|
|
111
119
|
field_args: Dict[str, Any],
|
|
112
120
|
field_widget_args: Dict[str, Any],
|
|
@@ -114,6 +122,9 @@ class ModelConverterBase:
|
|
|
114
122
|
label: Optional[str] = None,
|
|
115
123
|
loader: Optional[QueryAjaxModelLoader] = None,
|
|
116
124
|
) -> Optional[Dict[str, Any]]:
|
|
125
|
+
if not isinstance(prop, (RelationshipProperty, ColumnProperty)):
|
|
126
|
+
return None
|
|
127
|
+
|
|
117
128
|
kwargs: Union[dict, None]
|
|
118
129
|
kwargs = field_args.copy()
|
|
119
130
|
widget_args = field_widget_args.copy()
|
|
@@ -222,7 +233,7 @@ class ModelConverterBase:
|
|
|
222
233
|
|
|
223
234
|
return [] # pragma: nocover
|
|
224
235
|
|
|
225
|
-
def get_converter(self, prop:
|
|
236
|
+
def get_converter(self, prop: MODEL_PROPERTY) -> ConverterCallable:
|
|
226
237
|
if isinstance(prop, RelationshipProperty):
|
|
227
238
|
direction = get_direction(prop)
|
|
228
239
|
return self._converters[direction]
|
|
@@ -259,7 +270,7 @@ class ModelConverterBase:
|
|
|
259
270
|
async def convert(
|
|
260
271
|
self,
|
|
261
272
|
model: type,
|
|
262
|
-
prop:
|
|
273
|
+
prop: MODEL_PROPERTY,
|
|
263
274
|
engine: ENGINE_TYPE,
|
|
264
275
|
field_args: Dict[str, Any],
|
|
265
276
|
field_widget_args: Dict[str, Any],
|
|
@@ -405,49 +416,60 @@ class ModelConverter(ModelConverterBase):
|
|
|
405
416
|
) -> UnboundField:
|
|
406
417
|
return JSONField(**kwargs)
|
|
407
418
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
419
|
+
@converts("Interval")
|
|
420
|
+
def conv_interval(
|
|
421
|
+
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
422
|
+
) -> UnboundField:
|
|
423
|
+
kwargs["render_kw"]["placeholder"] = "Like: 1 day 1:25:33.652"
|
|
424
|
+
return IntervalField(**kwargs)
|
|
412
425
|
|
|
413
426
|
@converts(
|
|
414
427
|
"sqlalchemy.dialects.postgresql.base.INET",
|
|
428
|
+
"sqlalchemy.dialects.postgresql.types.INET",
|
|
415
429
|
"sqlalchemy_utils.types.ip_address.IPAddressType",
|
|
416
430
|
)
|
|
417
431
|
def conv_ip_address(
|
|
418
432
|
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
419
433
|
) -> UnboundField:
|
|
420
|
-
kwargs.setdefault("label", "IP Address")
|
|
421
434
|
kwargs.setdefault("validators", [])
|
|
422
435
|
kwargs["validators"].append(validators.IPAddress(ipv4=True, ipv6=True))
|
|
423
436
|
return StringField(**kwargs)
|
|
424
437
|
|
|
425
|
-
@converts(
|
|
438
|
+
@converts(
|
|
439
|
+
"sqlalchemy.dialects.postgresql.base.MACADDR",
|
|
440
|
+
"sqlalchemy.dialects.postgresql.types.MACADDR",
|
|
441
|
+
)
|
|
426
442
|
def conv_mac_address(
|
|
427
443
|
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
428
444
|
) -> UnboundField:
|
|
429
|
-
kwargs.setdefault("label", "MAC Address")
|
|
430
445
|
kwargs.setdefault("validators", [])
|
|
431
446
|
kwargs["validators"].append(validators.MacAddress())
|
|
432
447
|
return StringField(**kwargs)
|
|
433
448
|
|
|
434
449
|
@converts(
|
|
435
450
|
"sqlalchemy.dialects.postgresql.base.UUID",
|
|
451
|
+
"sqlalchemy.sql.sqltypes.UUID",
|
|
436
452
|
"sqlalchemy_utils.types.uuid.UUIDType",
|
|
437
453
|
)
|
|
438
454
|
def conv_uuid(
|
|
439
455
|
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
440
456
|
) -> UnboundField:
|
|
441
|
-
kwargs.setdefault("label", "UUID")
|
|
442
457
|
kwargs.setdefault("validators", [])
|
|
443
458
|
kwargs["validators"].append(validators.UUID())
|
|
444
459
|
return StringField(**kwargs)
|
|
445
460
|
|
|
461
|
+
@converts(
|
|
462
|
+
"sqlalchemy.dialects.postgresql.base.ARRAY", "sqlalchemy.sql.sqltypes.ARRAY"
|
|
463
|
+
)
|
|
464
|
+
def conv_ARRAY(
|
|
465
|
+
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
466
|
+
) -> UnboundField:
|
|
467
|
+
return Select2TagsField(**kwargs)
|
|
468
|
+
|
|
446
469
|
@converts("sqlalchemy_utils.types.email.EmailType")
|
|
447
470
|
def conv_email(
|
|
448
471
|
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
449
472
|
) -> UnboundField:
|
|
450
|
-
kwargs.setdefault("label", "Email")
|
|
451
473
|
kwargs.setdefault("validators", [])
|
|
452
474
|
kwargs["validators"].append(validators.Email())
|
|
453
475
|
return StringField(**kwargs)
|
|
@@ -478,6 +500,28 @@ class ModelConverter(ModelConverterBase):
|
|
|
478
500
|
)
|
|
479
501
|
return StringField(**kwargs)
|
|
480
502
|
|
|
503
|
+
@converts("sqlalchemy_utils.types.phone_number.PhoneNumberType")
|
|
504
|
+
def conv_phone_number(
|
|
505
|
+
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
506
|
+
) -> UnboundField:
|
|
507
|
+
kwargs.setdefault("validators", [])
|
|
508
|
+
kwargs["validators"].append(PhoneNumberValidator())
|
|
509
|
+
return StringField(**kwargs)
|
|
510
|
+
|
|
511
|
+
@converts("sqlalchemy_utils.types.color.ColorType")
|
|
512
|
+
def conv_color(
|
|
513
|
+
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
514
|
+
) -> UnboundField:
|
|
515
|
+
kwargs.setdefault("validators", [])
|
|
516
|
+
kwargs["validators"].append(ColorValidator())
|
|
517
|
+
return StringField(**kwargs)
|
|
518
|
+
|
|
519
|
+
@converts("sqlalchemy_fields.types.file.FileType")
|
|
520
|
+
def conv_file(
|
|
521
|
+
self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
|
|
522
|
+
) -> UnboundField:
|
|
523
|
+
return FileField(**kwargs)
|
|
524
|
+
|
|
481
525
|
@converts("ONETOONE")
|
|
482
526
|
def conv_one_to_one(
|
|
483
527
|
self, model: type, prop: RelationshipProperty, kwargs: Dict[str, Any]
|
|
@@ -510,9 +554,10 @@ async def get_model_form(
|
|
|
510
554
|
form_overrides: Optional[Dict[str, Type[Field]]] = None,
|
|
511
555
|
form_ajax_refs: Optional[Dict[str, QueryAjaxModelLoader]] = None,
|
|
512
556
|
form_include_pk: bool = False,
|
|
557
|
+
form_converter: Type[ModelConverterBase] = ModelConverter,
|
|
513
558
|
) -> Type[Form]:
|
|
514
559
|
type_name = model.__name__ + "Form"
|
|
515
|
-
converter =
|
|
560
|
+
converter = form_converter()
|
|
516
561
|
mapper = sqlalchemy_inspect(model)
|
|
517
562
|
form_args = form_args or {}
|
|
518
563
|
form_widget_args = form_widget_args or {}
|