sqladmin 0.17.0__py3-none-any.whl → 0.18.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 +11 -9
- sqladmin/_queries.py +8 -6
- sqladmin/ajax.py +5 -3
- sqladmin/application.py +34 -35
- sqladmin/authentication.py +4 -2
- sqladmin/fields.py +35 -33
- sqladmin/forms.py +55 -58
- sqladmin/helpers.py +8 -10
- sqladmin/models.py +59 -1
- sqladmin/pagination.py +5 -3
- sqladmin/templates/sqladmin/_macros.html +33 -0
- sqladmin/templates/sqladmin/create.html +2 -18
- sqladmin/templates/sqladmin/edit.html +8 -24
- sqladmin/templating.py +9 -7
- {sqladmin-0.17.0.dist-info → sqladmin-0.18.0.dist-info}/METADATA +1 -1
- {sqladmin-0.17.0.dist-info → sqladmin-0.18.0.dist-info}/RECORD +19 -19
- {sqladmin-0.17.0.dist-info → sqladmin-0.18.0.dist-info}/WHEEL +1 -1
- {sqladmin-0.17.0.dist-info → sqladmin-0.18.0.dist-info}/licenses/LICENSE.md +0 -0
sqladmin/forms.py
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
2
|
The converters are from Flask-Admin project.
|
|
3
3
|
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
4
6
|
import enum
|
|
5
7
|
import inspect
|
|
6
8
|
import sys
|
|
7
9
|
from typing import (
|
|
8
10
|
Any,
|
|
9
11
|
Callable,
|
|
10
|
-
Dict,
|
|
11
|
-
List,
|
|
12
|
-
Optional,
|
|
13
12
|
Sequence,
|
|
14
|
-
Tuple,
|
|
15
|
-
Type,
|
|
16
13
|
TypeVar,
|
|
17
14
|
Union,
|
|
18
15
|
no_type_check,
|
|
@@ -85,7 +82,7 @@ class ConverterCallable(Protocol):
|
|
|
85
82
|
self,
|
|
86
83
|
model: type,
|
|
87
84
|
prop: MODEL_PROPERTY,
|
|
88
|
-
kwargs:
|
|
85
|
+
kwargs: dict[str, Any],
|
|
89
86
|
) -> UnboundField:
|
|
90
87
|
... # pragma: no cover
|
|
91
88
|
|
|
@@ -107,7 +104,7 @@ def converts(*args: str) -> Callable[[T_CC], T_CC]:
|
|
|
107
104
|
|
|
108
105
|
|
|
109
106
|
class ModelConverterBase:
|
|
110
|
-
_converters:
|
|
107
|
+
_converters: dict[str, ConverterCallable] = {}
|
|
111
108
|
|
|
112
109
|
def __init__(self) -> None:
|
|
113
110
|
super().__init__()
|
|
@@ -128,12 +125,12 @@ class ModelConverterBase:
|
|
|
128
125
|
self,
|
|
129
126
|
prop: MODEL_PROPERTY,
|
|
130
127
|
session_maker: sessionmaker,
|
|
131
|
-
field_args:
|
|
132
|
-
field_widget_args:
|
|
128
|
+
field_args: dict[str, Any],
|
|
129
|
+
field_widget_args: dict[str, Any],
|
|
133
130
|
form_include_pk: bool,
|
|
134
|
-
label:
|
|
135
|
-
loader:
|
|
136
|
-
) ->
|
|
131
|
+
label: str | None = None,
|
|
132
|
+
loader: QueryAjaxModelLoader | None = None,
|
|
133
|
+
) -> dict[str, Any] | None:
|
|
137
134
|
if not isinstance(prop, (RelationshipProperty, ColumnProperty)):
|
|
138
135
|
return None
|
|
139
136
|
|
|
@@ -205,7 +202,7 @@ class ModelConverterBase:
|
|
|
205
202
|
prop: RelationshipProperty,
|
|
206
203
|
kwargs: dict,
|
|
207
204
|
session_maker: sessionmaker,
|
|
208
|
-
loader:
|
|
205
|
+
loader: QueryAjaxModelLoader | None = None,
|
|
209
206
|
) -> dict:
|
|
210
207
|
nullable = True
|
|
211
208
|
for pair in prop.local_remote_pairs:
|
|
@@ -225,7 +222,7 @@ class ModelConverterBase:
|
|
|
225
222
|
self,
|
|
226
223
|
prop: RelationshipProperty,
|
|
227
224
|
session_maker: sessionmaker,
|
|
228
|
-
) ->
|
|
225
|
+
) -> list[tuple[str, Any]]:
|
|
229
226
|
target_model = prop.mapper.class_
|
|
230
227
|
stmt = select(target_model)
|
|
231
228
|
|
|
@@ -283,13 +280,13 @@ class ModelConverterBase:
|
|
|
283
280
|
model: type,
|
|
284
281
|
prop: MODEL_PROPERTY,
|
|
285
282
|
session_maker: sessionmaker,
|
|
286
|
-
field_args:
|
|
287
|
-
field_widget_args:
|
|
283
|
+
field_args: dict[str, Any],
|
|
284
|
+
field_widget_args: dict[str, Any],
|
|
288
285
|
form_include_pk: bool,
|
|
289
|
-
label:
|
|
290
|
-
override:
|
|
291
|
-
form_ajax_refs:
|
|
292
|
-
) ->
|
|
286
|
+
label: str | None = None,
|
|
287
|
+
override: type[Field] | None = None,
|
|
288
|
+
form_ajax_refs: dict[str, QueryAjaxModelLoader] = {},
|
|
289
|
+
) -> UnboundField:
|
|
293
290
|
loader = form_ajax_refs.get(prop.key)
|
|
294
291
|
kwargs = await self._prepare_kwargs(
|
|
295
292
|
prop=prop,
|
|
@@ -329,7 +326,7 @@ class ModelConverterBase:
|
|
|
329
326
|
|
|
330
327
|
class ModelConverter(ModelConverterBase):
|
|
331
328
|
@staticmethod
|
|
332
|
-
def _string_common(prop: ColumnProperty) ->
|
|
329
|
+
def _string_common(prop: ColumnProperty) -> list[Validator]:
|
|
333
330
|
li = []
|
|
334
331
|
column: Column = prop.columns[0]
|
|
335
332
|
if isinstance(column.type.length, int) and column.type.length:
|
|
@@ -338,7 +335,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
338
335
|
|
|
339
336
|
@converts("String", "CHAR") # includes Unicode
|
|
340
337
|
def conv_string(
|
|
341
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
338
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
342
339
|
) -> UnboundField:
|
|
343
340
|
extra_validators = self._string_common(prop)
|
|
344
341
|
kwargs.setdefault("validators", [])
|
|
@@ -347,7 +344,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
347
344
|
|
|
348
345
|
@converts("Text", "LargeBinary", "Binary") # includes UnicodeText
|
|
349
346
|
def conv_text(
|
|
350
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
347
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
351
348
|
) -> UnboundField:
|
|
352
349
|
kwargs.setdefault("validators", [])
|
|
353
350
|
extra_validators = self._string_common(prop)
|
|
@@ -356,7 +353,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
356
353
|
|
|
357
354
|
@converts("Boolean", "dialects.mssql.base.BIT")
|
|
358
355
|
def conv_boolean(
|
|
359
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
356
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
360
357
|
) -> UnboundField:
|
|
361
358
|
if not prop.columns[0].nullable:
|
|
362
359
|
kwargs.setdefault("render_kw", {})
|
|
@@ -370,25 +367,25 @@ class ModelConverter(ModelConverterBase):
|
|
|
370
367
|
|
|
371
368
|
@converts("Date")
|
|
372
369
|
def conv_date(
|
|
373
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
370
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
374
371
|
) -> UnboundField:
|
|
375
372
|
return DateField(**kwargs)
|
|
376
373
|
|
|
377
374
|
@converts("Time")
|
|
378
375
|
def conv_time(
|
|
379
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
376
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
380
377
|
) -> UnboundField:
|
|
381
378
|
return TimeField(**kwargs)
|
|
382
379
|
|
|
383
380
|
@converts("DateTime")
|
|
384
381
|
def conv_datetime(
|
|
385
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
382
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
386
383
|
) -> UnboundField:
|
|
387
384
|
return DateTimeField(**kwargs)
|
|
388
385
|
|
|
389
386
|
@converts("Enum")
|
|
390
387
|
def conv_enum(
|
|
391
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
388
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
392
389
|
) -> UnboundField:
|
|
393
390
|
available_choices = [(e, e) for e in prop.columns[0].type.enums]
|
|
394
391
|
accepted_values = [choice[0] for choice in available_choices]
|
|
@@ -408,13 +405,13 @@ class ModelConverter(ModelConverterBase):
|
|
|
408
405
|
|
|
409
406
|
@converts("Integer") # includes BigInteger and SmallInteger
|
|
410
407
|
def conv_integer(
|
|
411
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
408
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
412
409
|
) -> UnboundField:
|
|
413
410
|
return IntegerField(**kwargs)
|
|
414
411
|
|
|
415
412
|
@converts("Numeric") # includes DECIMAL, Float/FLOAT, REAL, and DOUBLE
|
|
416
413
|
def conv_decimal(
|
|
417
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
414
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
418
415
|
) -> UnboundField:
|
|
419
416
|
# override default decimal places limit, use database defaults instead
|
|
420
417
|
kwargs.setdefault("places", None)
|
|
@@ -422,13 +419,13 @@ class ModelConverter(ModelConverterBase):
|
|
|
422
419
|
|
|
423
420
|
@converts("JSON", "JSONB")
|
|
424
421
|
def conv_json(
|
|
425
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
422
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
426
423
|
) -> UnboundField:
|
|
427
424
|
return JSONField(**kwargs)
|
|
428
425
|
|
|
429
426
|
@converts("Interval")
|
|
430
427
|
def conv_interval(
|
|
431
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
428
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
432
429
|
) -> UnboundField:
|
|
433
430
|
kwargs["render_kw"]["placeholder"] = "Like: 1 day 1:25:33.652"
|
|
434
431
|
return IntervalField(**kwargs)
|
|
@@ -439,7 +436,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
439
436
|
"sqlalchemy_utils.types.ip_address.IPAddressType",
|
|
440
437
|
)
|
|
441
438
|
def conv_ip_address(
|
|
442
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
439
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
443
440
|
) -> UnboundField:
|
|
444
441
|
kwargs.setdefault("validators", [])
|
|
445
442
|
kwargs["validators"].append(validators.IPAddress(ipv4=True, ipv6=True))
|
|
@@ -450,7 +447,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
450
447
|
"sqlalchemy.dialects.postgresql.types.MACADDR",
|
|
451
448
|
)
|
|
452
449
|
def conv_mac_address(
|
|
453
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
450
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
454
451
|
) -> UnboundField:
|
|
455
452
|
kwargs.setdefault("validators", [])
|
|
456
453
|
kwargs["validators"].append(validators.MacAddress())
|
|
@@ -463,7 +460,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
463
460
|
"sqlalchemy_utils.types.uuid.UUIDType",
|
|
464
461
|
)
|
|
465
462
|
def conv_uuid(
|
|
466
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
463
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
467
464
|
) -> UnboundField:
|
|
468
465
|
kwargs.setdefault("validators", [])
|
|
469
466
|
kwargs["validators"].append(validators.UUID())
|
|
@@ -473,13 +470,13 @@ class ModelConverter(ModelConverterBase):
|
|
|
473
470
|
"sqlalchemy.dialects.postgresql.base.ARRAY", "sqlalchemy.sql.sqltypes.ARRAY"
|
|
474
471
|
)
|
|
475
472
|
def conv_ARRAY(
|
|
476
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
473
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
477
474
|
) -> UnboundField:
|
|
478
475
|
return Select2TagsField(**kwargs)
|
|
479
476
|
|
|
480
477
|
@converts("sqlalchemy_utils.types.email.EmailType")
|
|
481
478
|
def conv_email(
|
|
482
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
479
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
483
480
|
) -> UnboundField:
|
|
484
481
|
kwargs.setdefault("validators", [])
|
|
485
482
|
kwargs["validators"].append(validators.Email())
|
|
@@ -487,7 +484,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
487
484
|
|
|
488
485
|
@converts("sqlalchemy_utils.types.url.URLType")
|
|
489
486
|
def conv_url(
|
|
490
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
487
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
491
488
|
) -> UnboundField:
|
|
492
489
|
kwargs.setdefault("validators", [])
|
|
493
490
|
kwargs["validators"].append(validators.URL())
|
|
@@ -495,7 +492,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
495
492
|
|
|
496
493
|
@converts("sqlalchemy_utils.types.currency.CurrencyType")
|
|
497
494
|
def conv_currency(
|
|
498
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
495
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
499
496
|
) -> UnboundField:
|
|
500
497
|
kwargs.setdefault("validators", [])
|
|
501
498
|
kwargs["validators"].append(CurrencyValidator())
|
|
@@ -503,7 +500,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
503
500
|
|
|
504
501
|
@converts("sqlalchemy_utils.types.timezone.TimezoneType")
|
|
505
502
|
def conv_timezone(
|
|
506
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
503
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
507
504
|
) -> UnboundField:
|
|
508
505
|
kwargs.setdefault("validators", [])
|
|
509
506
|
kwargs["validators"].append(
|
|
@@ -513,7 +510,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
513
510
|
|
|
514
511
|
@converts("sqlalchemy_utils.types.phone_number.PhoneNumberType")
|
|
515
512
|
def conv_phone_number(
|
|
516
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
513
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
517
514
|
) -> UnboundField:
|
|
518
515
|
kwargs.setdefault("validators", [])
|
|
519
516
|
kwargs["validators"].append(PhoneNumberValidator())
|
|
@@ -521,7 +518,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
521
518
|
|
|
522
519
|
@converts("sqlalchemy_utils.types.color.ColorType")
|
|
523
520
|
def conv_color(
|
|
524
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
521
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
525
522
|
) -> UnboundField:
|
|
526
523
|
kwargs.setdefault("validators", [])
|
|
527
524
|
kwargs["validators"].append(ColorValidator())
|
|
@@ -530,7 +527,7 @@ class ModelConverter(ModelConverterBase):
|
|
|
530
527
|
@converts("sqlalchemy_utils.types.choice.ChoiceType")
|
|
531
528
|
@no_type_check
|
|
532
529
|
def convert_choice_type(
|
|
533
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
530
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
534
531
|
) -> UnboundField:
|
|
535
532
|
available_choices = []
|
|
536
533
|
column = prop.columns[0]
|
|
@@ -559,32 +556,32 @@ class ModelConverter(ModelConverterBase):
|
|
|
559
556
|
|
|
560
557
|
@converts("fastapi_storages.integrations.sqlalchemy.FileType")
|
|
561
558
|
def conv_file(
|
|
562
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
559
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
563
560
|
) -> UnboundField:
|
|
564
561
|
return FileField(**kwargs)
|
|
565
562
|
|
|
566
563
|
@converts("fastapi_storages.integrations.sqlalchemy.ImageType")
|
|
567
564
|
def conv_image(
|
|
568
|
-
self, model: type, prop: ColumnProperty, kwargs:
|
|
565
|
+
self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
|
|
569
566
|
) -> UnboundField:
|
|
570
567
|
return FileField(**kwargs)
|
|
571
568
|
|
|
572
569
|
@converts("ONETOONE")
|
|
573
570
|
def conv_one_to_one(
|
|
574
|
-
self, model: type, prop: RelationshipProperty, kwargs:
|
|
571
|
+
self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
|
|
575
572
|
) -> UnboundField:
|
|
576
573
|
kwargs["allow_blank"] = True
|
|
577
574
|
return QuerySelectField(**kwargs)
|
|
578
575
|
|
|
579
576
|
@converts("MANYTOONE")
|
|
580
577
|
def conv_many_to_one(
|
|
581
|
-
self, model: type, prop: RelationshipProperty, kwargs:
|
|
578
|
+
self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
|
|
582
579
|
) -> UnboundField:
|
|
583
580
|
return QuerySelectField(**kwargs)
|
|
584
581
|
|
|
585
582
|
@converts("MANYTOMANY", "ONETOMANY")
|
|
586
583
|
def conv_many_to_many(
|
|
587
|
-
self, model: type, prop: RelationshipProperty, kwargs:
|
|
584
|
+
self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
|
|
588
585
|
) -> UnboundField:
|
|
589
586
|
return QuerySelectMultipleField(**kwargs)
|
|
590
587
|
|
|
@@ -592,17 +589,17 @@ class ModelConverter(ModelConverterBase):
|
|
|
592
589
|
async def get_model_form(
|
|
593
590
|
model: type,
|
|
594
591
|
session_maker: sessionmaker,
|
|
595
|
-
only:
|
|
596
|
-
exclude:
|
|
597
|
-
column_labels:
|
|
598
|
-
form_args:
|
|
599
|
-
form_widget_args:
|
|
600
|
-
form_class:
|
|
601
|
-
form_overrides:
|
|
602
|
-
form_ajax_refs:
|
|
592
|
+
only: Sequence[str] | None = None,
|
|
593
|
+
exclude: Sequence[str] | None = None,
|
|
594
|
+
column_labels: dict[str, str] | None = None,
|
|
595
|
+
form_args: dict[str, dict[str, Any]] | None = None,
|
|
596
|
+
form_widget_args: dict[str, dict[str, Any]] | None = None,
|
|
597
|
+
form_class: type[Form] = Form,
|
|
598
|
+
form_overrides: dict[str, type[Field]] | None = None,
|
|
599
|
+
form_ajax_refs: dict[str, QueryAjaxModelLoader] | None = None,
|
|
603
600
|
form_include_pk: bool = False,
|
|
604
|
-
form_converter:
|
|
605
|
-
) ->
|
|
601
|
+
form_converter: type[ModelConverterBase] = ModelConverter,
|
|
602
|
+
) -> type[Form]:
|
|
606
603
|
type_name = model.__name__ + "Form"
|
|
607
604
|
converter = form_converter()
|
|
608
605
|
mapper = sqlalchemy_inspect(model)
|
sqladmin/helpers.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import csv
|
|
2
4
|
import enum
|
|
3
5
|
import os
|
|
@@ -9,11 +11,7 @@ from typing import (
|
|
|
9
11
|
Any,
|
|
10
12
|
AsyncGenerator,
|
|
11
13
|
Callable,
|
|
12
|
-
Dict,
|
|
13
14
|
Generator,
|
|
14
|
-
List,
|
|
15
|
-
Optional,
|
|
16
|
-
Tuple,
|
|
17
15
|
TypeVar,
|
|
18
16
|
)
|
|
19
17
|
|
|
@@ -136,11 +134,11 @@ class Writer(ABC):
|
|
|
136
134
|
"""https://docs.python.org/3/library/csv.html#writer-objects"""
|
|
137
135
|
|
|
138
136
|
@abstractmethod
|
|
139
|
-
def writerow(self, row:
|
|
137
|
+
def writerow(self, row: list[str]) -> None:
|
|
140
138
|
pass # pragma: no cover
|
|
141
139
|
|
|
142
140
|
@abstractmethod
|
|
143
|
-
def writerows(self, rows:
|
|
141
|
+
def writerows(self, rows: list[list[str]]) -> None:
|
|
144
142
|
pass # pragma: no cover
|
|
145
143
|
|
|
146
144
|
@property
|
|
@@ -174,7 +172,7 @@ def stream_to_csv(
|
|
|
174
172
|
return callback(writer) # type: ignore
|
|
175
173
|
|
|
176
174
|
|
|
177
|
-
def get_primary_keys(model: Any) ->
|
|
175
|
+
def get_primary_keys(model: Any) -> tuple[Column, ...]:
|
|
178
176
|
return tuple(inspect(model).mapper.primary_key)
|
|
179
177
|
|
|
180
178
|
|
|
@@ -191,7 +189,7 @@ def get_object_identifier(obj: Any) -> Any:
|
|
|
191
189
|
return ";".join(str(v).replace("\\", "\\\\").replace(";", r"\;") for v in values)
|
|
192
190
|
|
|
193
191
|
|
|
194
|
-
def _object_identifier_parts(id_string: str, model: type) ->
|
|
192
|
+
def _object_identifier_parts(id_string: str, model: type) -> tuple[str, ...]:
|
|
195
193
|
pks = get_primary_keys(model)
|
|
196
194
|
if len(pks) == 1:
|
|
197
195
|
# Only one primary key so no special processing
|
|
@@ -255,7 +253,7 @@ def is_relationship(prop: MODEL_PROPERTY) -> bool:
|
|
|
255
253
|
return isinstance(prop, RelationshipProperty)
|
|
256
254
|
|
|
257
255
|
|
|
258
|
-
def parse_interval(value: str) ->
|
|
256
|
+
def parse_interval(value: str) -> timedelta | None:
|
|
259
257
|
match = (
|
|
260
258
|
standard_duration_re.match(value)
|
|
261
259
|
or iso8601_duration_re.match(value)
|
|
@@ -265,7 +263,7 @@ def parse_interval(value: str) -> Optional[timedelta]:
|
|
|
265
263
|
if not match:
|
|
266
264
|
return None
|
|
267
265
|
|
|
268
|
-
kw:
|
|
266
|
+
kw: dict[str, Any] = match.groupdict()
|
|
269
267
|
sign = -1 if kw.pop("sign", "+") == "-" else 1
|
|
270
268
|
if kw.get("microseconds"):
|
|
271
269
|
kw["microseconds"] = kw["microseconds"].ljust(6, "0")
|
sqladmin/models.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import time
|
|
4
|
+
import warnings
|
|
2
5
|
from enum import Enum
|
|
3
6
|
from typing import (
|
|
4
7
|
TYPE_CHECKING,
|
|
@@ -29,6 +32,7 @@ from starlette.exceptions import HTTPException
|
|
|
29
32
|
from starlette.requests import Request
|
|
30
33
|
from starlette.responses import StreamingResponse
|
|
31
34
|
from wtforms import Field, Form
|
|
35
|
+
from wtforms.fields.core import UnboundField
|
|
32
36
|
|
|
33
37
|
from sqladmin._queries import Query
|
|
34
38
|
from sqladmin._types import MODEL_ATTR
|
|
@@ -598,6 +602,28 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
598
602
|
```
|
|
599
603
|
"""
|
|
600
604
|
|
|
605
|
+
form_rules: ClassVar[list[str]] = []
|
|
606
|
+
"""List of rendering rules for model creation and edit form.
|
|
607
|
+
This property changes default form rendering behavior and to rearrange
|
|
608
|
+
order of rendered fields, add some text between fields, group them, etc.
|
|
609
|
+
If not set, will use default Flask-Admin form rendering logic.
|
|
610
|
+
|
|
611
|
+
???+ example
|
|
612
|
+
```python
|
|
613
|
+
class UserAdmin(ModelAdmin, model=User):
|
|
614
|
+
form_rules = [
|
|
615
|
+
"first_name",
|
|
616
|
+
"last_name",
|
|
617
|
+
]
|
|
618
|
+
```
|
|
619
|
+
"""
|
|
620
|
+
|
|
621
|
+
form_create_rules: ClassVar[list[str]] = []
|
|
622
|
+
"""Customized rules for the create form. Cannot be specified with `form_rules`."""
|
|
623
|
+
|
|
624
|
+
form_edit_rules: ClassVar[list[str]] = []
|
|
625
|
+
"""Customized rules for the edit form. Cannot be specified with `form_rules`."""
|
|
626
|
+
|
|
601
627
|
# General options
|
|
602
628
|
column_labels: ClassVar[Dict[MODEL_ATTR, str]] = {}
|
|
603
629
|
"""A mapping of column labels, used to map column names to new names.
|
|
@@ -685,6 +711,8 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
685
711
|
model_admin=self, name=name, options=options
|
|
686
712
|
)
|
|
687
713
|
|
|
714
|
+
self._refresh_form_rules_cache()
|
|
715
|
+
|
|
688
716
|
self._custom_actions_in_list: Dict[str, str] = {}
|
|
689
717
|
self._custom_actions_in_detail: Dict[str, str] = {}
|
|
690
718
|
self._custom_actions_confirmation: Dict[str, str] = {}
|
|
@@ -820,7 +848,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
820
848
|
return await self._get_object_by_pk(stmt)
|
|
821
849
|
|
|
822
850
|
async def get_object_for_edit(self, request: Request) -> Any:
|
|
823
|
-
stmt = self.
|
|
851
|
+
stmt = self.form_edit_query(request)
|
|
824
852
|
return await self._get_object_by_pk(stmt)
|
|
825
853
|
|
|
826
854
|
async def get_object_for_delete(self, value: Any) -> Any:
|
|
@@ -1054,6 +1082,13 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1054
1082
|
return select(self.model)
|
|
1055
1083
|
|
|
1056
1084
|
def edit_form_query(self, request: Request) -> Select:
|
|
1085
|
+
msg = (
|
|
1086
|
+
"Overriding 'edit_form_query' is deprecated. Use 'form_edit_query' instead."
|
|
1087
|
+
)
|
|
1088
|
+
warnings.warn(msg, DeprecationWarning, stacklevel=2)
|
|
1089
|
+
return self.form_edit_query(request)
|
|
1090
|
+
|
|
1091
|
+
def form_edit_query(self, request: Request) -> Select:
|
|
1057
1092
|
"""
|
|
1058
1093
|
The SQLAlchemy select expression used for the edit form page which can be
|
|
1059
1094
|
customized. By default it will select the object by primary key(s) without any
|
|
@@ -1143,3 +1178,26 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
|
|
|
1143
1178
|
media_type="text/csv",
|
|
1144
1179
|
headers={"Content-Disposition": f"attachment;filename={filename}"},
|
|
1145
1180
|
)
|
|
1181
|
+
|
|
1182
|
+
def _refresh_form_rules_cache(self) -> None:
|
|
1183
|
+
if self.form_rules:
|
|
1184
|
+
self._form_create_rules = self.form_rules
|
|
1185
|
+
self._form_edit_rules = self.form_rules
|
|
1186
|
+
else:
|
|
1187
|
+
self._form_create_rules = self.form_create_rules
|
|
1188
|
+
self._form_edit_rules = self.form_edit_rules
|
|
1189
|
+
|
|
1190
|
+
def _validate_form_class(self, ruleset: List[Any], form_class: Type[Form]) -> None:
|
|
1191
|
+
form_fields = []
|
|
1192
|
+
for name, obj in form_class.__dict__.items():
|
|
1193
|
+
if isinstance(obj, UnboundField):
|
|
1194
|
+
form_fields.append(name)
|
|
1195
|
+
|
|
1196
|
+
missing_fields = []
|
|
1197
|
+
if ruleset:
|
|
1198
|
+
for field_name in form_fields:
|
|
1199
|
+
if field_name not in ruleset:
|
|
1200
|
+
missing_fields.append(field_name)
|
|
1201
|
+
|
|
1202
|
+
for field_name in missing_fields:
|
|
1203
|
+
delattr(form_class, field_name)
|
sqladmin/pagination.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from dataclasses import dataclass, field
|
|
2
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
3
5
|
|
|
4
6
|
from starlette.datastructures import URL
|
|
5
7
|
|
|
@@ -12,11 +14,11 @@ class PageControl:
|
|
|
12
14
|
|
|
13
15
|
@dataclass
|
|
14
16
|
class Pagination:
|
|
15
|
-
rows:
|
|
17
|
+
rows: list[Any]
|
|
16
18
|
page: int
|
|
17
19
|
page_size: int
|
|
18
20
|
count: int
|
|
19
|
-
page_controls:
|
|
21
|
+
page_controls: list[PageControl] = field(default_factory=list)
|
|
20
22
|
max_page_controls: int = 7
|
|
21
23
|
|
|
22
24
|
@property
|
|
@@ -53,3 +53,36 @@
|
|
|
53
53
|
{% endfor %}
|
|
54
54
|
</div>
|
|
55
55
|
{% endmacro %}
|
|
56
|
+
|
|
57
|
+
{% macro render_field(field, kwargs={}) %}
|
|
58
|
+
<div class="mb-3 form-group row">
|
|
59
|
+
{{ field.label(class_="form-label col-sm-2 col-form-label") }}
|
|
60
|
+
<div class="col-sm-10">
|
|
61
|
+
{% if field.errors %}
|
|
62
|
+
{{ field(class_="form-control is-invalid") }}
|
|
63
|
+
{% else %}
|
|
64
|
+
{{ field() }}
|
|
65
|
+
{% endif %}
|
|
66
|
+
{% for error in field.errors %}
|
|
67
|
+
<div class="invalid-feedback">{{ error }}</div>
|
|
68
|
+
{% endfor %}
|
|
69
|
+
{% if field.description %}
|
|
70
|
+
<small class="text-muted">{{ field.description }}</small>
|
|
71
|
+
{% endif %}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
{% endmacro %}
|
|
75
|
+
|
|
76
|
+
{% macro render_form_fields(form, form_opts=None) %}
|
|
77
|
+
{% if form.hidden_tag is defined %}
|
|
78
|
+
{{ form.hidden_tag() }}
|
|
79
|
+
{% else %}
|
|
80
|
+
{% for f in form if f.widget.input_type == 'hidden' %}
|
|
81
|
+
{{ f }}
|
|
82
|
+
{% endfor %}
|
|
83
|
+
{% endif %}
|
|
84
|
+
|
|
85
|
+
{% for f in form if f.widget.input_type != 'hidden' %}
|
|
86
|
+
{{ render_field(f, kwargs) }}
|
|
87
|
+
{% endfor %}
|
|
88
|
+
{% endmacro %}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{% extends "sqladmin/layout.html" %}
|
|
2
|
+
{% from 'sqladmin/_macros.html' import render_form_fields %}
|
|
2
3
|
{% block content %}
|
|
3
4
|
<div class="col-12">
|
|
4
5
|
<div class="card">
|
|
@@ -14,24 +15,7 @@
|
|
|
14
15
|
{% endif %}
|
|
15
16
|
</div>
|
|
16
17
|
<fieldset class="form-fieldset">
|
|
17
|
-
{
|
|
18
|
-
<div class="mb-3 form-group row">
|
|
19
|
-
{{ field.label(class_="form-label col-sm-2 col-form-label") }}
|
|
20
|
-
<div class="col-sm-10">
|
|
21
|
-
{% if field.errors %}
|
|
22
|
-
{{ field(class_="form-control is-invalid") }}
|
|
23
|
-
{% else %}
|
|
24
|
-
{{ field() }}
|
|
25
|
-
{% endif %}
|
|
26
|
-
{% for error in field.errors %}
|
|
27
|
-
<div class="invalid-feedback">{{ error }}</div>
|
|
28
|
-
{% endfor %}
|
|
29
|
-
{% if field.description %}
|
|
30
|
-
<small class="text-muted">{{ field.description }}</small>
|
|
31
|
-
{% endif %}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
{% endfor %}
|
|
18
|
+
{{ render_form_fields(form, form_opts=form_opts) }}
|
|
35
19
|
</fieldset>
|
|
36
20
|
<div class="row">
|
|
37
21
|
<div class="col-md-2">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
{% extends "sqladmin/layout.html" %}
|
|
2
|
+
{% from 'sqladmin/_macros.html' import render_form_fields %}
|
|
2
3
|
{% block content %}
|
|
3
4
|
<div class="col-12">
|
|
4
5
|
<div class="card">
|
|
@@ -14,24 +15,7 @@
|
|
|
14
15
|
{% endif %}
|
|
15
16
|
</div>
|
|
16
17
|
<fieldset class="form-fieldset">
|
|
17
|
-
{
|
|
18
|
-
<div class="mb-3 form-group row">
|
|
19
|
-
{{ field.label(class_="form-label col-sm-2 col-form-label") }}
|
|
20
|
-
<div class="col-sm-10">
|
|
21
|
-
{% if field.errors %}
|
|
22
|
-
{{ field(class_="form-control is-invalid") }}
|
|
23
|
-
{% else %}
|
|
24
|
-
{{ field() }}
|
|
25
|
-
{% endif %}
|
|
26
|
-
{% for error in field.errors %}
|
|
27
|
-
<div class="invalid-feedback">{{ error }}</div>
|
|
28
|
-
{% endfor %}
|
|
29
|
-
{% if field.description %}
|
|
30
|
-
<small class="text-muted">{{ field.description }}</small>
|
|
31
|
-
{% endif %}
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
{% endfor %}
|
|
18
|
+
{{ render_form_fields(form, form_opts=form_opts) }}
|
|
35
19
|
</fieldset>
|
|
36
20
|
<div class="row">
|
|
37
21
|
<div class="col-md-2">
|
|
@@ -44,11 +28,11 @@
|
|
|
44
28
|
<input type="submit" name="save" value="Save" class="btn">
|
|
45
29
|
<input type="submit" name="save" value="Save and continue editing" class="btn">
|
|
46
30
|
{% if model_view.can_create %}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
31
|
+
{% if model_view.save_as %}
|
|
32
|
+
<input type="submit" name="save" value="Save as new" class="btn">
|
|
33
|
+
{% else %}
|
|
34
|
+
<input type="submit" name="save" value="Save and add another" class="btn">
|
|
35
|
+
{% endif %}
|
|
52
36
|
{% endif %}
|
|
53
37
|
</div>
|
|
54
38
|
</div>
|
|
@@ -57,4 +41,4 @@
|
|
|
57
41
|
</div>
|
|
58
42
|
</div>
|
|
59
43
|
</div>
|
|
60
|
-
{% endblock %}
|
|
44
|
+
{% endblock %}
|