sqladmin 0.18.0__tar.gz → 0.20.0__tar.gz

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.
Files changed (54) hide show
  1. {sqladmin-0.18.0 → sqladmin-0.20.0}/PKG-INFO +1 -1
  2. {sqladmin-0.18.0 → sqladmin-0.20.0}/pyproject.toml +1 -1
  3. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/__init__.py +1 -1
  4. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/ajax.py +8 -3
  5. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/application.py +14 -10
  6. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/forms.py +11 -3
  7. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/models.py +38 -5
  8. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/pagination.py +9 -0
  9. sqladmin-0.20.0/sqladmin/statics/css/tabler-icons.min.css +4 -0
  10. sqladmin-0.20.0/sqladmin/statics/css/tabler-icons.min.css.map +1 -0
  11. sqladmin-0.20.0/sqladmin/statics/webfonts/tabler-icons.woff2 +0 -0
  12. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/base.html +4 -0
  13. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/list.html +1 -1
  14. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/widgets.py +4 -1
  15. {sqladmin-0.18.0 → sqladmin-0.20.0}/.gitignore +0 -0
  16. {sqladmin-0.18.0 → sqladmin-0.20.0}/LICENSE.md +0 -0
  17. {sqladmin-0.18.0 → sqladmin-0.20.0}/README.md +0 -0
  18. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/_menu.py +0 -0
  19. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/_queries.py +0 -0
  20. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/_types.py +0 -0
  21. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/_validators.py +0 -0
  22. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/authentication.py +0 -0
  23. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/exceptions.py +0 -0
  24. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/fields.py +0 -0
  25. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/formatters.py +0 -0
  26. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/helpers.py +0 -0
  27. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/py.typed +0 -0
  28. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/css/flatpickr.min.css +0 -0
  29. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/css/fontawesome.min.css +0 -0
  30. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/css/main.css +0 -0
  31. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/css/select2.min.css +0 -0
  32. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/css/tabler.min.css +0 -0
  33. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/bootstrap.min.js +0 -0
  34. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/flatpickr.min.js +0 -0
  35. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/jquery.min.js +0 -0
  36. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/main.js +0 -0
  37. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/popper.min.js +0 -0
  38. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/select2.full.min.js +0 -0
  39. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/js/tabler.min.js +0 -0
  40. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/webfonts/fa-brands-400.woff2 +0 -0
  41. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/webfonts/fa-regular-400.woff2 +0 -0
  42. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/statics/webfonts/fa-solid-900.woff2 +0 -0
  43. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/_macros.html +0 -0
  44. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/create.html +0 -0
  45. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/details.html +0 -0
  46. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/edit.html +0 -0
  47. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/error.html +0 -0
  48. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/index.html +0 -0
  49. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/layout.html +0 -0
  50. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/login.html +0 -0
  51. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/modals/delete.html +0 -0
  52. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/modals/details_action_confirmation.html +0 -0
  53. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templates/sqladmin/modals/list_action_confirmation.html +0 -0
  54. {sqladmin-0.18.0 → sqladmin-0.20.0}/sqladmin/templating.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sqladmin
3
- Version: 0.18.0
3
+ Version: 0.20.0
4
4
  Summary: SQLAlchemy admin for FastAPI and Starlette
5
5
  Project-URL: Documentation, https://aminalaee.dev/sqladmin
6
6
  Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
