sqladmin 0.17.0__py3-none-any.whl → 0.19.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,
@@ -21,7 +18,12 @@ from typing import (
21
18
  import anyio
22
19
  from sqlalchemy import Boolean, select
23
20
  from sqlalchemy import inspect as sqlalchemy_inspect
24
- from sqlalchemy.orm import ColumnProperty, RelationshipProperty, sessionmaker
21
+ from sqlalchemy.orm import (
22
+ ColumnProperty,
23
+ RelationshipProperty,
24
+ sessionmaker,
25
+ )
26
+ from sqlalchemy.sql.elements import Label
25
27
  from sqlalchemy.sql.schema import Column
26
28
  from wtforms import (
27
29
  BooleanField,
@@ -85,7 +87,7 @@ class ConverterCallable(Protocol):
85
87
  self,
86
88
  model: type,
87
89
  prop: MODEL_PROPERTY,
88
- kwargs: Dict[str, Any],
90
+ kwargs: dict[str, Any],
89
91
  ) -> UnboundField:
90
92
  ... # pragma: no cover
91
93
 
@@ -107,7 +109,7 @@ def converts(*args: str) -> Callable[[T_CC], T_CC]:
107
109
 
108
110
 
109
111
  class ModelConverterBase:
110
- _converters: Dict[str, ConverterCallable] = {}
112
+ _converters: dict[str, ConverterCallable] = {}
111
113
 
112
114
  def __init__(self) -> None:
113
115
  super().__init__()
@@ -128,12 +130,12 @@ class ModelConverterBase:
128
130
  self,
129
131
  prop: MODEL_PROPERTY,
130
132
  session_maker: sessionmaker,
131
- field_args: Dict[str, Any],
132
- field_widget_args: Dict[str, Any],
133
+ field_args: dict[str, Any],
134
+ field_widget_args: dict[str, Any],
133
135
  form_include_pk: bool,
134
- label: Optional[str] = None,
135
- loader: Optional[QueryAjaxModelLoader] = None,
136
- ) -> Optional[Dict[str, Any]]:
136
+ label: str | None = None,
137
+ loader: QueryAjaxModelLoader | None = None,
138
+ ) -> dict[str, Any] | None:
137
139
  if not isinstance(prop, (RelationshipProperty, ColumnProperty)):
138
140
  return None
139
141
 
@@ -205,7 +207,7 @@ class ModelConverterBase:
205
207
  prop: RelationshipProperty,
206
208
  kwargs: dict,
207
209
  session_maker: sessionmaker,
208
- loader: Optional[QueryAjaxModelLoader] = None,
210
+ loader: QueryAjaxModelLoader | None = None,
209
211
  ) -> dict:
210
212
  nullable = True
211
213
  for pair in prop.local_remote_pairs:
@@ -225,7 +227,7 @@ class ModelConverterBase:
225
227
  self,
226
228
  prop: RelationshipProperty,
227
229
  session_maker: sessionmaker,
228
- ) -> List[Tuple[str, Any]]:
230
+ ) -> list[tuple[str, Any]]:
229
231
  target_model = prop.mapper.class_
230
232
  stmt = select(target_model)
231
233
 
@@ -283,13 +285,13 @@ class ModelConverterBase:
283
285
  model: type,
284
286
  prop: MODEL_PROPERTY,
285
287
  session_maker: sessionmaker,
286
- field_args: Dict[str, Any],
287
- field_widget_args: Dict[str, Any],
288
+ field_args: dict[str, Any],
289
+ field_widget_args: dict[str, Any],
288
290
  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]:
291
+ label: str | None = None,
292
+ override: type[Field] | None = None,
293
+ form_ajax_refs: dict[str, QueryAjaxModelLoader] = {},
294
+ ) -> UnboundField:
293
295
  loader = form_ajax_refs.get(prop.key)
