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/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: Dict[str, Any],
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: Dict[str, ConverterCallable] = {}
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: Dict[str, Any],
132
- field_widget_args: Dict[str, Any],
128
+ field_args: dict[str, Any],
129
+ field_widget_args: dict[str, Any],
133
130
  form_include_pk: bool,
134
- label: Optional[str] = None,
135
- loader: Optional[QueryAjaxModelLoader] = None,
136
- ) -> Optional[Dict[str, Any]]:
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: Optional[QueryAjaxModelLoader] = None,
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
- ) -> List[Tuple[str, Any]]:
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: Dict[str, Any],
287
- field_widget_args: Dict[str, Any],
283
+ field_args: dict[str, Any],
284
+ field_widget_args: dict[str, Any],
288
285
  form_include_pk: bool,
289
- label: Optional[str] = None,
290
- override: Optional[Type[Field]] = None,
291
- form_ajax_refs: Dict[str, QueryAjaxModelLoader] = {},
292
- ) -> Optional[UnboundField]:
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) -> List[Validator]:
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Dict[str, Any]
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: Optional[Sequence[str]] = None,
596
- exclude: Optional[Sequence[str]] = None,
597
- column_labels: Optional[Dict[str, str]] = None,
598
- form_args: Optional[Dict[str, Dict[str, Any]]] = None,
599
- form_widget_args: Optional[Dict[str, Dict[str, Any]]] = None,
600
- form_class: Type[Form] = Form,
601
- form_overrides: Optional[Dict[str, Type[Field]]] = None,
602
- form_ajax_refs: Optional[Dict[str, QueryAjaxModelLoader]] = None,
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: Type[ModelConverterBase] = ModelConverter,
605
- ) -> Type[Form]:
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: List[str]) -> None:
137
+ def writerow(self, row: list[str]) -> None:
140
138
  pass # pragma: no cover
141
139
 
142
140
  @abstractmethod
143
- def writerows(self, rows: List[List[str]]) -> None:
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) -> Tuple[Column, ...]:
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) -> Tuple[str, ...]:
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) -> Optional[timedelta]:
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: Dict[str, Any] = match.groupdict()
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.edit_form_query(request)
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, List
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: List[Any]
17
+ rows: list[Any]
16
18
  page: int
17
19
  page_size: int
18
20
  count: int
19
- page_controls: List[PageControl] = field(default_factory=list)
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
- {% for field in form %}
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
- {% for field in form %}
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
- {% if model_view.save_as %}
48
- <input type="submit" name="save" value="Save as new" class="btn">
49
- {% else %}
50
- <input type="submit" name="save" value="Save and add another" class="btn">
51
- {% endif %}
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 %}