sqladmin 0.16.0__py3-none-any.whl → 0.17.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 CHANGED
@@ -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.16.0"
4
+ __version__ = "0.17.0"
5
5
 
6
6
  __all__ = [
7
7
  "Admin",
sqladmin/_menu.py CHANGED
@@ -42,7 +42,7 @@ class ItemMenu:
42
42
  class CategoryMenu(ItemMenu):
43
43
  def is_active(self, request: Request) -> bool:
44
44
  return any(
45
- c.is_active(request) and c.is_accessible(request) for c in self.children
45
+ c.is_visible(request) and c.is_accessible(request) for c in self.children
46
46
  )
47
47
 
48
48
  @property
sqladmin/_queries.py CHANGED
@@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, List
3
3
  import anyio
4
4
  from sqlalchemy import select
5
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
- from sqlalchemy.orm import Session, joinedload
6
+ from sqlalchemy.orm import Session, selectinload
7
7
  from sqlalchemy.sql.expression import Select, and_, or_
8
8
  from starlette.requests import Request
9
9
 
@@ -152,7 +152,7 @@ class Query:
152
152
  stmt = self.model_view._stmt_by_identifier(pk)
153
153
 
154
154
  for relation in self.model_view._form_relations:
155
- stmt = stmt.options(joinedload(relation))
155
+ stmt = stmt.options(selectinload(relation))
156
156
 
157
157
  async with self.model_view.session_maker(expire_on_commit=False) as session:
158
158
  result = await session.execute(stmt)
sqladmin/application.py CHANGED
@@ -5,6 +5,7 @@ from types import MethodType
5
5
  from typing import (
6
6
  TYPE_CHECKING,
7
7
  Any,
8
+ Awaitable,
8
9
  Callable,
9
10
  List,
10
11
  Optional,
@@ -15,14 +16,14 @@ from typing import (
15
16
  cast,
16
17
  no_type_check,
17
18
  )
18
- from urllib.parse import urljoin
19
+ from urllib.parse import parse_qsl, urljoin
19
20
 
20
21
  from jinja2 import ChoiceLoader, FileSystemLoader, PackageLoader
21
22
  from sqlalchemy.engine import Engine
22
23
  from sqlalchemy.ext.asyncio import AsyncSession
23
24
  from sqlalchemy.orm import Session, sessionmaker
24
25
  from starlette.applications import Starlette
25
- from starlette.datastructures import URL, FormData, UploadFile
26
+ from starlette.datastructures import URL, FormData, MultiDict, UploadFile
26
27
  from starlette.exceptions import HTTPException
27
28
  from starlette.middleware import Middleware
28
29
  from starlette.requests import Request
@@ -371,15 +372,17 @@ class Admin(BaseAdminView):
371
372
 
372
373
  statics = StaticFiles(packages=["sqladmin"])
373
374
 
374
- # def http_exception(request: Request, exc: Exception) -> Response:
375
- # assert isinstance(exc, HTTPException)
376
- # context = {
377
- # "status_code": exc.status_code,
378
- # "message": exc.detail,
379
- # }
380
- # return self.templates.TemplateResponse(
381
- # request, "error.html", context, status_code=exc.status_code
382
- # )
375
+ async def http_exception(
376
+ request: Request, exc: Exception
377
+ ) -> Union[Response, Awaitable[Response]]:
378
+ assert isinstance(exc, HTTPException)
379
+ context = {
380
+ "status_code": exc.status_code,
381
+ "message": exc.detail,
382
+ }
383
+ return await self.templates.TemplateResponse(
384
+ request, "sqladmin/error.html", context, status_code=exc.status_code
385
+ )
383
386
 
384
387
  routes = [
385
388
  Mount("/statics", app=statics, name="statics"),
@@ -417,7 +420,7 @@ class Admin(BaseAdminView):
417
420
  ]
418
421
 
419
422
  self.admin.router.routes = routes
420
- # self.admin.exception_handlers = {HTTPException: http_exception}
423
+ self.admin.exception_handlers = {HTTPException: http_exception}
421
424
  self.admin.debug = debug
422
425
  self.app.mount(base_url, app=self.admin, name="admin")
423
426
 
@@ -425,7 +428,7 @@ class Admin(BaseAdminView):
425
428
  async def index(self, request: Request) -> Response:
426
429
  """Index route which can be overridden to create dashboards."""
427
430
 
428
- return await self.templates.TemplateResponse(request, "index.html")
431
+ return await self.templates.TemplateResponse(request, "sqladmin/index.html")
429
432
 
430
433
  @login_required
431
434
  async def list(self, request: Request) -> Response:
@@ -437,6 +440,14 @@ class Admin(BaseAdminView):
437
440
  pagination = await model_view.list(request)
438
441
  pagination.add_pagination_urls(request.url)
439
442
 
443
+ if (
444
+ pagination.page * pagination.page_size
445
+ > pagination.count + pagination.page_size
446
+ ):
447
+ raise HTTPException(
448
+ status_code=400, detail="Invalid page or pageSize parameter"
449
+ )
450
+
440
451
  context = {"model_view": model_view, "pagination": pagination}
441
452
  return await self.templates.TemplateResponse(
442
453
  request, model_view.list_template, context
@@ -482,7 +493,11 @@ class Admin(BaseAdminView):
482
493
 
483
494
  await model_view.delete_model(request, pk)
484
495
 
485
- return Response(content=str(request.url_for("admin:list", identity=identity)))
496
+ referer_url = URL(request.headers.get("referer", ""))
497
+ referer_params = MultiDict(parse_qsl(referer_url.query))
498
+ url = URL(str(request.url_for("admin:list", identity=identity)))
499
+ url = url.include_query_params(**referer_params)
500
+ return Response(content=str(url))
486
501
 
487
502
  @login_required
488
503
  async def create(self, request: Request) -> Response:
@@ -539,7 +554,7 @@ class Admin(BaseAdminView):
539
554
  identity = request.path_params["identity"]
540
555
  model_view = self._find_model_view(identity)
541
556
 
542
- model = await model_view.get_object_for_edit(request.path_params["pk"])
557
+ model = await model_view.get_object_for_edit(request)
543
558
  if not model:
544
559
  raise HTTPException(status_code=404)
545
560
 
@@ -606,13 +621,13 @@ class Admin(BaseAdminView):
606
621
 
607
622
  context = {}
608
623
  if request.method == "GET":
609
- return await self.templates.TemplateResponse(request, "login.html")
624
+ return await self.templates.TemplateResponse(request, "sqladmin/login.html")
610
625
 
611
626
  ok = await self.authentication_backend.login(request)
612
627
  if not ok:
613
628
  context["error"] = "Invalid credentials."
614
629
  return await self.templates.TemplateResponse(
615
- request, "login.html", context, status_code=400
630
+ request, "sqladmin/login.html", context, status_code=400
616
631
  )
617
632
 
618
633
  return RedirectResponse(request.url_for("admin:index"), status_code=302)
sqladmin/forms.py CHANGED
@@ -169,7 +169,7 @@ class ModelConverterBase:
169
169
  if (column.primary_key or column.foreign_keys) and not form_include_pk:
170
170
  return None
171
171
 
172
- default = getattr(column, "default", None)
172
+ default = getattr(column, "default", None) or kwargs.get("default")
173
173
 
174
174
  if default is not None:
175
175
  # Only actually change default if it has an attribute named
sqladmin/helpers.py CHANGED
@@ -159,7 +159,7 @@ class _PseudoBuffer:
159
159
 
160
160
 
161
161
  def stream_to_csv(
162
- callback: Callable[[Writer], AsyncGenerator[T, None]]
162
+ callback: Callable[[Writer], AsyncGenerator[T, None]],
163
163
  ) -> Generator[T, None, None]:
164
164
  """Function that takes a callable (that yields from a CSV Writer), and
165
165
  provides it a writer that streams the output directly instead of
@@ -241,10 +241,13 @@ def get_direction(prop: MODEL_PROPERTY) -> str:
241
241
 
242
242
  def get_column_python_type(column: Column) -> type:
243
243
  try:
244
- if hasattr(column.type, "impl"):
245
- return column.type.impl.python_type
246
244
  return column.type.python_type
247
245
  except NotImplementedError:
246
+ if hasattr(column.type, "impl"):
247
+ try:
248
+ return column.type.impl.python_type
249
+ except NotImplementedError:
250
+ ...
248
251
  return str
249
252
 
250
253
 
sqladmin/models.py CHANGED
@@ -20,11 +20,12 @@ from urllib.parse import urlencode
20
20
  import anyio
21
21
  from sqlalchemy import Column, String, asc, cast, desc, func, inspect, or_
22
22
  from sqlalchemy.exc import NoInspectionAvailable
23
- from sqlalchemy.orm import joinedload, sessionmaker
23
+ from sqlalchemy.orm import selectinload, sessionmaker
24
24
  from sqlalchemy.orm.exc import DetachedInstanceError
25
25
  from sqlalchemy.sql.elements import ClauseElement
26
26
  from sqlalchemy.sql.expression import Select, select
27
27
  from starlette.datastructures import URL
28
+ from starlette.exceptions import HTTPException
28
29
  from starlette.requests import Request
29
30
  from starlette.responses import StreamingResponse
30
31
  from wtforms import Field, Form
@@ -316,7 +317,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
316
317
  ```
317
318
  """
318
319
 
319
- column_default_sort: ClassVar[Union[str, Tuple[str, bool], list]] = []
320
+ column_default_sort: ClassVar[Union[MODEL_ATTR, Tuple[MODEL_ATTR, bool], list]] = []
320
321
  """Default sort column if no sorting is applied.
321
322
 
322
323
  ???+ example
@@ -414,17 +415,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
414
415
  """
415
416
 
416
417
  # Templates
417
- list_template: ClassVar[str] = "list.html"
418
- """List view template. Default is `list.html`."""
418
+ list_template: ClassVar[str] = "sqladmin/list.html"
419
+ """List view template. Default is `sqladmin/list.html`."""
419
420
 
420
- create_template: ClassVar[str] = "create.html"
421
- """Create view template. Default is `create.html`."""
421
+ create_template: ClassVar[str] = "sqladmin/create.html"
422
+ """Create view template. Default is `sqladmin/create.html`."""
422
423
 
423
- details_template: ClassVar[str] = "details.html"
424
- """Details view template. Default is `details.html`."""
424
+ details_template: ClassVar[str] = "sqladmin/details.html"
425
+ """Details view template. Default is `sqladmin/details.html`."""
425
426
 
426
- edit_template: ClassVar[str] = "edit.html"
427
- """Edit view template. Default is `edit.html`."""
427
+ edit_template: ClassVar[str] = "sqladmin/edit.html"
428
+ """Edit view template. Default is `sqladmin/edit.html`."""
428
429
 
429
430
  # Export
430
431
  column_export_list: ClassVar[List[MODEL_ATTR]] = []
@@ -672,12 +673,10 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
672
673
  self._export_prop_names = self.get_export_columns()
673
674
 
674
675
  self._search_fields = [
675
- attr if isinstance(attr, str) else attr.key
676
- for attr in self.column_searchable_list
676
+ self._get_prop_name(attr) for attr in self.column_searchable_list
677
677
  ]
678
678
  self._sort_fields = [
679
- attr if isinstance(attr, str) else attr.key
680
- for attr in self.column_sortable_list
679
+ self._get_prop_name(attr) for attr in self.column_sortable_list
681
680
  ]
682
681
 
683
682
  self._form_ajax_refs = {}
@@ -727,6 +726,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
727
726
  pk=get_object_identifier(obj),
728
727
  )
729
728
 
729
+ def _get_prop_name(self, prop: MODEL_ATTR) -> str:
730
+ return prop if isinstance(prop, str) else prop.key
731
+
730
732
  def _get_default_sort(self) -> List[Tuple[str, bool]]:
731
733
  if self.column_default_sort:
732
734
  if isinstance(self.column_default_sort, list):
@@ -745,6 +747,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
745
747
 
746
748
  return value
747
749
 
750
+ def validate_page_number(self, number: Union[str, None], default: int) -> int:
751
+ if not number:
752
+ return default
753
+
754
+ try:
755
+ return int(number)
756
+ except ValueError:
757
+ raise HTTPException(
758
+ status_code=400, detail="Invalid page or pageSize parameter"
759
+ )
760
+
748
761
  async def count(self, request: Request, stmt: Optional[Select] = None) -> int:
749
762
  if stmt is None:
750
763
  stmt = self.count_query(request)
@@ -752,14 +765,14 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
752
765
  return rows[0]
753
766
 
754
767
  async def list(self, request: Request) -> Pagination:
755
- page = int(request.query_params.get("page", 1))
756
- page_size = int(request.query_params.get("pageSize", 0))
768
+ page = self.validate_page_number(request.query_params.get("page"), 1)
769
+ page_size = self.validate_page_number(request.query_params.get("pageSize"), 0)
757
770
  page_size = min(page_size or self.page_size, max(self.page_size_options))
758
771
  search = request.query_params.get("search", None)
759
772
 
760
773
  stmt = self.list_query(request)
761
774
  for relation in self._list_relations:
762
- stmt = stmt.options(joinedload(relation))
775
+ stmt = stmt.options(selectinload(relation))
763
776
 
764
777
  stmt = self.sort_query(stmt, request)
765
778
 
@@ -789,7 +802,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
789
802
  stmt = self.list_query(request).limit(limit)
790
803
 
791
804
  for relation in self._list_relations:
792
- stmt = stmt.options(joinedload(relation))
805
+ stmt = stmt.options(selectinload(relation))
793
806
 
794
807
  rows = await self._run_query(stmt)
795
808
  return rows
@@ -802,16 +815,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
802
815
  stmt = self._stmt_by_identifier(value)
803
816
 
804
817
  for relation in self._details_relations:
805
- stmt = stmt.options(joinedload(relation))
818
+ stmt = stmt.options(selectinload(relation))
806
819
 
807
820
  return await self._get_object_by_pk(stmt)
808
821
 
809
- async def get_object_for_edit(self, value: Any) -> Any:
810
- stmt = self._stmt_by_identifier(value)
811
-
812
- for relation in self._form_relations:
813
- stmt = stmt.options(joinedload(relation))
814
-
822
+ async def get_object_for_edit(self, request: Request) -> Any:
823
+ stmt = self.edit_form_query(request)
815
824
  return await self._get_object_by_pk(stmt)
816
825
 
817
826
  async def get_object_for_delete(self, value: Any) -> Any:
@@ -881,9 +890,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
881
890
  if include == "__all__":
882
891
  return self._prop_names
883
892
  elif include:
884
- return [item if isinstance(item, str) else item.key for item in include]
893
+ return [self._get_prop_name(item) for item in include]
885
894
  elif exclude:
886
- exclude = [item if isinstance(item, str) else item.key for item in exclude]
895
+ exclude = [self._get_prop_name(item) for item in exclude]
887
896
  return [prop for prop in self._prop_names if prop not in exclude]
888
897
  return defaults
889
898
 
@@ -956,10 +965,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
956
965
  ) -> Dict[str, Any]:
957
966
  pairs = {}
958
967
  for label, value in pair.items():
959
- if isinstance(label, str):
960
- pairs[label] = value
961
- else:
962
- pairs[label.key] = value
968
+ pairs[self._get_prop_name(label)] = value
963
969
  return pairs
964
970
 
965
971
  async def delete_model(self, request: Request, pk: Any) -> None:
@@ -1047,6 +1053,18 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1047
1053
 
1048
1054
  return select(self.model)
1049
1055
 
1056
+ def edit_form_query(self, request: Request) -> Select:
1057
+ """
1058
+ The SQLAlchemy select expression used for the edit form page which can be
1059
+ customized. By default it will select the object by primary key(s) without any
1060
+ additional filters.
1061
+ """
1062
+
1063
+ stmt = self._stmt_by_identifier(request.path_params["pk"])
1064
+ for relation in self._form_relations:
1065
+ stmt = stmt.options(selectinload(relation))
1066
+ return stmt
1067
+
1050
1068
  def count_query(self, request: Request) -> Select:
1051
1069
  """
1052
1070
  The SQLAlchemy select expression used for the count query
@@ -1075,7 +1093,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1075
1093
  for sort_field, is_desc in sort_fields:
1076
1094
  model = self.model
1077
1095
 
1078
- parts = sort_field.split(".")
1096
+ parts = self._get_prop_name(sort_field).split(".")
1079
1097
  for part in parts[:-1]:
1080
1098
  model = getattr(model, part).mapper.class_
1081
1099
  stmt = stmt.join(model)
@@ -1,3 +1,3 @@
1
1
  .table thead th {
2
2
  text-transform: none;
3
- }
3
+ }
@@ -1,7 +1,8 @@
1
1
  {% macro menu_category(menu, request) %}
2
- {% if menu.is_visible(request) and menu.is_accessible(request) %}
2
+ {% if menu.is_active(request) %}
3
3
  <li class="nav-item dropdown">
4
- <a class="nav-link dropdown-toggle {% if menu.is_active(request) %}active{% endif %}" data-bs-toggle="dropdown" href="#">
4
+ <a class="nav-link dropdown-toggle {% if menu.is_active(request) %}active{% endif %}" data-bs-toggle="dropdown"
5
+ href="#">
5
6
  <span class="nav-link-icon d-md-none d-lg-inline-block">
6
7
  {% if menu.icon %}<i class="{{ menu.icon }}"></i>{% endif %}
7
8
  </span>
@@ -11,12 +12,15 @@
11
12
  <div class="dropdown-menu-columns">
12
13
  <div class="dropdown-menu-column">
13
14
  {% for sub_menu in menu.children %}
14
- <a class="nav-link ps-lg-3 {% if sub_menu.is_active(request) %}active{% endif %}" href="{{ sub_menu.url(request) }}">
15
+ {% if sub_menu.is_visible(request) and sub_menu.is_accessible(request) %}
16
+ <a class="nav-link ps-lg-3 {% if sub_menu.is_active(request) %}active{% endif %}"
17
+ href="{{ sub_menu.url(request) }}">
15
18
  <span class="nav-link-icon d-md-none d-lg-inline-block">
16
19
  {% if sub_menu.icon %}<i class="{{ sub_menu.icon }}"></i>{% endif %}
17
20
  </span>
18
21
  <span class="nav-link-title">{{ sub_menu.display_name }}</span>
19
22
  </a>
23
+ {% endif %}
20
24
  {% endfor %}
21
25
  </div>
22
26
  </div>
@@ -40,12 +44,12 @@
40
44
 
41
45
  {% macro display_menu(menu, request) %}
42
46
  <div class="navbar-nav">
43
- {% for item in menu.items %}
44
- {% if item.type_ == "View" %}
45
- {{ menu_item(item, request) }}
46
- {% elif item.type_ == "Category" %}
47
- {{ menu_category(item, request) }}
48
- {% endif %}
49
- {% endfor %}
47
+ {% for item in menu.items %}
48
+ {% if item.type_ == "View" %}
49
+ {{ menu_item(item, request) }}
50
+ {% elif item.type_ == "Category" %}
51
+ {{ menu_category(item, request) }}
52
+ {% endif %}
53
+ {% endfor %}
50
54
  </div>
51
55
  {% endmacro %}
@@ -0,0 +1,35 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
7
+ <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/tabler.min.css') }}">
8
+ <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/fontawesome.min.css') }}">
9
+ <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/select2.min.css') }}">
10
+ <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/flatpickr.min.css') }}">
11
+ <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/main.css') }}">
12
+ {% block head %}
13
+ {% endblock %}
14
+ <title>{{ admin.title }}</title>
15
+ </head>
16
+
17
+ <body>
18
+ {% block body %}
19
+ <main>
20
+ {% block main %}
21
+ {% endblock %}
22
+ </main>
23
+ {% endblock %}
24
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/jquery.min.js') }}"></script>
25
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/tabler.min.js') }}"></script>
26
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/popper.min.js') }}"></script>
27
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/bootstrap.min.js') }}"></script>
28
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/select2.full.min.js') }}"></script>
29
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/flatpickr.min.js') }}"></script>
30
+ <script type="text/javascript" src="{{ url_for('admin:statics', path='js/main.js') }}"></script>
31
+ {% block tail %}
32
+ {% endblock %}
33
+ </body>
34
+
35
+ </html>
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends "sqladmin/layout.html" %}
2
2
  {% block content %}
3
3
  <div class="col-12">
4
4
  <div class="card">
@@ -8,6 +8,11 @@
8
8
  <div class="card-body border-bottom py-3">
9
9
  <form action="{{ url_for('admin:create', identity=model_view.identity) }}" method="POST"
10
10
  enctype="multipart/form-data">
11
+ <div class="row">
12
+ {% if error %}
13
+ <div class="alert alert-danger" role="alert">{{ error }}</div>
14
+ {% endif %}
15
+ </div>
11
16
  <fieldset class="form-fieldset">
12
17
  {% for field in form %}
13
18
  <div class="mb-3 form-group row">
@@ -21,14 +26,14 @@
21
26
  {% for error in field.errors %}
22
27
  <div class="invalid-feedback">{{ error }}</div>
23
28
  {% endfor %}
29
+ {% if field.description %}
30
+ <small class="text-muted">{{ field.description }}</small>
31
+ {% endif %}
24
32
  </div>
25
33
  </div>
26
34
  {% endfor %}
27
35
  </fieldset>
28
36
  <div class="row">
29
- {% if error %}
30
- <div class="alert alert-danger" role="alert">{{ error }}</div>
31
- {% endif %}
32
37
  <div class="col-md-2">
33
38
  <a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
34
39
  Cancel
@@ -46,4 +51,4 @@
46
51
  </div>
47
52
  </div>
48
53
  </div>
49
- {% endblock %}
54
+ {% endblock %}
@@ -1,12 +1,12 @@
1
- {% extends "layout.html" %}
1
+ {% extends "sqladmin/layout.html" %}
2
2
  {% block content %}
3
3
  <div class="col-12">
4
4
  <div class="card">
5
5
  <div class="card-header">
6
6
  <h3 class="card-title">
7
7
  {% for pk in model_view.pk_columns -%}
8
- {{ pk.name }}
9
- {%- if not loop.last %};{% endif -%}
8
+ {{ pk.name }}
9
+ {%- if not loop.last %};{% endif -%}
10
10
  {% endfor %}: {{ get_object_identifier(model) }}</h3>
11
11
  </div>
12
12
  <div class="card-body border-bottom py-3">
@@ -27,12 +27,13 @@
27
27
  {% if name in model_view._relation_names %}
28
28
  {% if is_list( value ) %}
29
29
  <td>
30
- {% for elem, formatted_elem in zip(value, formatted_value) %}
31
- <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
32
- {% endfor %}
30
+ {% for elem, formatted_elem in zip(value, formatted_value) %}
31
+ <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
32
+ {% endfor %}
33
33
  </td>
34
34
  {% else %}
35
- <td><a href="{{ model_view._url_for_details_with_prop(request, model, name) }}">{{ formatted_value }}</a></td>
35
+ <td><a href="{{ model_view._url_for_details_with_prop(request, model, name) }}">{{ formatted_value }}</a>
36
+ </td>
36
37
  {% endif %}
37
38
  {% else %}
38
39
  <td>{{ formatted_value }}</td>
@@ -51,7 +52,9 @@
51
52
  </div>
52
53
  {% if model_view.can_delete %}
53
54
  <div class="col-md-1">
54
- <a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(model) }}" data-url="{{ model_view._url_for_delete(request, model) }}" data-bs-toggle="modal" data-bs-target="#modal-delete" class="btn btn-danger">
55
+ <a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(model) }}"
56
+ data-url="{{ model_view._url_for_delete(request, model) }}" data-bs-toggle="modal"
57
+ data-bs-target="#modal-delete" class="btn btn-danger">
55
58
  Delete
56
59
  </a>
57
60
  </div>
@@ -66,11 +69,13 @@
66
69
  {% for custom_action,label in model_view._custom_actions_in_detail.items() %}
67
70
  <div class="col-md-1">
68
71
  {% if custom_action in model_view._custom_actions_confirmation %}
69
- <a href="#" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#modal-confirmation-{{ custom_action }}">
72
+ <a href="#" class="btn btn-secondary" data-bs-toggle="modal"
73
+ data-bs-target="#modal-confirmation-{{ custom_action }}">
70
74
  {{ label }}
71
75
  </a>
72
76
  {% else %}
73
- <a href="{{ model_view._url_for_action(request, custom_action) }}?pks={{ get_object_identifier(model) }}" class="btn btn-secondary">
77
+ <a href="{{ model_view._url_for_action(request, custom_action) }}?pks={{ get_object_identifier(model) }}"
78
+ class="btn btn-secondary">
74
79
  {{ label }}
75
80
  </a>
76
81
  {% endif %}
@@ -82,15 +87,16 @@
82
87
  </div>
83
88
  </div>
84
89
  {% if model_view.can_delete %}
85
- {% include 'modals/delete.html' %}
90
+ {% include 'sqladmin/modals/delete.html' %}
86
91
  {% endif %}
87
92
 
88
93
  {% for custom_action in model_view._custom_actions_in_detail %}
89
94
  {% if custom_action in model_view._custom_actions_confirmation %}
90
- {% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action, url=model_view._url_for_action(request, custom_action) + '?pks=' + (get_object_identifier(model) | string) %}
91
- {% include 'modals/details_action_confirmation.html' %}
95
+ {% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action,
96
+ url=model_view._url_for_action(request, custom_action) + '?pks=' + (get_object_identifier(model) | string) %}
97
+ {% include 'sqladmin/modals/details_action_confirmation.html' %}
92
98
  {% endwith %}
93
99
  {% endif %}
94
100
  {% endfor %}
95
101
 
96
- {% endblock %}
102
+ {% endblock %}
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends "sqladmin/layout.html" %}
2
2
  {% block content %}
3
3
  <div class="col-12">
4
4
  <div class="card">
@@ -6,7 +6,13 @@
6
6
  <h3 class="card-title">Edit {{ model_view.name }}</h3>
7
7
  </div>
8
8
  <div class="card-body border-bottom py-3">
9
- <form action="{{ model_view._build_url_for('admin:edit', request, obj) }}" method="POST" enctype="multipart/form-data">
9
+ <form action="{{ model_view._build_url_for('admin:edit', request, obj) }}" method="POST"
10
+ enctype="multipart/form-data">
11
+ <div class="row">
12
+ {% if error %}
13
+ <div class="alert alert-danger" role="alert">{{ error }}</div>
14
+ {% endif %}
15
+ </div>
10
16
  <fieldset class="form-fieldset">
11
17
  {% for field in form %}
12
18
  <div class="mb-3 form-group row">
@@ -20,14 +26,14 @@
20
26
  {% for error in field.errors %}
21
27
  <div class="invalid-feedback">{{ error }}</div>
22
28
  {% endfor %}
29
+ {% if field.description %}
30
+ <small class="text-muted">{{ field.description }}</small>
31
+ {% endif %}
23
32
  </div>
24
33
  </div>
25
34
  {% endfor %}
26
35
  </fieldset>
27
36
  <div class="row">
28
- {% if error %}
29
- <div class="alert alert-danger" role="alert">{{ error }}</div>
30
- {% endif %}
31
37
  <div class="col-md-2">
32
38
  <a href="{{ url_for('admin:list', identity=model_view.identity) }}" class="btn">
33
39
  Cancel
@@ -37,10 +43,12 @@
37
43
  <div class="btn-group flex-wrap" data-toggle="buttons">
38
44
  <input type="submit" name="save" value="Save" class="btn">
39
45
  <input type="submit" name="save" value="Save and continue editing" class="btn">
40
- {% if model_view.save_as %}
41
- <input type="submit" name="save" value="Save as new" class="btn">
42
- {% else %}
43
- <input type="submit" name="save" value="Save and add another" class="btn">
46
+ {% 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 %}
44
52
  {% endif %}
45
53
  </div>
46
54
  </div>
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends "sqladmin/layout.html" %}
2
2
  {% block body %}
3
3
  <div class="page page-center">
4
4
  <div class="container-tight py-4">
@@ -8,4 +8,4 @@
8
8
  </div>
9
9
  </div>
10
10
  </div>
11
- {% endblock %}
11
+ {% endblock %}
@@ -0,0 +1,3 @@
1
+ {% extends "sqladmin/layout.html" %}
2
+ {% block content %}
3
+ {% endblock %}
@@ -1,5 +1,5 @@
1
- {% extends "base.html" %}
2
- {% from '_macros.html' import display_menu %}
1
+ {% extends "sqladmin/base.html" %}
2
+ {% from 'sqladmin/_macros.html' import display_menu %}
3
3
  {% block body %}
4
4
  <div class="wrapper">
5
5
  <aside class="navbar navbar-expand-lg navbar-vertical navbar-expand-md navbar-dark">
@@ -19,7 +19,7 @@
19
19
  <span class="navbar-toggler-icon"></span>
20
20
  </button>
21
21
  <div class="collapse navbar-collapse" id="navbarSupportedContent">
22
- {{ display_menu(admin._menu, request) }}
22
+ {{ display_menu(admin._menu, request) }}
23
23
  </div>
24
24
  </nav>
25
25
  {% if admin.authentication_backend %}
@@ -52,4 +52,4 @@
52
52
  </div>
53
53
  </div>
54
54
  </div>
55
- {% endblock %}
55
+ {% endblock %}
@@ -1,4 +1,4 @@
1
- {% extends "layout.html" %}
1
+ {% extends "sqladmin/layout.html" %}
2
2
  {% block content %}
3
3
  <div class="col-12">
4
4
  <div class="card">
@@ -8,18 +8,22 @@
8
8
  {% if model_view.can_export %}
9
9
  {% if model_view.export_types | length > 1 %}
10
10
  <div class="ms-3 d-inline-block dropdown">
11
- <a href="#" class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown" aria-expanded="false">
11
+ <a href="#" class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown"
12
+ aria-expanded="false">
12
13
  Export
13
14
  </a>
14
15
  <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
15
16
  {% for export_type in model_view.export_types %}
16
- <li><a class="dropdown-item" href="{{ url_for('admin:export', identity=model_view.identity, export_type=export_type) }}">{{ export_type | upper }}</a></li>
17
+ <li><a class="dropdown-item"
18
+ href="{{ url_for('admin:export', identity=model_view.identity, export_type=export_type) }}">{{
19
+ export_type | upper }}</a></li>
17
20
  {% endfor %}
18
21
  </ul>
19
22
  </div>
20
23
  {% elif model_view.export_types | length == 1 %}
21
24
  <div class="ms-3 d-inline-block">
22
- <a href="{{ url_for('admin:export', identity=model_view.identity, export_type=model_view.export_types[0]) }}" class="btn btn-secondary">
25
+ <a href="{{ url_for('admin:export', identity=model_view.identity, export_type=model_view.export_types[0]) }}"
26
+ class="btn btn-secondary">
23
27
  Export
24
28
  </a>
25
29
  </div>
@@ -37,23 +41,27 @@
37
41
  <div class="card-body border-bottom py-3">
38
42
  <div class="d-flex justify-content-between">
39
43
  <div class="dropdown col-4">
40
- <button {% if not model_view.can_delete and not model_view._custom_actions_in_list %} disabled {% endif %} class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
44
+ <button {% if not model_view.can_delete and not model_view._custom_actions_in_list %} disabled {% endif %}
45
+ class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
46
+ aria-haspopup="true" aria-expanded="false">
41
47
  Actions
42
48
  </button>
43
49
  {% if model_view.can_delete or model_view._custom_actions_in_list %}
44
50
  <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
45
51
  {% if model_view.can_delete %}
46
- <a class="dropdown-item" id="action-delete" href="#" data-name="{{ model_view.name }}" data-url="{{ url_for('admin:delete', identity=model_view.identity) }}" data-bs-toggle="modal" data-bs-target="#modal-delete">Delete selected items</a>
52
+ <a class="dropdown-item" id="action-delete" href="#" data-name="{{ model_view.name }}"
53
+ data-url="{{ url_for('admin:delete', identity=model_view.identity) }}" data-bs-toggle="modal"
54
+ data-bs-target="#modal-delete">Delete selected items</a>
47
55
  {% endif %}
48
56
  {% for custom_action, label in model_view._custom_actions_in_list.items() %}
49
57
  {% if custom_action in model_view._custom_actions_confirmation %}
50
- <a class="dropdown-item" id="action-customconfirm-{{ custom_action }}" href="#"
51
- data-bs-toggle="modal" data-bs-target="#modal-confirmation-{{ custom_action }}">
58
+ <a class="dropdown-item" id="action-customconfirm-{{ custom_action }}" href="#" data-bs-toggle="modal"
59
+ data-bs-target="#modal-confirmation-{{ custom_action }}">
52
60
  {{ label }}
53
61
  </a>
54
62
  {% else %}
55
63
  <a class="dropdown-item" id="action-custom-{{ custom_action }}" href="#"
56
- data-url="{{ model_view._url_for_action(request, custom_action) }}">
64
+ data-url="{{ model_view._url_for_action(request, custom_action) }}">
57
65
  {{ label }}
58
66
  </a>
59
67
  {% endif %}
@@ -64,9 +72,12 @@
64
72
  {% if model_view.column_searchable_list %}
65
73
  <div class="col-md-4 text-muted">
66
74
  <div class="input-group">
67
- <input id="search-input" type="text" class="form-control" placeholder="Search: {{ model_view.search_placeholder() }}" value="{{ request.query_params.get('search', '') }}">
75
+ <input id="search-input" type="text" class="form-control"
76
+ placeholder="Search: {{ model_view.search_placeholder() }}"
77
+ value="{{ request.query_params.get('search', '') }}">
68
78
  <button id="search-button" class="btn" type="button">Search</button>
69
- <button id="search-reset" class="btn" type="button" {% if not request.query_params.get('search') %}disabled{% endif %}><i class="fa-solid fa-times"></i></button>
79
+ <button id="search-reset" class="btn" type="button" {% if not request.query_params.get('search')
80
+ %}disabled{% endif %}><i class="fa-solid fa-times"></i></button>
70
81
  </div>
71
82
  </div>
72
83
  {% endif %}
@@ -76,16 +87,19 @@
76
87
  <table class="table card-table table-vcenter text-nowrap">
77
88
  <thead>
78
89
  <tr>
79
- <th class="w-1"><input class="form-check-input m-0 align-middle" type="checkbox" aria-label="Select all" id="select-all"></th>
90
+ <th class="w-1"><input class="form-check-input m-0 align-middle" type="checkbox" aria-label="Select all"
91
+ id="select-all"></th>
80
92
  <th class="w-1"></th>
81
93
  {% for name in model_view._list_prop_names %}
82
94
  {% set label = model_view._column_labels.get(name, name) %}
83
95
  <th>
84
96
  {% if name in model_view._sort_fields %}
85
97
  {% if request.query_params.get("sortBy") == name and request.query_params.get("sort") == "asc" %}
86
- <a href="{{ request.url.include_query_params(sort='desc') }}"><i class="fa-solid fa-arrow-down"></i> {{ label }}</a>
98
+ <a href="{{ request.url.include_query_params(sort='desc') }}"><i class="fa-solid fa-arrow-up"></i> {{
99
+ label }}</a>
87
100
  {% elif request.query_params.get("sortBy") == name and request.query_params.get("sort") == "desc" %}
88
- <a href="{{ request.url.include_query_params(sort='asc') }}"><i class="fa-solid fa-arrow-up"></i> {{ label }}</a>
101
+ <a href="{{ request.url.include_query_params(sort='asc') }}"><i class="fa-solid fa-arrow-down"></i> {{ label
102
+ }}</a>
89
103
  {% else %}
90
104
  <a href="{{ request.url.include_query_params(sortBy=name, sort='asc') }}">{{ label }}</a>
91
105
  {% endif %}
@@ -105,17 +119,21 @@
105
119
  </td>
106
120
  <td class="text-end">
107
121
  {% if model_view.can_view_details %}
108
- <a href="{{ model_view._build_url_for('admin:details', request, row) }}" data-bs-toggle="tooltip" data-bs-placement="top" title="View">
122
+ <a href="{{ model_view._build_url_for('admin:details', request, row) }}" data-bs-toggle="tooltip"
123
+ data-bs-placement="top" title="View">
109
124
  <span class="me-1"><i class="fa-solid fa-eye"></i></span>
110
125
  </a>
111
126
  {% endif %}
112
127
  {% if model_view.can_edit %}
113
- <a href="{{ model_view._build_url_for('admin:edit', request, row) }}" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit">
128
+ <a href="{{ model_view._build_url_for('admin:edit', request, row) }}" data-bs-toggle="tooltip"
129
+ data-bs-placement="top" title="Edit">
114
130
  <span class="me-1"><i class="fa-solid fa-pen-to-square"></i></span>
115
131
  </a>
116
132
  {% endif %}
117
133
  {% if model_view.can_delete %}
118
- <a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(row) }}" data-url="{{ model_view._url_for_delete(request, row) }}" data-bs-toggle="modal" data-bs-target="#modal-delete" title="Delete">
134
+ <a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(row) }}"
135
+ data-url="{{ model_view._url_for_delete(request, row) }}" data-bs-toggle="modal"
136
+ data-bs-target="#modal-delete" title="Delete">
119
137
  <span class="me-1"><i class="fa-solid fa-trash"></i></span>
120
138
  </a>
121
139
  {% endif %}
@@ -125,9 +143,9 @@
125
143
  {% if name in model_view._relation_names %}
126
144
  {% if is_list( value ) %}
127
145
  <td>
128
- {% for elem, formatted_elem in zip(value, formatted_value) %}
129
- <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
130
- {% endfor %}
146
+ {% for elem, formatted_elem in zip(value, formatted_value) %}
147
+ <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
148
+ {% endfor %}
131
149
  </td>
132
150
  {% else %}
133
151
  <td><a href="{{ model_view._url_for_details_with_prop(request, row, name) }}">{{ formatted_value }}</a></td>
@@ -142,35 +160,40 @@
142
160
  </table>
143
161
  </div>
144
162
  <div class="card-footer d-flex justify-content-between align-items-center gap-2">
145
- <p class="m-0 text-muted">Showing <span>{{ ((pagination.page - 1) * pagination.page_size) + 1 }}</span> to <span>{{ min(pagination.page * pagination.page_size, pagination.count) }}</span> of <span>{{ pagination.count }}</span> items</p>
163
+ <p class="m-0 text-muted">Showing <span>{{ ((pagination.page - 1) * pagination.page_size) + 1 }}</span> to
164
+ <span>{{ min(pagination.page * pagination.page_size, pagination.count) }}</span> of <span>{{ pagination.count
165
+ }}</span> items
166
+ </p>
146
167
  <ul class="pagination m-0 ms-auto">
147
168
  <li class="page-item {% if not pagination.has_previous %}disabled{% endif %}">
148
169
  {% if pagination.has_previous %}
149
170
  <a class="page-link" href="{{ pagination.previous_page.url }}">
150
- {% else %}
151
- <a class="page-link" href="#">
152
- {% endif %}
153
- <i class="fa-solid fa-chevron-left"></i>
154
- prev
155
- </a>
171
+ {% else %}
172
+ <a class="page-link" href="#">
173
+ {% endif %}
174
+ <i class="fa-solid fa-chevron-left"></i>
175
+ prev
176
+ </a>
156
177
  </li>
157
178
  {% for page_control in pagination.page_controls %}
158
- <li class="page-item {% if page_control.number == pagination.page %}active{% endif %}"><a class="page-link" href="{{ page_control.url }}">{{ page_control.number }}</a></li>
179
+ <li class="page-item {% if page_control.number == pagination.page %}active{% endif %}"><a class="page-link"
180
+ href="{{ page_control.url }}">{{ page_control.number }}</a></li>
159
181
  {% endfor %}
160
182
  <li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
161
183
  {% if pagination.has_next %}
162
184
  <a class="page-link" href="{{ pagination.next_page.url }}">
163
- {% else %}
164
- <a class="page-link" href="#">
165
- {% endif %}
166
- next
167
- <i class="fa-solid fa-chevron-right"></i>
168
- </a>
185
+ {% else %}
186
+ <a class="page-link" href="#">
187
+ {% endif %}
188
+ next
189
+ <i class="fa-solid fa-chevron-right"></i>
190
+ </a>
169
191
  </li>
170
192
  </ul>
171
193
  <div class="dropdown text-muted">
172
194
  Show
173
- <a href="#" class="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
195
+ <a href="#" class="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
196
+ aria-expanded="false">
174
197
  {{ request.query_params.get("pageSize") or model_view.page_size }} / Page
175
198
  </a>
176
199
  <div class="dropdown-menu">
@@ -184,13 +207,14 @@
184
207
  </div>
185
208
  </div>
186
209
  {% if model_view.can_delete %}
187
- {% include 'modals/delete.html' %}
210
+ {% include 'sqladmin/modals/delete.html' %}
188
211
  {% endif %}
189
212
 
190
213
  {% for custom_action in model_view._custom_actions_in_list %}
191
214
  {% if custom_action in model_view._custom_actions_confirmation %}
192
- {% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action, url=model_view._url_for_action(request, custom_action) %}
193
- {% include 'modals/list_action_confirmation.html' %}
215
+ {% with confirmation_message = model_view._custom_actions_confirmation[custom_action], custom_action=custom_action,
216
+ url=model_view._url_for_action(request, custom_action) %}
217
+ {% include 'sqladmin/modals/list_action_confirmation.html' %}
194
218
  {% endwith %}
195
219
  {% endif %}
196
220
  {% endfor %}
@@ -1,4 +1,4 @@
1
- {% extends "base.html" %}
1
+ {% extends "sqladmin/base.html" %}
2
2
  {% block body %}
3
3
  <div class="d-flex align-items-center justify-content-center vh-100">
4
4
  <form class="Fcol-lg-6 col-md-6 card card-md" action="{{ url_for('admin:login') }}" method="POST" autocomplete="off">
@@ -34,4 +34,4 @@
34
34
  </div>
35
35
  </form>
36
36
  </div>
37
- {% endblock %}
37
+ {% endblock %}
@@ -26,4 +26,4 @@
26
26
  </div>
27
27
  </div>
28
28
  </div>
29
- </div>
29
+ </div>
@@ -1,4 +1,5 @@
1
- <div class="modal modal-blur fade" id="modal-confirmation-{{ custom_action }}" tabindex="-1" role="dialog" aria-hidden="true">
1
+ <div class="modal modal-blur fade" id="modal-confirmation-{{ custom_action }}" tabindex="-1" role="dialog"
2
+ aria-hidden="true">
2
3
  <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
3
4
  <div class="modal-content">
4
5
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
@@ -25,4 +26,4 @@
25
26
  </div>
26
27
  </div>
27
28
  </div>
28
- </div>
29
+ </div>
@@ -1,4 +1,5 @@
1
- <div class="modal modal-blur fade" id="modal-confirmation-{{ custom_action }}" tabindex="-1" role="dialog" aria-hidden="true">
1
+ <div class="modal modal-blur fade" id="modal-confirmation-{{ custom_action }}" tabindex="-1" role="dialog"
2
+ aria-hidden="true">
2
3
  <div class="modal-dialog modal-sm modal-dialog-centered" role="document">
3
4
  <div class="modal-content">
4
5
  <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
@@ -25,4 +26,4 @@
25
26
  </div>
26
27
  </div>
27
28
  </div>
28
- </div>
29
+ </div>
sqladmin/widgets.py CHANGED
@@ -75,15 +75,23 @@ class FileInputWidget(widgets.FileInput):
75
75
  """
76
76
 
77
77
  def __call__(self, field: Field, **kwargs: Any) -> str:
78
- file_input = super().__call__(field, **kwargs)
79
- checkbox_id = f"{field.id}_checkbox"
80
- checkbox_label = Markup(
81
- f'<label class="form-check-label" for="{checkbox_id}">Clear</label>'
82
- )
83
- checkbox_input = Markup(
84
- f'<input class="form-check-input" type="checkbox" id="{checkbox_id}" name="{checkbox_id}">' # noqa: E501
85
- )
86
- checkbox = Markup(
87
- f'<div class="form-check">{checkbox_input}{checkbox_label}</div>'
88
- )
89
- return file_input + checkbox
78
+ if not field.flags.required:
79
+ checkbox_id = f"{field.id}_checkbox"
80
+ checkbox_label = Markup(
81
+ f'<label class="form-check-label" for="{checkbox_id}">Clear</label>'
82
+ )
83
+ checkbox_input = Markup(
84
+ f'<input class="form-check-input" type="checkbox" id="{checkbox_id}" name="{checkbox_id}">' # noqa: E501
85
+ )
86
+ checkbox = Markup(
87
+ f'<div class="form-check">{checkbox_input}{checkbox_label}</div>'
88
+ )
89
+ else:
90
+ checkbox = Markup()
91
+
92
+ if field.data:
93
+ current_value = Markup(f"<p>Currently: {field.data}</p>")
94
+ field.flags.required = False
95
+ return current_value + checkbox + super().__call__(field, **kwargs)
96
+ else:
97
+ return super().__call__(field, **kwargs)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: sqladmin
3
- Version: 0.16.0
3
+ Version: 0.17.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
@@ -82,9 +82,16 @@ Main features include:
82
82
 
83
83
  ## Installation
84
84
 
85
+ Install using `pip`:
86
+
85
87
  ```shell
86
88
  $ pip install sqladmin
87
- $ pip install sqladmin[full]
89
+ ```
90
+
91
+ This will install the full version of sqladmin with optional dependencies:
92
+
93
+ ```shell
94
+ $ pip install "sqladmin[full]"
88
95
  ```
89
96
 
90
97
  ---
@@ -0,0 +1,50 @@
1
+ sqladmin/__init__.py,sha256=p1XjIDLwQG28ayOpu2i60UG2NjL-mugVbHIaoARKpRo,216
2
+ sqladmin/_menu.py,sha256=srm7SUSN4RiP1UQ9y5uHcTX9sVVcvEV-6z5sfvLq-fc,2623
3
+ sqladmin/_queries.py,sha256=ucZpjUZgEw8BVY4Ln6yHzgpOWUsadBI8-Cvy6x3LZe4,9673
4
+ sqladmin/_types.py,sha256=3Zs0aPb14OS-9leahKxxzFopnIOiNftPZwdUmFDBKog,347
5
+ sqladmin/_validators.py,sha256=w0siGhZQq4MD__lu9Edua9DgMOoKET_kk-alpARFHIM,1604
6
+ sqladmin/ajax.py,sha256=oerBfp90Bow0CsDonBdurCl8XfzNvVWprzm8C3PJmlI,2552
7
+ sqladmin/application.py,sha256=yhh6VXMlkKf-xVN1HTSthco7ELBT3QgMohzuz-7TXqc,27206
8
+ sqladmin/authentication.py,sha256=R4ad-8A5zWZ0vInK0pRj_oWirrQgWEY0Ng36j_NMC1U,2459
9
+ sqladmin/exceptions.py,sha256=6-E8m7rbWE3A7hNaSmB6CVqFzkEuwUpmU5AdGbouPCw,154
10
+ sqladmin/fields.py,sha256=5CFibyLYliD48Y6p2AKzmtdQeTHxxcn5_7LieQmBpGY,11752
11
+ sqladmin/formatters.py,sha256=K06la0mm9-Bs5UA9L6KGJC_X_lV3UHdJ3ENI6j9j2Zg,480
12
+ sqladmin/forms.py,sha256=cFUSS0QBX_g3B5hCuDN7ckD0Hz2l-HWPqqgbmE4lqKw,21429
13
+ sqladmin/helpers.py,sha256=wkMn0MfnRw4mC45rcusz4bV7Frb82kQvD04qTVFzc_4,8627
14
+ sqladmin/models.py,sha256=Hx7oUvLhYaAvts9KBWCtuP3kSCguadb70uUtmvJp8J8,37530
15
+ sqladmin/pagination.py,sha256=AmaZ5xNgPdBqTURYdOltz5IcSz8pY4qGQsr8evpUtHk,2256
16
+ sqladmin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ sqladmin/templating.py,sha256=uIOfv7AFS4i_pErxdj5bcKDKxXz28BLlzCkLVFHqZsI,2244
18
+ sqladmin/widgets.py,sha256=C_9GsqrnOiFkEHo6tBIkep9g4OCzMLeiB56lZM7bgqs,3110
19
+ sqladmin/statics/css/flatpickr.min.css,sha256=GzSkJVLJbxDk36qko2cnawOGiqz_Y8GsQv_jMTUrx1Q,16166
20
+ sqladmin/statics/css/fontawesome.min.css,sha256=CTSx_A06dm1B063156EVh15m6Y67pAjZZaQc89LLSrU,102217
21
+ sqladmin/statics/css/main.css,sha256=XziO3HgOtX6JmGb0lHSNzktBjevR_3XcmqmkV7E9jpc,45
22
+ sqladmin/statics/css/select2.min.css,sha256=FdatTf20PQr_rWg-cAKfl6j4_IY3oohFAJ7gVC3M34E,14966
23
+ sqladmin/statics/css/tabler.min.css,sha256=9Mi6oBMfUzSQcvXKNXI080TGtYClqjusVxxlrM-12EQ,498576
24
+ sqladmin/statics/js/bootstrap.min.js,sha256=5dHuQEbO64HT5DMJ0FO0I7hwGOYMTPDdjufF0-npBGU,62399
25
+ sqladmin/statics/js/flatpickr.min.js,sha256=P3cmJKYotkCthfbkx0ci27L0y3mp2ZlCJ-RrV3oOsDE,58060
26
+ sqladmin/statics/js/jquery.min.js,sha256=_xUj-3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej_m4,89501
27
+ sqladmin/statics/js/main.js,sha256=xMh4ruSMIfdTxRWmXiBKdpP-5yLXxBdjadrTtVtYq0A,4068
28
+ sqladmin/statics/js/popper.min.js,sha256=wZjZsjDhPsJ8oPi__hLJzWmP9Fj30Vm0c_9jALIiyjE,19167
29
+ sqladmin/statics/js/select2.full.min.js,sha256=XG_auAy4aieWldzMImofrFDiySK-pwJC7aoo9St7rS0,79212
30
+ sqladmin/statics/js/tabler.min.js,sha256=kuHV8gS0R5FupAkK6iExYXHKc5ZFbnSRYtVSorjvl0M,146912
31
+ sqladmin/statics/webfonts/fa-brands-400.woff2,sha256=-q5vwKqUzFveUHZkfIF6IyBglqHL7aENHG89idYWPtE,109808
32
+ sqladmin/statics/webfonts/fa-regular-400.woff2,sha256=jn5eobFfYqsU29QXaOj7zSHMhZpOpdqBJFfucUKZ-zU,24948
33
+ sqladmin/statics/webfonts/fa-solid-900.woff2,sha256=cVKmkz7j1pDsKvPQnanXAXI9Fqo0EKbYDyj_iGbzuIA,150124
34
+ sqladmin/templates/sqladmin/_macros.html,sha256=nnUwiUAWBSkGD645U2VcuMkRoN3tT2X8RxFaCFvx9ww,1944
35
+ sqladmin/templates/sqladmin/base.html,sha256=MjcbhwWOEhwamvGevnH3FPlrjirvErhGhOdslpMLGrU,1592
36
+ sqladmin/templates/sqladmin/create.html,sha256=YvsGQ4lCVXgDXDsUvOcpGPOIx9KEsrYpBGYQ4CO3jwk,1970
37
+ sqladmin/templates/sqladmin/details.html,sha256=RuWdlsZw5m_gm24Tdn3APNlfKtTHfYw7BnZB0_Tj6Hw,3932
38
+ sqladmin/templates/sqladmin/edit.html,sha256=ZA_Y7SlMSgbUU5PET7JzvhIRFMTVw8BJ_46HvF-hhDY,2226
39
+ sqladmin/templates/sqladmin/error.html,sha256=gb-172SMuQKncv0QE8DQdQXeM-fw7oXC0LPLO3ia0IM,290
40
+ sqladmin/templates/sqladmin/index.html,sha256=vh_IhhYmHPOkdZNrXSEc4e9gXXeZ-nsRBCsJQ_mC7YI,71
41
+ sqladmin/templates/sqladmin/layout.html,sha256=iBIhypkXp6O3hAHDdMNc4pWd9yxt5mQy7o2lBQD-6Ec,1994
42
+ sqladmin/templates/sqladmin/list.html,sha256=L51ubV1OyeDYbuOscw9N1Ar3tU8mgWSRXeuCUYs_iCQ,10262
43
+ sqladmin/templates/sqladmin/login.html,sha256=Y_hlcIapfVFPNbSIbCe4Tbj5DLLD46emkSlL5-RP4iY,1514
44
+ sqladmin/templates/sqladmin/modals/delete.html,sha256=jTuv6geT-AhK5HTgRmntrJ8CEi98-kwKrVDrzkOQWhw,1092
45
+ sqladmin/templates/sqladmin/modals/details_action_confirmation.html,sha256=mN8LJ5OqypxNLAg2_GYZgQmGeK4E6t7JL5RmOEYuliM,1020
46
+ sqladmin/templates/sqladmin/modals/list_action_confirmation.html,sha256=U52LLNmpLaMuUZSVtGK15oLXsEu6m2S3l9zj9sjN6uM,1078
47
+ sqladmin-0.17.0.dist-info/METADATA,sha256=nNDF1rnQCDmFiJa0B_9XBs-SCPoFGy5dUrw7VOjY9W0,5270
48
+ sqladmin-0.17.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
49
+ sqladmin-0.17.0.dist-info/licenses/LICENSE.md,sha256=4zzpHQMPtND4hzIgJA5qnb4R_wRBWJlYGqNrZolBeP8,1488
50
+ sqladmin-0.17.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.18.0
2
+ Generator: hatchling 1.24.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,32 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
6
- <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/tabler.min.css') }}">
7
- <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/fontawesome.min.css') }}">
8
- <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/select2.min.css') }}">
9
- <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/flatpickr.min.css') }}">
10
- <link rel="stylesheet" href="{{ url_for('admin:statics', path='css/main.css') }}">
11
- {% block head %}
12
- {% endblock %}
13
- <title>{{ admin.title }}</title>
14
- </head>
15
- <body>
16
- {% block body %}
17
- <main>
18
- {% block main %}
19
- {% endblock %}
20
- </main>
21
- {% endblock %}
22
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/jquery.min.js') }}"></script>
23
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/tabler.min.js') }}"></script>
24
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/popper.min.js') }}"></script>
25
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/bootstrap.min.js') }}"></script>
26
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/select2.full.min.js') }}"></script>
27
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/flatpickr.min.js') }}"></script>
28
- <script type="text/javascript" src="{{ url_for('admin:statics', path='js/main.js') }}"></script>
29
- {% block tail %}
30
- {% endblock %}
31
- </body>
32
- </html>
@@ -1,3 +0,0 @@
1
- {% extends "layout.html" %}
2
- {% block content %}
3
- {% endblock %}
@@ -1,50 +0,0 @@
1
- sqladmin/__init__.py,sha256=dH6cZz4jnTV45ho7So4i4jF79aaGCD-qPOEVHBXfUYk,216
2
- sqladmin/_menu.py,sha256=-B5anwJTCZ8jcuVtFbn5Um0sBqEf2h_ySymGl11pIu8,2622
3
- sqladmin/_queries.py,sha256=xZNv4eahSGZCffdsYi5h-6f951RHrRmT4AtLOtqYr8s,9669
4
- sqladmin/_types.py,sha256=3Zs0aPb14OS-9leahKxxzFopnIOiNftPZwdUmFDBKog,347
5
- sqladmin/_validators.py,sha256=w0siGhZQq4MD__lu9Edua9DgMOoKET_kk-alpARFHIM,1604
6
- sqladmin/ajax.py,sha256=oerBfp90Bow0CsDonBdurCl8XfzNvVWprzm8C3PJmlI,2552
7
- sqladmin/application.py,sha256=BvQV-pOPFo6SOrDWb_05nOXlMOWgEjTFnzNAiR_KY60,26643
8
- sqladmin/authentication.py,sha256=R4ad-8A5zWZ0vInK0pRj_oWirrQgWEY0Ng36j_NMC1U,2459
9
- sqladmin/exceptions.py,sha256=6-E8m7rbWE3A7hNaSmB6CVqFzkEuwUpmU5AdGbouPCw,154
10
- sqladmin/fields.py,sha256=5CFibyLYliD48Y6p2AKzmtdQeTHxxcn5_7LieQmBpGY,11752
11
- sqladmin/formatters.py,sha256=K06la0mm9-Bs5UA9L6KGJC_X_lV3UHdJ3ENI6j9j2Zg,480
12
- sqladmin/forms.py,sha256=K_uq8AdTvBmsjZiIxKROwDfAZSZ5mlMt951Tga_rQMU,21404
13
- sqladmin/helpers.py,sha256=6FxD4wXhc9RRss-dR02NGuMVYOSQBOD2pt4EGRedCa4,8545
14
- sqladmin/models.py,sha256=koyQqrleR-MJl-3nboxRT-jT48J715QOmhvVH1GlGbA,36683
15
- sqladmin/pagination.py,sha256=AmaZ5xNgPdBqTURYdOltz5IcSz8pY4qGQsr8evpUtHk,2256
16
- sqladmin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- sqladmin/templating.py,sha256=uIOfv7AFS4i_pErxdj5bcKDKxXz28BLlzCkLVFHqZsI,2244
18
- sqladmin/widgets.py,sha256=JB9lYUuZGsxSvBIDosvNYYDBPjlOr10ALoDpkugoB_w,2797
19
- sqladmin/statics/css/flatpickr.min.css,sha256=GzSkJVLJbxDk36qko2cnawOGiqz_Y8GsQv_jMTUrx1Q,16166
20
- sqladmin/statics/css/fontawesome.min.css,sha256=CTSx_A06dm1B063156EVh15m6Y67pAjZZaQc89LLSrU,102217
21
- sqladmin/statics/css/main.css,sha256=G1fZ4-mnRIXSXEXNHIAMGyegV-46bSYxjxwmwud30IE,46
22
- sqladmin/statics/css/select2.min.css,sha256=FdatTf20PQr_rWg-cAKfl6j4_IY3oohFAJ7gVC3M34E,14966
23
- sqladmin/statics/css/tabler.min.css,sha256=9Mi6oBMfUzSQcvXKNXI080TGtYClqjusVxxlrM-12EQ,498576
24
- sqladmin/statics/js/bootstrap.min.js,sha256=5dHuQEbO64HT5DMJ0FO0I7hwGOYMTPDdjufF0-npBGU,62399
25
- sqladmin/statics/js/flatpickr.min.js,sha256=P3cmJKYotkCthfbkx0ci27L0y3mp2ZlCJ-RrV3oOsDE,58060
26
- sqladmin/statics/js/jquery.min.js,sha256=_xUj-3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej_m4,89501
27
- sqladmin/statics/js/main.js,sha256=xMh4ruSMIfdTxRWmXiBKdpP-5yLXxBdjadrTtVtYq0A,4068
28
- sqladmin/statics/js/popper.min.js,sha256=wZjZsjDhPsJ8oPi__hLJzWmP9Fj30Vm0c_9jALIiyjE,19167
29
- sqladmin/statics/js/select2.full.min.js,sha256=XG_auAy4aieWldzMImofrFDiySK-pwJC7aoo9St7rS0,79212
30
- sqladmin/statics/js/tabler.min.js,sha256=kuHV8gS0R5FupAkK6iExYXHKc5ZFbnSRYtVSorjvl0M,146912
31
- sqladmin/statics/webfonts/fa-brands-400.woff2,sha256=-q5vwKqUzFveUHZkfIF6IyBglqHL7aENHG89idYWPtE,109808
32
- sqladmin/statics/webfonts/fa-regular-400.woff2,sha256=jn5eobFfYqsU29QXaOj7zSHMhZpOpdqBJFfucUKZ-zU,24948
33
- sqladmin/statics/webfonts/fa-solid-900.woff2,sha256=cVKmkz7j1pDsKvPQnanXAXI9Fqo0EKbYDyj_iGbzuIA,150124
34
- sqladmin/templates/_macros.html,sha256=9o5uMwK8Q0S1rgCiQq2LPAc2ZOvk7Cf1RU52jNS-wSk,1847
35
- sqladmin/templates/base.html,sha256=g3UAx3czT797k6yq7e4YQHj-ZW2cqfx_PEMs9Xkh-oI,1647
36
- sqladmin/templates/create.html,sha256=P6BM1DMS51H4lerQrJpaUCpTnkts6pVrpTKci3O7CVA,1782
37
- sqladmin/templates/details.html,sha256=hY8i75pNKnRmfkSwMPSCYxxqPFGM_7zyjrgjjAf6H-I,3833
38
- sqladmin/templates/edit.html,sha256=7T7ZEtCc3Ui2xzpr7YRIXhnSPnaVaLo6K9mpoDN3HuE,1948
39
- sqladmin/templates/error.html,sha256=DGmfAYQB5eYpo1nfK_rDa1-YQ93OR5CKRkWl7x--ajE,282
40
- sqladmin/templates/index.html,sha256=4JMiiNqyHkriMjaXFgaJ1OGqPMEQKFZVr3bzZUzU_zU,63
41
- sqladmin/templates/layout.html,sha256=w8hThsavi3Ru2IIKRco_wAON5V0EhttwlGMRFNSjmD4,1975
42
- sqladmin/templates/list.html,sha256=c4zWkqSl0Pl7GOLtborHpxDx6wrNQT8gFDTLvBTK_pE,9878
43
- sqladmin/templates/login.html,sha256=aR_HArZjRP722VTU3zjXkBrKba7ipOaS7cnyzT8p-ck,1506
44
- sqladmin/templates/modals/delete.html,sha256=8K4iNkszaG3Qopt22OLTXxsLCsDidzjhtPQchyBwVLI,1093
45
- sqladmin/templates/modals/details_action_confirmation.html,sha256=hOoeqcS2SqlG92yArTuqr6zMz0lYfPZ4nKbohPDsFxc,1019
46
- sqladmin/templates/modals/list_action_confirmation.html,sha256=_LiqDuZ2dhd61eud0_LsHOXeYu4psyK9IYVCulWFfTY,1077
47
- sqladmin-0.16.0.dist-info/METADATA,sha256=QMssMZK6kReemZYGUqC1NSI3eE2uc8TnTV-oTXW5TQA,5156
48
- sqladmin-0.16.0.dist-info/WHEEL,sha256=9QBuHhg6FNW7lppboF2vKVbCGTVzsFykgRQjjlajrhA,87
49
- sqladmin-0.16.0.dist-info/licenses/LICENSE.md,sha256=4zzpHQMPtND4hzIgJA5qnb4R_wRBWJlYGqNrZolBeP8,1488
50
- sqladmin-0.16.0.dist-info/RECORD,,