294
296
  kwargs = await self._prepare_kwargs(
295
297
  prop=prop,
@@ -329,7 +331,7 @@ class ModelConverterBase:
329
331
 
330
332
  class ModelConverter(ModelConverterBase):
331
333
  @staticmethod
332
- def _string_common(prop: ColumnProperty) -> List[Validator]:
334
+ def _string_common(prop: ColumnProperty) -> list[Validator]:
333
335
  li = []
334
336
  column: Column = prop.columns[0]
335
337
  if isinstance(column.type.length, int) and column.type.length:
@@ -338,7 +340,7 @@ class ModelConverter(ModelConverterBase):
338
340
 
339
341
  @converts("String", "CHAR") # includes Unicode
340
342
  def conv_string(
341
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
343
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
342
344
  ) -> UnboundField:
343
345
  extra_validators = self._string_common(prop)
344
346
  kwargs.setdefault("validators", [])
@@ -347,7 +349,7 @@ class ModelConverter(ModelConverterBase):
347
349
 
348
350
  @converts("Text", "LargeBinary", "Binary") # includes UnicodeText
349
351
  def conv_text(
350
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
352
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
351
353
  ) -> UnboundField:
352
354
  kwargs.setdefault("validators", [])
353
355
  extra_validators = self._string_common(prop)
@@ -356,7 +358,7 @@ class ModelConverter(ModelConverterBase):
356
358
 
357
359
  @converts("Boolean", "dialects.mssql.base.BIT")
358
360
  def conv_boolean(
359
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
361
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
360
362
  ) -> UnboundField:
361
363
  if not prop.columns[0].nullable:
362
364
  kwargs.setdefault("render_kw", {})
@@ -370,25 +372,25 @@ class ModelConverter(ModelConverterBase):
370
372
 
371
373
  @converts("Date")
372
374
  def conv_date(
373
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
375
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
374
376
  ) -> UnboundField:
375
377
  return DateField(**kwargs)
376
378
 
377
379
  @converts("Time")
378
380
  def conv_time(
379
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
381
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
380
382
  ) -> UnboundField:
381
383
  return TimeField(**kwargs)
382
384
 
383
385
  @converts("DateTime")
384
386
  def conv_datetime(
385
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
387
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
386
388
  ) -> UnboundField:
387
389
  return DateTimeField(**kwargs)
388
390
 
389
391
  @converts("Enum")
390
392
  def conv_enum(
391
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
393
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
392
394
  ) -> UnboundField:
393
395
  available_choices = [(e, e) for e in prop.columns[0].type.enums]
394
396
  accepted_values = [choice[0] for choice in available_choices]
@@ -408,13 +410,13 @@ class ModelConverter(ModelConverterBase):
408
410
 
409
411
  @converts("Integer") # includes BigInteger and SmallInteger
410
412
  def conv_integer(
411
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
413
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
412
414
  ) -> UnboundField:
413
415
  return IntegerField(**kwargs)
414
416
 
415
417
  @converts("Numeric") # includes DECIMAL, Float/FLOAT, REAL, and DOUBLE
416
418
  def conv_decimal(
417
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
419
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
418
420
  ) -> UnboundField:
419
421
  # override default decimal places limit, use database defaults instead
420
422
  kwargs.setdefault("places", None)
@@ -422,13 +424,13 @@ class ModelConverter(ModelConverterBase):
422
424
 
423
425
  @converts("JSON", "JSONB")
424
426
  def conv_json(
425
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
427
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
426
428
  ) -> UnboundField:
427
429
  return JSONField(**kwargs)
428
430
 
429
431
  @converts("Interval")
430
432
  def conv_interval(
431
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
433
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
432
434
  ) -> UnboundField:
433
435
  kwargs["render_kw"]["placeholder"] = "Like: 1 day 1:25:33.652"
434
436
  return IntervalField(**kwargs)
@@ -439,7 +441,7 @@ class ModelConverter(ModelConverterBase):
439
441
  "sqlalchemy_utils.types.ip_address.IPAddressType",
440
442
  )
441
443
  def conv_ip_address(
442
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
444
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
443
445
  ) -> UnboundField:
444
446
  kwargs.setdefault("validators", [])
445
447
  kwargs["validators"].append(validators.IPAddress(ipv4=True, ipv6=True))