@@ -95,7 +95,7 @@ dependencies = [
95
95
  dependencies = [
96
96
  "mkdocs-material==9.1.3",
97
97
  "mkdocs==1.4.2",
98
- "mkdocstrings[python]==0.20.0",
98
+ "mkdocstrings[python]==0.25.0",
99
99
  ]
100
100
 
101
101
  [tool.hatch.envs.test.scripts]
@@ -1,7 +1,7 @@
1
1
  from sqladmin.application import Admin, action, expose
2
2
  from sqladmin.models import BaseView, ModelView
3
3
 
4
- __version__ = "0.18.0"
4
+ __version__ = "0.20.0"
5
5
 
6
6
  __all__ = [
7
7
  "Admin",
@@ -26,6 +26,7 @@ class QueryAjaxModelLoader:
26
26
  self.model_admin = model_admin
27
27
  self.fields = options.get("fields", {})
28
28
  self.order_by = options.get("order_by")
29
+ self.limit = options.get("limit", DEFAULT_PAGE_SIZE)
29
30
 
30
31
  pks = get_primary_keys(self.model)
31
32
  self.pk = pks[0] if len(pks) == 1 else None
@@ -60,7 +61,7 @@ class QueryAjaxModelLoader:
60
61
 
61
62
  return {"id": str(get_object_identifier(model)), "text": str(model)}
62
63
 
63
- async def get_list(self, term: str, limit: int = DEFAULT_PAGE_SIZE) -> list[Any]:
64
+ async def get_list(self, term: str) -> list[Any]:
64
65
  stmt = select(self.model)
65
66
 
66
67
  # no type casting to string if a ColumnAssociationProxyInstance is given
@@ -71,9 +72,13 @@ class QueryAjaxModelLoader:
71
72
  stmt = stmt.filter(or_(*filters))
72
73
 
73
74
  if self.order_by:
74
- stmt = stmt.order_by(self.order_by)
75
+ if isinstance(self.order_by, list):
76
+ for o in self.order_by:
77
+ stmt = stmt.order_by(o)
78
+ else:
79
+ stmt = stmt.order_by(self.order_by)
75
80
 
76
- stmt = stmt.limit(limit)
81
+ stmt = stmt.limit(self.limit)
77
82
  result = await self.model_admin._run_query(stmt)
78
83
  return result
79
84
 
@@ -68,6 +68,7 @@ class BaseAdmin:
68
68
  base_url: str = "/admin",
69
69
  title: str = "Admin",
70
70
  logo_url: str | None = None,
71
+ favicon_url: str | None = None,
71
72
  templates_dir: str = "templates",
72
73
  middlewares: Sequence[Middleware] | None = None,
73
74
  authentication_backend: AuthenticationBackend | None = None,
@@ -78,6 +79,7 @@ class BaseAdmin:
78
79
  self.templates_dir = templates_dir
79
80
  self.title = title
80
81
  self.logo_url = logo_url
82
+ self.favicon_url = favicon_url
81
83
 
82
84
  if session_maker:
83
85
  self.session_maker = session_maker
@@ -340,6 +342,7 @@ class Admin(BaseAdminView):
340
342
  base_url: str = "/admin",
341
343
  title: str = "Admin",
342
344
  logo_url: str | None = None,
345
+ favicon_url: str | None = None,
343
346
  middlewares: Sequence[Middleware] | None = None,
344
347
  debug: bool = False,
345
348
  templates_dir: str = "templates",
@@ -353,6 +356,7 @@ class Admin(BaseAdminView):
353
356
  base_url: Base URL for Admin interface.
354
357
  title: Admin title.
355
358
  logo_url: URL of logo to be displayed instead of title.
359
+ favicon_url: URL of favicon to be displayed.
356
360
  """
357
361
 
358
362
  super().__init__(
@@ -362,6 +366,7 @@ class Admin(BaseAdminView):
362
366
  base_url=base_url,
363
367
  title=title,
364
368
  logo_url=logo_url,
369
+ favicon_url=favicon_url,
365
370
  templates_dir=templates_dir,
366
371
  middlewares=middlewares,
367
372
  authentication_backend=authentication_backend,
@@ -437,12 +442,13 @@ class Admin(BaseAdminView):
437
442
  pagination = await model_view.list(request)
438
443
  pagination.add_pagination_urls(request.url)
439
444
 
440
- if (
441
- pagination.page * pagination.page_size
442
- > pagination.count + pagination.page_size
443
- ):
444
- raise HTTPException(
445
- status_code=400, detail="Invalid page or pageSize parameter"
445
+ request_page = model_view.validate_page_number(
446
+ request.query_params.get("page"), 1
447
+ )
448
+
449
+ if request_page > pagination.page:
450
+ return RedirectResponse(
451
+ request.url.include_query_params(page=pagination.page), status_code=302
446
452
  )
447
453
 
448
454
  context = {"model_view": model_view, "pagination": pagination}
@@ -505,8 +511,7 @@ class Admin(BaseAdminView):
505
511
  identity = request.path_params["identity"]
506
512
  model_view = self._find_model_view(identity)
507
513
 
508
- Form = await model_view.scaffold_form()
509
- model_view._validate_form_class(model_view._form_create_rules, Form)
514
+ Form = await model_view.scaffold_form(model_view._form_create_rules)
510
515
  form_data = await self._handle_form_data(request)
511
516
  form = Form(form_data)
512
517
 
@@ -556,8 +561,7 @@ class Admin(BaseAdminView):
556
561
  if not model:
557
562
  raise HTTPException(status_code=404)
558
563
 
559
- Form = await model_view.scaffold_form()
560
- model_view._validate_form_class(model_view._form_edit_rules, Form)
564
+ Form = await model_view.scaffold_form(model_view._form_edit_rules)
561
565
  context = {
562
566
  "obj": model,
563
567
  "model_view": model_view,
@@ -18,7 +18,12 @@ from typing import (
18
18
  import anyio
19
19
  from sqlalchemy import Boolean, select
20
20
  from sqlalchemy import inspect as sqlalchemy_inspect
21
- 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
22
27
  from sqlalchemy.sql.schema import Column
23
28
  from wtforms import (
24
29
  BooleanField,
@@ -612,9 +617,12 @@ async def get_model_form(
612
617
  attributes = []
613
618
  names = only or mapper.attrs.keys()
614
619
  for name in names:
615
- 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
+ ):
616
624
  continue
617
- attributes.append((name, mapper.attrs[name]))
625
+ attributes.append((name, attr))
618
626
 
619
627
  field_dict = {}
620
628
  for name, attr in attributes:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  import time
4
5
  import warnings
5
6
  from enum import Enum
@@ -176,7 +177,7 @@ class BaseView(BaseModelView):
176
177
 
177
178
  icon: ClassVar[str] = ""
178
179
  """Display icon for ModelAdmin in the sidebar.
179
- Currently only supports FontAwesome icons.
180
+ Currently only supports FontAwesome and Tabler icons.
180
181
  """
181
182
 
182
183
  category: ClassVar[str] = ""
@@ -454,7 +455,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
454
455
  ```
455
456
  """
456
457
 
457
- export_types: ClassVar[List[str]] = ["csv"]
458
+ export_types: ClassVar[List[str]] = ["csv", "json"]
458
459
  """A list of available export filetypes.
459
460
  Currently only `csv` is supported.
460
461
  """
@@ -1015,10 +1016,11 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1015
1016
  By default do nothing.
1016
1017
  """
1017
1018
 
1018
- async def scaffold_form(self) -> Type[Form]:
1019
+ async def scaffold_form(self, rules: List[str] | None = None) -> Type[Form]:
1019
1020
  if self.form is not None:
1020
1021
  return self.form
1021
- return await get_model_form(
1022
+
1023
+ form = await get_model_form(
1022
1024
  model=self.model,
1023
1025
  session_maker=self.session_maker,
1024
1026
  only=self._form_prop_names,
@@ -1032,6 +1034,11 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1032
1034
  form_converter=self.form_converter,
1033
1035
  )
1034
1036
 
1037
+ if rules:
1038
+ self._validate_form_class(rules, form)
1039
+
1040
+ return form
1041
+
1035
1042
  def search_placeholder(self) -> str:
1036
1043
  """Return search placeholder text.
1037
1044
 
@@ -1152,7 +1159,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1152
1159
  ) -> StreamingResponse:
1153
1160
  if export_type == "csv":
1154
1161
  return await self._export_csv(data)
1155
- raise NotImplementedError("Only export_type='csv' is implemented.")
1162
+ elif export_type == "json":
1163
+ return await self._export_json(data)
1164
+ raise NotImplementedError("Only export_type='csv' or 'json' is implemented.")
1156
1165
 
1157
1166
  async def _export_csv(
1158
1167
  self,
@@ -1179,6 +1188,30 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1179
1188
  headers={"Content-Disposition": f"attachment;filename={filename}"},
1180
1189
  )
1181
1190
 
1191
+ async def _export_json(
1192
+ self,
1193
+ data: List[Any],
1194
+ ) -> StreamingResponse:
1195
+ async def generate() -> AsyncGenerator[str, None]:
1196
+ yield "["
1197
+ separator = "," if len(data) > 1 else ""
1198
+
1199
+ for row in data:
1200
+ row_dict = {
1201
+ name: await self.get_prop_value(row, name)
1202
+ for name in self._export_prop_names
1203
+ }
1204
+ yield json.dumps(row_dict) + separator
1205
+
1206
+ yield "]"
1207
+
1208
+ filename = secure_filename(self.get_export_name(export_type="json"))
1209
+ return StreamingResponse(
1210
+ content=generate(),
1211
+ media_type="application/json",
1212
+ headers={"Content-Disposition": f"attachment;filename={filename}"},
1213
+ )
1214
+
1182
1215
  def _refresh_form_rules_cache(self) -> None:
1183
1216
  if self.form_rules:
1184
1217
  self._form_create_rules = self.form_rules
@@ -46,6 +46,15 @@ class Pagination:
46
46
 
47
47
  raise RuntimeError("Next page not found.")
48
48
 
49
+ def __post_init__(self) -> None:
50
+ # Clamp page
51
+ self.page = min(self.page, max(1, self.count // self.page_size + 1))
52
+
53
+ def resize(self, page_size: int) -> Pagination:
54
+ self.page = (self.page - 1) * self.page_size // page_size + 1
55
+ self.page_size = page_size
56
+ return self
57
+
49
58
  def add_pagination_urls(self, base_url: URL) -> None:
50
59
  # Previous pages
51
60
  for p in range(self.page - min(self.max_page_controls, 3), self.page):