@@ -450,7 +452,7 @@ class ModelConverter(ModelConverterBase):
450
452
  "sqlalchemy.dialects.postgresql.types.MACADDR",
451
453
  )
452
454
  def conv_mac_address(
453
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
455
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
454
456
  ) -> UnboundField:
455
457
  kwargs.setdefault("validators", [])
456
458
  kwargs["validators"].append(validators.MacAddress())
@@ -463,7 +465,7 @@ class ModelConverter(ModelConverterBase):
463
465
  "sqlalchemy_utils.types.uuid.UUIDType",
464
466
  )
465
467
  def conv_uuid(
466
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
468
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
467
469
  ) -> UnboundField:
468
470
  kwargs.setdefault("validators", [])
469
471
  kwargs["validators"].append(validators.UUID())
@@ -473,13 +475,13 @@ class ModelConverter(ModelConverterBase):
473
475
  "sqlalchemy.dialects.postgresql.base.ARRAY", "sqlalchemy.sql.sqltypes.ARRAY"
474
476
  )
475
477
  def conv_ARRAY(
476
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
478
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
477
479
  ) -> UnboundField:
478
480
  return Select2TagsField(**kwargs)
479
481
 
480
482
  @converts("sqlalchemy_utils.types.email.EmailType")
481
483
  def conv_email(
482
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
484
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
483
485
  ) -> UnboundField:
484
486
  kwargs.setdefault("validators", [])
485
487
  kwargs["validators"].append(validators.Email())
@@ -487,7 +489,7 @@ class ModelConverter(ModelConverterBase):
487
489
 
488
490
  @converts("sqlalchemy_utils.types.url.URLType")
489
491
  def conv_url(
490
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
492
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
491
493
  ) -> UnboundField:
492
494
  kwargs.setdefault("validators", [])
493
495
  kwargs["validators"].append(validators.URL())
@@ -495,7 +497,7 @@ class ModelConverter(ModelConverterBase):
495
497
 
496
498
  @converts("sqlalchemy_utils.types.currency.CurrencyType")
497
499
  def conv_currency(
498
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
500
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
499
501
  ) -> UnboundField:
500
502
  kwargs.setdefault("validators", [])
501
503
  kwargs["validators"].append(CurrencyValidator())
@@ -503,7 +505,7 @@ class ModelConverter(ModelConverterBase):
503
505
 
504
506
  @converts("sqlalchemy_utils.types.timezone.TimezoneType")
505
507
  def conv_timezone(
506
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
508
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
507
509
  ) -> UnboundField:
508
510
  kwargs.setdefault("validators", [])
509
511
  kwargs["validators"].append(
@@ -513,7 +515,7 @@ class ModelConverter(ModelConverterBase):
513
515
 
514
516
  @converts("sqlalchemy_utils.types.phone_number.PhoneNumberType")
515
517
  def conv_phone_number(
516
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
518
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
517
519
  ) -> UnboundField:
518
520
  kwargs.setdefault("validators", [])
519
521
  kwargs["validators"].append(PhoneNumberValidator())
@@ -521,7 +523,7 @@ class ModelConverter(ModelConverterBase):
521
523
 
522
524
  @converts("sqlalchemy_utils.types.color.ColorType")
523
525
  def conv_color(
524
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
526
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
525
527
  ) -> UnboundField:
526
528
  kwargs.setdefault("validators", [])
527
529
  kwargs["validators"].append(ColorValidator())
@@ -530,7 +532,7 @@ class ModelConverter(ModelConverterBase):
530
532
  @converts("sqlalchemy_utils.types.choice.ChoiceType")
531
533
  @no_type_check
532
534
  def convert_choice_type(
533
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
535
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
534
536
  ) -> UnboundField:
535
537
  available_choices = []
536
538
  column = prop.columns[0]
@@ -559,32 +561,32 @@ class ModelConverter(ModelConverterBase):
559
561
 
560
562
  @converts("fastapi_storages.integrations.sqlalchemy.FileType")
561
563
  def conv_file(
562
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
564
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
563
565
  ) -> UnboundField:
564
566
  return FileField(**kwargs)
565
567
 
566
568
  @converts("fastapi_storages.integrations.sqlalchemy.ImageType")
567
569
  def conv_image(
568
- self, model: type, prop: ColumnProperty, kwargs: Dict[str, Any]
570
+ self, model: type, prop: ColumnProperty, kwargs: dict[str, Any]
569
571
  ) -> UnboundField:
570
572
  return FileField(**kwargs)
571
573
 
572
574
  @converts("ONETOONE")
573
575
  def conv_one_to_one(
574
- self, model: type, prop: RelationshipProperty, kwargs: Dict[str, Any]
576
+ self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
575
577
  ) -> UnboundField:
576
578
  kwargs["allow_blank"] = True
577
579
  return QuerySelectField(**kwargs)
578
580
 
579
581
  @converts("MANYTOONE")
580
582
  def conv_many_to_one(
581
- self, model: type, prop: RelationshipProperty, kwargs: Dict[str, Any]
583
+ self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
582
584
  ) -> UnboundField:
583
585
  return QuerySelectField(**kwargs)
584
586
 
585
587
  @converts("MANYTOMANY", "ONETOMANY")
586
588
  def conv_many_to_many(
587
- self, model: type, prop: RelationshipProperty, kwargs: Dict[str, Any]
589
+ self, model: type, prop: RelationshipProperty, kwargs: dict[str, Any]
588
590
  ) -> UnboundField:
589
591
  return QuerySelectMultipleField(**kwargs)
590
592
 
@@ -592,17 +594,17 @@ class ModelConverter(ModelConverterBase):
592
594
  async def get_model_form(
593
595
  model: type,
594
596
  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,
597
+ only: Sequence[str] | None = None,
598
+ exclude: Sequence[str] | None = None,
599
+ column_labels: dict[str, str] | None = None,
600
+ form_args: dict[str, dict[str, Any]] | None = None,
601
+ form_widget_args: dict[str, dict[str, Any]] | None = None,
602
+ form_class: type[Form] = Form,
603
+ form_overrides: dict[str, type[Field]] | None = None,
604
+ form_ajax_refs: dict[str, QueryAjaxModelLoader] | None = None,
603
605
  form_include_pk: bool = False,
604
- form_converter: Type[ModelConverterBase] = ModelConverter,
605
- ) -> Type[Form]:
606
+ form_converter: type[ModelConverterBase] = ModelConverter,
607
+ ) -> type[Form]:
606
608
  type_name = model.__name__ + "Form"
607
609
  converter = form_converter()
608
610
  mapper = sqlalchemy_inspect(model)
@@ -615,9 +617,12 @@ async def get_model_form(
615
617
  attributes = []
616
618
  names = only or mapper.attrs.keys()
617
619
  for name in names:
618
- if exclude and name in exclude:
620
+ attr = mapper.attrs[name]
621
+ if (exclude and name in exclude) or (
622
+ isinstance(attr, ColumnProperty) and isinstance(attr.expression, Label)
623
+ ):
619
624
  continue
620
- attributes.append((name, mapper.attrs[name]))
625
+ attributes.append((name, attr))
621
626
 
622
627
  field_dict = {}
623
628
  for name, attr in attributes:
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
@@ -172,7 +176,7 @@ class BaseView(BaseModelView):
172
176
 
173
177
  icon: ClassVar[str] = ""
174
178
  """Display icon for ModelAdmin in the sidebar.
175
- Currently only supports FontAwesome icons.
179
+ Currently only supports FontAwesome and Tabler icons.
176
180
  """
177
181
 
178
182
  category: ClassVar[str] = ""
@@ -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
@@ -44,6 +46,11 @@ class Pagination:
44
46
 
45
47
  raise RuntimeError("Next page not found.")
46
48
 
49
+ def resize(self, page_size: int) -> Pagination:
50
+ self.page = (self.page - 1) * self.page_size // page_size + 1
51
+ self.page_size = page_size
52
+ return self
53
+
47
54
  def add_pagination_urls(self, base_url: URL) -> None:
48
55
  # Previous pages
49
56
  for p in range(self.page - min(self.max_page_controls, 3), self.page):