sqladmin 0.20.1__py3-none-any.whl → 0.21.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.20.1"
4
+ __version__ = "0.21.0"
5
5
 
6
6
  __all__ = [
7
7
  "Admin",
sqladmin/_menu.py CHANGED
@@ -44,7 +44,7 @@ class ItemMenu:
44
44
  class CategoryMenu(ItemMenu):
45
45
  def is_active(self, request: Request) -> bool:
46
46
  return any(
47
- c.is_visible(request) and c.is_accessible(request) for c in self.children
47
+ c.is_active(request) and c.is_accessible(request) for c in self.children
48
48
  )
49
49
 
50
50
  @property
sqladmin/_types.py CHANGED
@@ -1,9 +1,25 @@
1
- from typing import Union
1
+ from typing import Any, Callable, List, Protocol, Tuple, Union, runtime_checkable
2
2
 
3
3
  from sqlalchemy.engine import Engine
4
4
  from sqlalchemy.ext.asyncio import AsyncEngine
5
5
  from sqlalchemy.orm import ColumnProperty, InstrumentedAttribute, RelationshipProperty
6
+ from sqlalchemy.sql.expression import Select
7
+ from starlette.requests import Request
6
8
 
7
9
  MODEL_PROPERTY = Union[ColumnProperty, RelationshipProperty]
8
10
  ENGINE_TYPE = Union[Engine, AsyncEngine]
9
11
  MODEL_ATTR = Union[str, InstrumentedAttribute]
12
+
13
+
14
+ @runtime_checkable
15
+ class ColumnFilter(Protocol):
16
+ title: str
17
+ parameter_name: str
18
+
19
+ async def lookups(
20
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
21
+ ) -> List[Tuple[str, str]]:
22
+ ...
23
+
24
+ async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
25
+ ...
sqladmin/application.py CHANGED
@@ -197,16 +197,22 @@ class BaseAdmin:
197
197
  view_instance: BaseView | ModelView,
198
198
  ) -> None:
199
199
  if hasattr(func, "_exposed"):
200
+ if view.is_model:
201
+ path = f"/{view_instance.identity}" + getattr(func, "_path")
202
+ name = f"view-{view_instance.identity}-{func.__name__}"
203
+ else:
204
+ view.identity = getattr(func, "_identity")
205
+ path = getattr(func, "_path")
206
+ name = getattr(func, "_identity")
207
+
200
208
  self.admin.add_route(
201
209
  route=func,
202
- path=getattr(func, "_path"),
210
+ path=path,
203
211
  methods=getattr(func, "_methods"),
204
- name=getattr(func, "_identity"),
212
+ name=name,
205
213
  include_in_schema=getattr(func, "_include_in_schema"),
206
214
  )
207
215
 
208
- view.identity = getattr(func, "_identity")
209
-
210
216
  def add_model_view(self, view: type[ModelView]) -> None:
211
217
  """Add ModelView to the Admin.
212
218
 
@@ -233,6 +239,11 @@ class BaseAdmin:
233
239
  self._find_decorated_funcs(
234
240
  view, view_instance, self._handle_action_decorated_func
235
241
  )
242
+
243
+ self._find_decorated_funcs(
244
+ view, view_instance, self._handle_expose_decorated_func
245
+ )
246
+
236
247
  self._views.append(view_instance)
237
248
  self._build_menu(view_instance)
238
249
 
@@ -266,7 +277,7 @@ class BaseAdmin:
266
277
 
267
278
  def _build_menu(self, view: ModelView | BaseView) -> None:
268
279
  if view.category:
269
- menu = CategoryMenu(name=view.category)
280
+ menu = CategoryMenu(name=view.category, icon=view.category_icon)
270
281
  menu.add_child(ViewMenu(view=view, name=view.name, icon=view.icon))
271
282
  self._menu.add(menu)
272
283
  else:
@@ -461,10 +472,9 @@ class Admin(BaseAdminView):
461
472
  """Details route."""
462
473
 
463
474
  await self._details(request)
464
-
465
475
  model_view = self._find_model_view(request.path_params["identity"])
476
+ model = await model_view.get_object_for_details(request)
466
477
 
467
- model = await model_view.get_object_for_details(request.path_params["pk"])
468
478
  if not model:
469
479
  raise HTTPException(status_code=404)
470
480
 
@@ -638,7 +648,11 @@ class Admin(BaseAdminView):
638
648
  async def logout(self, request: Request) -> Response:
639
649
  assert self.authentication_backend is not None
640
650
 
641
- await self.authentication_backend.logout(request)
651
+ response = await self.authentication_backend.logout(request)
652
+
653
+ if isinstance(response, Response):
654
+ return response
655
+
642
656
  return RedirectResponse(request.url_for("admin:index"), status_code=302)
643
657
 
644
658
  async def ajax_lookup(self, request: Request) -> Response:
@@ -29,9 +29,13 @@ class AuthenticationBackend:
29
29
  """
30
30
  raise NotImplementedError()
31
31
 
32
- async def logout(self, request: Request) -> bool:
32
+ async def logout(self, request: Request) -> Response | bool:
33
33
  """Implement logout logic here.
34
34
  This will usually clear the session with `request.session.clear()`.
35
+
36
+ If a `Response` or `RedirectResponse` is returned,
37
+ that response is returned to the user,
38
+ otherwise the user will be redirected to the index page.
35
39
  """
36
40
  raise NotImplementedError()
37
41
 
@@ -40,7 +44,7 @@ class AuthenticationBackend:
40
44
  This method will be called for each incoming request
41
45
  to validate the authentication.
42
46
 
43
- If a 'Response' or `RedirectResponse` is returned,
47
+ If a `Response` or `RedirectResponse` is returned,
44
48
  that response is returned to the user,
45
49
  otherwise a True/False is expected.
46
50
  """
sqladmin/filters.py ADDED
@@ -0,0 +1,174 @@
1
+ import re
2
+ from typing import Any, Callable, List, Optional, Tuple
3
+
4
+ from sqlalchemy import Integer
5
+ from sqlalchemy.sql.expression import Select, select
6
+ from starlette.requests import Request
7
+
8
+ from sqladmin._types import MODEL_ATTR
9
+
10
+
11
+ def get_parameter_name(column: MODEL_ATTR) -> str:
12
+ if isinstance(column, str):
13
+ return column
14
+ else:
15
+ return column.key
16
+
17
+
18
+ def prettify_attribute_name(name: str) -> str:
19
+ return re.sub(r"_([A-Za-z])", r" \1", name).title()
20
+
21
+
22
+ def get_title(column: MODEL_ATTR) -> str:
23
+ name = get_parameter_name(column)
24
+ return prettify_attribute_name(name)
25
+
26
+
27
+ def get_column_obj(column: MODEL_ATTR, model: Any = None) -> Any:
28
+ if isinstance(column, str):
29
+ if model is None:
30
+ raise ValueError("model is required for string column filters")
31
+ return getattr(model, column)
32
+ return column
33
+
34
+
35
+ def get_foreign_column_name(column_obj: Any) -> str:
36
+ fk = next(iter(column_obj.foreign_keys))
37
+ return fk.column.name
38
+
39
+
40
+ def get_model_from_column(column: Any) -> Any:
41
+ return column.parent.class_
42
+
43
+
44
+ class BooleanFilter:
45
+ def __init__(
46
+ self,
47
+ column: MODEL_ATTR,
48
+ title: Optional[str] = None,
49
+ parameter_name: Optional[str] = None,
50
+ ):
51
+ self.column = column
52
+ self.title = title or get_title(column)
53
+ self.parameter_name = parameter_name or get_parameter_name(column)
54
+
55
+ async def lookups(
56
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
57
+ ) -> List[Tuple[str, str]]:
58
+ return [
59
+ ("all", "All"),
60
+ ("true", "Yes"),
61
+ ("false", "No"),
62
+ ]
63
+
64
+ async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
65
+ column_obj = get_column_obj(self.column, model)
66
+ if value == "true":
67
+ return query.filter(column_obj.is_(True))
68
+ elif value == "false":
69
+ return query.filter(column_obj.is_(False))
70
+ else:
71
+ return query
72
+
73
+
74
+ class AllUniqueStringValuesFilter:
75
+ def __init__(
76
+ self,
77
+ column: MODEL_ATTR,
78
+ title: Optional[str] = None,
79
+ parameter_name: Optional[str] = None,
80
+ ):
81
+ self.column = column
82
+ self.title = title or get_title(column)
83
+ self.parameter_name = parameter_name or get_parameter_name(column)
84
+
85
+ async def lookups(
86
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
87
+ ) -> List[Tuple[str, str]]:
88
+ column_obj = get_column_obj(self.column, model)
89
+
90
+ return [("", "All")] + [
91
+ (value[0], value[0])
92
+ for value in await run_query(select(column_obj).distinct())
93
+ ]
94
+
95
+ async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
96
+ if value == "":
97
+ return query
98
+
99
+ column_obj = get_column_obj(self.column, model)
100
+ return query.filter(column_obj == value)
101
+
102
+
103
+ class StaticValuesFilter:
104
+ def __init__(
105
+ self,
106
+ column: MODEL_ATTR,
107
+ values: List[Tuple[str, str]],
108
+ title: Optional[str] = None,
109
+ parameter_name: Optional[str] = None,
110
+ ):
111
+ self.column = column
112
+ self.title = title or get_title(column)
113
+ self.parameter_name = parameter_name or get_parameter_name(column)
114
+ self.values = values
115
+
116
+ async def lookups(
117
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
118
+ ) -> List[Tuple[str, str]]:
119
+ return [("", "All")] + self.values
120
+
121
+ async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
122
+ column_obj = get_column_obj(self.column, model)
123
+ if value == "":
124
+ return query
125
+ return query.filter(column_obj == value)
126
+
127
+
128
+ class ForeignKeyFilter:
129
+ def __init__(
130
+ self,
131
+ foreign_key: MODEL_ATTR,
132
+ foreign_display_field: MODEL_ATTR,
133
+ foreign_model: Any = None,
134
+ title: Optional[str] = None,
135
+ parameter_name: Optional[str] = None,
136
+ ):
137
+ self.foreign_key = foreign_key
138
+ self.foreign_display_field = foreign_display_field
139
+ self.foreign_model = foreign_model
140
+ self.title = title or get_title(foreign_key)
141
+ self.parameter_name = parameter_name or get_parameter_name(foreign_key)
142
+
143
+ async def lookups(
144
+ self, request: Request, model: Any, run_query: Callable[[Select], Any]
145
+ ) -> List[Tuple[str, str]]:
146
+ foreign_key_obj = get_column_obj(self.foreign_key, model)
147
+ if self.foreign_model is None and isinstance(self.foreign_display_field, str):
148
+ raise ValueError("foreign_model is required for string foreign key filters")
149
+ if self.foreign_model is None:
150
+ assert not isinstance(self.foreign_display_field, str)
151
+ foreign_display_field_obj = self.foreign_display_field
152
+ else:
153
+ foreign_display_field_obj = get_column_obj(
154
+ self.foreign_display_field, self.foreign_model
155
+ )
156
+ if not self.foreign_model:
157
+ self.foreign_model = get_model_from_column(foreign_display_field_obj)
158
+ foreign_model_key_name = get_foreign_column_name(foreign_key_obj)
159
+ foreign_model_key_obj = getattr(self.foreign_model, foreign_model_key_name)
160
+
161
+ return [("", "All")] + [
162
+ (str(key), str(value))
163
+ for key, value in await run_query(
164
+ select(foreign_model_key_obj, foreign_display_field_obj).distinct()
165
+ )
166
+ ]
167
+
168
+ async def get_filtered_query(self, query: Select, value: Any, model: Any) -> Select:
169
+ foreign_key_obj = get_column_obj(self.foreign_key, model)
170
+ column_type = foreign_key_obj.type
171
+ if isinstance(column_type, Integer):
172
+ value = int(value)
173
+
174
+ return query.filter(foreign_key_obj == value)
sqladmin/helpers.py CHANGED
@@ -152,8 +152,10 @@ class _PseudoBuffer:
152
152
  interface.
153
153
  """
154
154
 
155
- def write(self, value: T) -> T:
156
- return value
155
+ encoding = "utf-8"
156
+
157
+ def write(self, value: T) -> bytes:
158
+ return str(value).encode(self.encoding)
157
159
 
158
160
 
159
161
  def stream_to_csv(
sqladmin/models.py CHANGED
@@ -36,7 +36,7 @@ from wtforms import Field, Form
36
36
  from wtforms.fields.core import UnboundField
37
37
 
38
38
  from sqladmin._queries import Query
39
- from sqladmin._types import MODEL_ATTR
39
+ from sqladmin._types import MODEL_ATTR, ColumnFilter
40
40
  from sqladmin.ajax import create_ajax_loader
41
41
  from sqladmin.exceptions import InvalidModelError
42
42
  from sqladmin.formatters import BASE_FORMATTERS
@@ -183,6 +183,9 @@ class BaseView(BaseModelView):
183
183
  category: ClassVar[str] = ""
184
184
  """Category name to group views together."""
185
185
 
186
+ category_icon: ClassVar[str] = ""
187
+ """Display icon for category in the sidebar."""
188
+
186
189
 
187
190
  class ModelView(BaseView, metaclass=ModelViewMeta):
188
191
  """Base class for defining admnistrative behaviour for the model.
@@ -312,6 +315,17 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
312
315
  ```
313
316
  """
314
317
 
318
+ column_filters: ClassVar[Sequence[ColumnFilter]] = []
319
+ """Collection of the filterable columns for the list view.
320
+ Columns can either be string names or SQLAlchemy columns.
321
+
322
+ ???+ example
323
+ ```python
324
+ class UserAdmin(ModelView, model=User):
325
+ column_filters = [User.is_admin]
326
+ ```
327
+ """
328
+
315
329
  column_sortable_list: ClassVar[Sequence[MODEL_ATTR]] = []
316
330
  """Collection of the sortable columns for the list view.
317
331
 
@@ -398,7 +412,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
398
412
  """
399
413
 
400
414
  save_as: ClassVar[bool] = False
401
- """Set `save_as` to enable a save as new feature on admin change forms.
415
+ """Set `save_as` to enable a "save as new" feature on admin change forms.
402
416
 
403
417
  Normally, objects have three save options:
404
418
  ``Save`, `Save and continue editing` and `Save and add another`.
@@ -432,6 +446,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
432
446
  edit_template: ClassVar[str] = "sqladmin/edit.html"
433
447
  """Edit view template. Default is `sqladmin/edit.html`."""
434
448
 
449
+ # Template configuration
450
+ show_compact_lists: ClassVar[bool] = True
451
+ """Show compact lists. Default is `True`.
452
+ If False, when showing lists of objects, each object will be \
453
+ displayed in a separate line."""
454
+
435
455
  # Export
436
456
  column_export_list: ClassVar[List[MODEL_ATTR]] = []
437
457
  """List of columns to include when exporting.
@@ -645,7 +665,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
645
665
  - None will be displayed as an empty string
646
666
  - bool will be displayed as a checkmark if it is True otherwise as an X.
647
667
 
648
- If you dont like the default behavior and dont want any type formatters applied,
668
+ If you don't like the default behavior and don't want any type formatters applied,
649
669
  just override this property with an empty dictionary:
650
670
 
651
671
  ???+ example
@@ -718,6 +738,19 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
718
738
  self._custom_actions_in_detail: Dict[str, str] = {}
719
739
  self._custom_actions_confirmation: Dict[str, str] = {}
720
740
 
741
+ def _run_arbitrary_query_sync(self, stmt: ClauseElement) -> Any:
742
+ with self.session_maker(expire_on_commit=False) as session:
743
+ result = session.execute(stmt)
744
+ return result.all()
745
+
746
+ async def _run_arbitrary_query(self, stmt: ClauseElement) -> Any:
747
+ if self.is_async:
748
+ async with self.session_maker(expire_on_commit=False) as session:
749
+ result = await session.execute(stmt)
750
+ return result.all()
751
+ else:
752
+ return self._run_arbitrary_query_sync(stmt)
753
+
721
754
  def _run_query_sync(self, stmt: ClauseElement) -> Any:
722
755
  with self.session_maker(expire_on_commit=False) as session:
723
756
  result = session.execute(stmt)
@@ -803,6 +836,12 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
803
836
  for relation in self._list_relations:
804
837
  stmt = stmt.options(selectinload(relation))
805
838
 
839
+ for filter in self.get_filters():
840
+ if request.query_params.get(filter.parameter_name):
841
+ stmt = await filter.get_filtered_query(
842
+ stmt, request.query_params.get(filter.parameter_name), self.model
843
+ )
844
+
806
845
  stmt = self.sort_query(stmt, request)
807
846
 
808
847
  if search:
@@ -840,12 +879,8 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
840
879
  rows = await self._run_query(stmt)
841
880
  return rows[0] if rows else None
842
881
 
843
- async def get_object_for_details(self, value: Any) -> Any:
844
- stmt = self._stmt_by_identifier(value)
845
-
846
- for relation in self._details_relations:
847
- stmt = stmt.options(selectinload(relation))
848
-
882
+ async def get_object_for_details(self, request: Request) -> Any:
883
+ stmt = self.details_query(request)
849
884
  return await self._get_object_by_pk(stmt)
850
885
 
851
886
  async def get_object_for_edit(self, request: Request) -> Any:
@@ -973,6 +1008,15 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
973
1008
  defaults=self._list_prop_names,
974
1009
  )
975
1010
 
1011
+ def get_filters(self) -> List[ColumnFilter]:
1012
+ """Get list of filters."""
1013
+
1014
+ filters = getattr(self, "column_filters", None)
1015
+ if not filters:
1016
+ return []
1017
+
1018
+ return filters
1019
+
976
1020
  async def on_model_change(
977
1021
  self, data: dict, model: Any, is_created: bool, request: Request
978
1022
  ) -> None:
@@ -1088,6 +1132,14 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1088
1132
 
1089
1133
  return select(self.model)
1090
1134
 
1135
+ def details_query(self, request: Request) -> Select:
1136
+ """
1137
+ The SQLAlchemy select expression used for the details page which can be
1138
+ customized. By default it will select all objects without any filters.
1139
+ """
1140
+
1141
+ return self.form_edit_query(request)
1142
+
1091
1143
  def edit_form_query(self, request: Request) -> Select:
1092
1144
  msg = (
1093
1145
  "Overriding 'edit_form_query' is deprecated. Use 'form_edit_query' instead."
@@ -1184,7 +1236,7 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1184
1236
 
1185
1237
  return StreamingResponse(
1186
1238
  content=stream_to_csv(generate),
1187
- media_type="text/csv",
1239
+ media_type="text/csv; charset=utf-8",
1188
1240
  headers={"Content-Disposition": f"attachment;filename={filename}"},
1189
1241
  )
1190
1242
 
@@ -1203,7 +1255,9 @@ class ModelView(BaseView, metaclass=ModelViewMeta):
1203
1255
  name: str(await self.get_prop_value(row, name))
1204
1256
  for name in self._export_prop_names
1205
1257
  }
1206
- yield json.dumps(row_dict) + (separator if idx < last_idx else "")
1258
+ yield json.dumps(row_dict, ensure_ascii=False) + (
1259
+ separator if idx < last_idx else ""
1260
+ )
1207
1261
 
1208
1262
  yield "]"
1209
1263
 
@@ -1,3 +1,10 @@
1
1
  .table thead th {
2
2
  text-transform: none;
3
- }
3
+ }
4
+
5
+ .required-label::before {
6
+ content: "*";
7
+ color: red;
8
+ margin-right: 5px;
9
+ }
10
+
@@ -1,5 +1,5 @@
1
1
  {% macro menu_category(menu, request) %}
2
- {% if menu.is_active(request) %}
2
+ {% if menu.is_visible(request) and menu.is_accessible(request) %}
3
3
  <li class="nav-item dropdown">
4
4
  <a class="nav-link dropdown-toggle {% if menu.is_active(request) %}active{% endif %}" data-bs-toggle="dropdown"
5
5
  href="#">
@@ -56,7 +56,10 @@
56
56
 
57
57
  {% macro render_field(field, kwargs={}) %}
58
58
  <div class="mb-3 form-group row">
59
- {{ field.label(class_="form-label col-sm-2 col-form-label") }}
59
+ {{ field.label(
60
+ class_="form-label col-sm-2 col-form-label" + (' required-label' if field.flags.required else ''),
61
+ **({'title': "This is a required field"} if field.flags.required else {})
62
+ ) }}
60
63
  <div class="col-sm-10">
61
64
  {% if field.errors %}
62
65
  {{ field(class_="form-control is-invalid") }}
@@ -28,7 +28,11 @@
28
28
  {% if is_list( value ) %}
29
29
  <td>
30
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>
31
+ {% if model_view.show_compact_lists %}
32
+ <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
33
+ {% else %}
34
+ <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">{{ formatted_elem }}</a><br/>
35
+ {% endif %}
32
36
  {% endfor %}
33
37
  </td>
34
38
  {% else %}
@@ -1,210 +1,249 @@
1
1
  {% extends "sqladmin/layout.html" %}
2
2
  {% block content %}
3
- <div class="col-12">
4
- <div class="card">
5
- <div class="card-header">
6
- <h3 class="card-title">{{ model_view.name_plural }}</h3>
7
- <div class="ms-auto">
8
- {% if model_view.can_export %}
9
- {% if model_view.export_types | length > 1 %}
10
- <div class="ms-3 d-inline-block dropdown">
11
- <a href="#" class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown"
12
- aria-expanded="false">
13
- Export
14
- </a>
15
- <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
16
- {% for export_type in model_view.export_types %}
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>
20
- {% endfor %}
21
- </ul>
22
- </div>
23
- {% elif model_view.export_types | length == 1 %}
24
- <div class="ms-3 d-inline-block">
25
- <a href="{{ url_for('admin:export', identity=model_view.identity, export_type=model_view.export_types[0]) }}"
26
- class="btn btn-secondary">
27
- Export
28
- </a>
29
- </div>
30
- {% endif %}
31
- {% endif %}
32
- {% if model_view.can_create %}
33
- <div class="ms-3 d-inline-block">
34
- <a href="{{ url_for('admin:create', identity=model_view.identity) }}" class="btn btn-primary">
35
- + New {{ model_view.name }}
36
- </a>
37
- </div>
38
- {% endif %}
39
- </div>
40
- </div>
41
- <div class="card-body border-bottom py-3">
42
- <div class="d-flex justify-content-between">
43
- <div class="dropdown col-4">
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">
47
- Actions
48
- </button>
49
- {% if model_view.can_delete or model_view._custom_actions_in_list %}
50
- <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
51
- {% if model_view.can_delete %}
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>
55
- {% endif %}
56
- {% for custom_action, label in model_view._custom_actions_in_list.items() %}
57
- {% if custom_action in model_view._custom_actions_confirmation %}
58
- <a class="dropdown-item" id="action-customconfirm-{{ custom_action }}" href="#" data-bs-toggle="modal"
59
- data-bs-target="#modal-confirmation-{{ custom_action }}">
60
- {{ label }}
61
- </a>
62
- {% else %}
63
- <a class="dropdown-item" id="action-custom-{{ custom_action }}" href="#"
64
- data-url="{{ model_view._url_for_action(request, custom_action) }}">
65
- {{ label }}
66
- </a>
67
- {% endif %}
68
- {% endfor %}
3
+ <div class="container-fluid">
4
+ <div class="row">
5
+ <div class="col-12">
6
+ <div class="d-flex">
7
+ <div class="flex-grow-1 me-2">
8
+ <div class="card">
9
+ <div class="card-header">
10
+ <h3 class="card-title">{{ model_view.name_plural }}</h3>
11
+ <div class="ms-auto">
12
+ {% if model_view.can_export %}
13
+ {% if model_view.export_types | length > 1 %}
14
+ <div class="ms-3 d-inline-block dropdown">
15
+ <a href="#" class="btn btn-secondary dropdown-toggle" id="dropdownMenuButton1" data-bs-toggle="dropdown"
16
+ aria-expanded="false">
17
+ Export
18
+ </a>
19
+ <ul class="dropdown-menu" aria-labelledby="dropdownMenuButton1">
20
+ {% for export_type in model_view.export_types %}
21
+ <li><a class="dropdown-item"
22
+ href="{{ url_for('admin:export', identity=model_view.identity, export_type=export_type) }}">{{
23
+ export_type | upper }}</a></li>
24
+ {% endfor %}
25
+ </ul>
26
+ </div>
27
+ {% elif model_view.export_types | length == 1 %}
28
+ <div class="ms-3 d-inline-block">
29
+ <a href="{{ url_for('admin:export', identity=model_view.identity, export_type=model_view.export_types[0]) }}"
30
+ class="btn btn-secondary">
31
+ Export
32
+ </a>
33
+ </div>
34
+ {% endif %}
35
+ {% endif %}
36
+ {% if model_view.can_create %}
37
+ <div class="ms-3 d-inline-block">
38
+ <a href="{{ url_for('admin:create', identity=model_view.identity) }}" class="btn btn-primary">
39
+ + New {{ model_view.name }}
40
+ </a>
41
+ </div>
42
+ {% endif %}
43
+ </div>
44
+ </div>
45
+ <div class="card-body border-bottom py-3">
46
+ <div class="d-flex justify-content-between">
47
+ <div class="dropdown col-4">
48
+ <button {% if not model_view.can_delete and not model_view._custom_actions_in_list %} disabled {% endif %}
49
+ class="btn btn-light dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown"
50
+ aria-haspopup="true" aria-expanded="false">
51
+ Actions
52
+ </button>
53
+ {% if model_view.can_delete or model_view._custom_actions_in_list %}
54
+ <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
55
+ {% if model_view.can_delete %}
56
+ <a class="dropdown-item" id="action-delete" href="#" data-name="{{ model_view.name }}"
57
+ data-url="{{ url_for('admin:delete', identity=model_view.identity) }}" data-bs-toggle="modal"
58
+ data-bs-target="#modal-delete">Delete selected items</a>
59
+ {% endif %}
60
+ {% for custom_action, label in model_view._custom_actions_in_list.items() %}
61
+ {% if custom_action in model_view._custom_actions_confirmation %}
62
+ <a class="dropdown-item" id="action-customconfirm-{{ custom_action }}" href="#" data-bs-toggle="modal"
63
+ data-bs-target="#modal-confirmation-{{ custom_action }}">
64
+ {{ label }}
65
+ </a>
66
+ {% else %}
67
+ <a class="dropdown-item" id="action-custom-{{ custom_action }}" href="#"
68
+ data-url="{{ model_view._url_for_action(request, custom_action) }}">
69
+ {{ label }}
70
+ </a>
71
+ {% endif %}
72
+ {% endfor %}
73
+ </div>
74
+ {% endif %}
75
+ </div>
76
+ {% if model_view.column_searchable_list %}
77
+ <div class="col-md-4 text-muted">
78
+ <div class="input-group">
79
+ <input id="search-input" type="text" class="form-control"
80
+ placeholder="Search: {{ model_view.search_placeholder() }}"
81
+ value="{{ request.query_params.get('search', '') }}">
82
+ <button id="search-button" class="btn" type="button">Search</button>
83
+ <button id="search-reset" class="btn" type="button" {% if not request.query_params.get('search')
84
+ %}disabled{% endif %}><i class="fa-solid fa-times"></i></button>
85
+ </div>
86
+ </div>
87
+ {% endif %}
88
+ </div>
89
+ </div>
90
+ <div class="table-responsive">
91
+ <table class="table card-table table-vcenter text-nowrap">
92
+ <thead>
93
+ <tr>
94
+ <th class="w-1"><input class="form-check-input m-0 align-middle" type="checkbox" aria-label="Select all"
95
+ id="select-all"></th>
96
+ <th class="w-1"></th>
97
+ {% for name in model_view._list_prop_names %}
98
+ {% set label = model_view._column_labels.get(name, name) %}
99
+ <th>
100
+ {% if name in model_view._sort_fields %}
101
+ {% if request.query_params.get("sortBy") == name and request.query_params.get("sort") == "asc" %}
102
+ <a href="{{ request.url.include_query_params(sort='desc') }}"><i class="fa-solid fa-arrow-up"></i> {{
103
+ label }}</a>
104
+ {% elif request.query_params.get("sortBy") == name and request.query_params.get("sort") == "desc" %}
105
+ <a href="{{ request.url.include_query_params(sort='asc') }}"><i class="fa-solid fa-arrow-down"></i> {{ label
106
+ }}</a>
107
+ {% else %}
108
+ <a href="{{ request.url.include_query_params(sortBy=name, sort='asc') }}">{{ label }}</a>
109
+ {% endif %}
110
+ {% else %}
111
+ {{ label }}
112
+ {% endif %}
113
+ </th>
114
+ {% endfor %}
115
+ </tr>
116
+ </thead>
117
+ <tbody>
118
+ {% for row in pagination.rows %}
119
+ <tr>
120
+ <td>
121
+ <input type="hidden" value="{{ get_object_identifier(row) }}">
122
+ <input class="form-check-input m-0 align-middle select-box" type="checkbox" aria-label="Select item">
123
+ </td>
124
+ <td class="text-end">
125
+ {% if model_view.can_view_details %}
126
+ <a href="{{ model_view._build_url_for('admin:details', request, row) }}" data-bs-toggle="tooltip"
127
+ data-bs-placement="top" title="View">
128
+ <span class="me-1"><i class="fa-solid fa-eye"></i></span>
129
+ </a>
130
+ {% endif %}
131
+ {% if model_view.can_edit %}
132
+ <a href="{{ model_view._build_url_for('admin:edit', request, row) }}" data-bs-toggle="tooltip"
133
+ data-bs-placement="top" title="Edit">
134
+ <span class="me-1"><i class="fa-solid fa-pen-to-square"></i></span>
135
+ </a>
136
+ {% endif %}
137
+ {% if model_view.can_delete %}
138
+ <a href="#" data-name="{{ model_view.name }}" data-pk="{{ get_object_identifier(row) }}"
139
+ data-url="{{ model_view._url_for_delete(request, row) }}" data-bs-toggle="modal"
140
+ data-bs-target="#modal-delete" title="Delete">
141
+ <span class="me-1"><i class="fa-solid fa-trash"></i></span>
142
+ </a>
143
+ {% endif %}
144
+ </td>
145
+ {% for name in model_view._list_prop_names %}
146
+ {% set value, formatted_value = model_view.get_list_value(row, name) %}
147
+ {% if name in model_view._relation_names %}
148
+ {% if is_list( value ) %}
149
+ <td>
150
+ {% for elem, formatted_elem in zip(value, formatted_value) %}
151
+ {% if model_view.show_compact_lists %}
152
+ <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">({{ formatted_elem }})</a>
153
+ {% else %}
154
+ <a href="{{ model_view._build_url_for('admin:details', request, elem) }}">{{ formatted_elem }}</a><br/>
155
+ {% endif %}
156
+ {% endfor %}
157
+ </td>
158
+ {% else %}
159
+ <td><a href="{{ model_view._url_for_details_with_prop(request, row, name) }}">{{ formatted_value }}</a></td>
160
+ {% endif %}
161
+ {% else %}
162
+ <td>{{ formatted_value }}</td>
163
+ {% endif %}
164
+ {% endfor %}
165
+ </tr>
166
+ {% endfor %}
167
+ </tbody>
168
+ </table>
169
+ </div>
170
+ <div class="card-footer d-flex justify-content-between align-items-center gap-2">
171
+ <p class="m-0 text-muted">Showing <span>{{ ((pagination.page - 1) * pagination.page_size) + 1 }}</span> to
172
+ <span>{{ min(pagination.page * pagination.page_size, pagination.count) }}</span> of <span>{{ pagination.count
173
+ }}</span> items
174
+ </p>
175
+ <ul class="pagination m-0 ms-auto">
176
+ <li class="page-item {% if not pagination.has_previous %}disabled{% endif %}">
177
+ {% if pagination.has_previous %}
178
+ <a class="page-link" href="{{ pagination.previous_page.url }}">
179
+ {% else %}
180
+ <a class="page-link" href="#">
181
+ {% endif %}
182
+ <i class="fa-solid fa-chevron-left"></i>
183
+ prev
184
+ </a>
185
+ </li>
186
+ {% for page_control in pagination.page_controls %}
187
+ <li class="page-item {% if page_control.number == pagination.page %}active{% endif %}"><a class="page-link"
188
+ href="{{ page_control.url }}">{{ page_control.number }}</a></li>
189
+ {% endfor %}
190
+ <li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
191
+ {% if pagination.has_next %}
192
+ <a class="page-link" href="{{ pagination.next_page.url }}">
193
+ {% else %}
194
+ <a class="page-link" href="#">
195
+ {% endif %}
196
+ next
197
+ <i class="fa-solid fa-chevron-right"></i>
198
+ </a>
199
+ </li>
200
+ </ul>
201
+ <div class="dropdown text-muted">
202
+ Show
203
+ <a href="#" class="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
204
+ aria-expanded="false">
205
+ {{ request.query_params.get("pageSize") or model_view.page_size }} / Page
206
+ </a>
207
+ <div class="dropdown-menu">
208
+ {% for page_size_option in model_view.page_size_options %}
209
+ <a class="dropdown-item" href="{{ request.url.include_query_params(pageSize=page_size_option, page=pagination.resize(page_size_option).page) }}">
210
+ {{ page_size_option }} / Page
211
+ </a>
212
+ {% endfor %}
213
+ </div>
214
+ </div>
215
+ </div>
69
216
  </div>
70
- {% endif %}
71
217
  </div>
72
- {% if model_view.column_searchable_list %}
73
- <div class="col-md-4 text-muted">
74
- <div class="input-group">
75
- <input id="search-input" type="text" class="form-control"
76
- placeholder="Search: {{ model_view.search_placeholder() }}"
77
- value="{{ request.query_params.get('search', '') }}">
78
- <button id="search-button" class="btn" type="button">Search</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>
218
+ {% if model_view.get_filters() %}
219
+ <div class="col-md-3" style="width: 300px; flex-shrink: 0;">
220
+ <div id="filter-sidebar" class="card">
221
+ <div class="card-header">
222
+ <h3 class="card-title">Filters</h3>
223
+ </div>
224
+ <div class="card-body p-0">
225
+ <div class="list-group list-group-flush">
226
+ {% for filter in model_view.get_filters() %}
227
+ <div class="list-group-item">
228
+ <div class="p-2">
229
+ <div class="fw-bold text-truncate">{{ filter.title }}</div>
230
+ <div>
231
+ {% for lookup in filter.lookups(request, model_view.model, model_view._run_arbitrary_query) %}
232
+ <a href="{{ request.url.include_query_params(**{filter.parameter_name: lookup[0]}) }}" class="d-block text-decoration-none text-truncate">
233
+ {{ lookup[1] }}
234
+ </a>
235
+ {% endfor %}
236
+ </div>
237
+ </div>
238
+ </div>
239
+ {% endfor %}
240
+ </div>
241
+ </div>
81
242
  </div>
82
243
  </div>
83
244
  {% endif %}
84
245
  </div>
85
246
  </div>
86
- <div class="table-responsive">
87
- <table class="table card-table table-vcenter text-nowrap">
88
- <thead>
89
- <tr>
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>
92
- <th class="w-1"></th>
93
- {% for name in model_view._list_prop_names %}
94
- {% set label = model_view._column_labels.get(name, name) %}
95
- <th>
96
- {% if name in model_view._sort_fields %}
97
- {% if request.query_params.get("sortBy") == name and request.query_params.get("sort") == "asc" %}
98
- <a href="{{ request.url.include_query_params(sort='desc') }}"><i class="fa-solid fa-arrow-up"></i> {{
99
- label }}</a>
100
- {% elif request.query_params.get("sortBy") == name and request.query_params.get("sort") == "desc" %}
101
- <a href="{{ request.url.include_query_params(sort='asc') }}"><i class="fa-solid fa-arrow-down"></i> {{ label
102
- }}</a>
103
- {% else %}
104
- <a href="{{ request.url.include_query_params(sortBy=name, sort='asc') }}">{{ label }}</a>
105
- {% endif %}
106
- {% else %}
107
- {{ label }}
108
- {% endif %}
109
- </th>
110
- {% endfor %}
111
- </tr>
112
- </thead>
113
- <tbody>
114
- {% for row in pagination.rows %}
115
- <tr>
116
- <td>
117
- <input type="hidden" value="{{ get_object_identifier(row) }}">
118
- <input class="form-check-input m-0 align-middle select-box" type="checkbox" aria-label="Select item">
119
- </td>
120
- <td class="text-end">
121
- {% if model_view.can_view_details %}
122
- <a href="{{ model_view._build_url_for('admin:details', request, row) }}" data-bs-toggle="tooltip"
123
- data-bs-placement="top" title="View">
124
- <span class="me-1"><i class="fa-solid fa-eye"></i></span>
125
- </a>
126
- {% endif %}
127
- {% if model_view.can_edit %}
128
- <a href="{{ model_view._build_url_for('admin:edit', request, row) }}" data-bs-toggle="tooltip"
129
- data-bs-placement="top" title="Edit">
130
- <span class="me-1"><i class="fa-solid fa-pen-to-square"></i></span>
131
- </a>
132
- {% endif %}
133
- {% if model_view.can_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">
137
- <span class="me-1"><i class="fa-solid fa-trash"></i></span>
138
- </a>
139
- {% endif %}
140
- </td>
141
- {% for name in model_view._list_prop_names %}
142
- {% set value, formatted_value = model_view.get_list_value(row, name) %}
143
- {% if name in model_view._relation_names %}
144
- {% if is_list( value ) %}
145
- <td>
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 %}
149
- </td>
150
- {% else %}
151
- <td><a href="{{ model_view._url_for_details_with_prop(request, row, name) }}">{{ formatted_value }}</a></td>
152
- {% endif %}
153
- {% else %}
154
- <td>{{ formatted_value }}</td>
155
- {% endif %}
156
- {% endfor %}
157
- </tr>
158
- {% endfor %}
159
- </tbody>
160
- </table>
161
- </div>
162
- <div class="card-footer d-flex justify-content-between align-items-center gap-2">
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>
167
- <ul class="pagination m-0 ms-auto">
168
- <li class="page-item {% if not pagination.has_previous %}disabled{% endif %}">
169
- {% if pagination.has_previous %}
170
- <a class="page-link" href="{{ pagination.previous_page.url }}">
171
- {% else %}
172
- <a class="page-link" href="#">
173
- {% endif %}
174
- <i class="fa-solid fa-chevron-left"></i>
175
- prev
176
- </a>
177
- </li>
178
- {% for page_control in pagination.page_controls %}
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>
181
- {% endfor %}
182
- <li class="page-item {% if not pagination.has_next %}disabled{% endif %}">
183
- {% if pagination.has_next %}
184
- <a class="page-link" href="{{ pagination.next_page.url }}">
185
- {% else %}
186
- <a class="page-link" href="#">
187
- {% endif %}
188
- next
189
- <i class="fa-solid fa-chevron-right"></i>
190
- </a>
191
- </li>
192
- </ul>
193
- <div class="dropdown text-muted">
194
- Show
195
- <a href="#" class="btn btn-sm btn-light dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
196
- aria-expanded="false">
197
- {{ request.query_params.get("pageSize") or model_view.page_size }} / Page
198
- </a>
199
- <div class="dropdown-menu">
200
- {% for page_size_option in model_view.page_size_options %}
201
- <a class="dropdown-item" href="{{ request.url.include_query_params(pageSize=page_size_option, page=pagination.resize(page_size_option).page) }}">
202
- {{ page_size_option }} / Page
203
- </a>
204
- {% endfor %}
205
- </div>
206
- </div>
207
- </div>
208
247
  </div>
209
248
  {% if model_view.can_delete %}
210
249
  {% include 'sqladmin/modals/delete.html' %}
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: sqladmin
3
- Version: 0.20.1
3
+ Version: 0.21.0
4
4
  Summary: SQLAlchemy admin for FastAPI and Starlette
5
- Project-URL: Documentation, https://aminalaee.dev/sqladmin
5
+ Project-URL: Documentation, https://aminalaee.github.io/sqladmin/
6
6
  Project-URL: Issues, https://github.com/aminalaee/sqladmin/issues
7
7
  Project-URL: Source, https://github.com/aminalaee/sqladmin
8
8
  Author-email: Amin Alaee <me@aminalaee.dev>
@@ -72,7 +72,7 @@ Main features include:
72
72
 
73
73
  ---
74
74
 
75
- **Documentation**: [https://aminalaee.dev/sqladmin](https://aminalaee.dev/sqladmin)
75
+ **Documentation**: [https://aminalaee.github.io/sqladmin](https://aminalaee.github.io/sqladmin)
76
76
 
77
77
  **Source Code**: [https://github.com/aminalaee/sqladmin](https://github.com/aminalaee/sqladmin)
78
78
 
@@ -1,24 +1,25 @@
1
- sqladmin/__init__.py,sha256=3kjWCBseZv6vxNmMtKgrBicZUgE93ZwOF14aquXK_p0,216
2
- sqladmin/_menu.py,sha256=V6uKvIJGKrC0kmXq4M-DAgvPoHvgoGgKAw47OjsswGA,2609
1
+ sqladmin/__init__.py,sha256=KAhJq1lkT6W32lerJwPX-W87UENnUFwYQUDGGMpCSzE,216
2
+ sqladmin/_menu.py,sha256=kjBIk_PIpa__C-gSRPIWv9OEQR1uFB4M_1LOU5MHYu0,2608
3
3
  sqladmin/_queries.py,sha256=KqxMABvepoA0j-8Xizg6ASYS-sZDSdm5iFlU74vilQY,9697
4
- sqladmin/_types.py,sha256=3Zs0aPb14OS-9leahKxxzFopnIOiNftPZwdUmFDBKog,347
4
+ sqladmin/_types.py,sha256=OuuL7rOkymB9SjKphc5SLCdAnsSTLVINRVzkCMUufFs,827
5
5
  sqladmin/_validators.py,sha256=w0siGhZQq4MD__lu9Edua9DgMOoKET_kk-alpARFHIM,1604
6
6
  sqladmin/ajax.py,sha256=wSP5P9cAIh3GImIc_-E_Mi14aJAcbtiy4_pDPukTs5E,2764
7
- sqladmin/application.py,sha256=7aMILrxNrDxGAM8WX32XxGQbDjGmy6gp0A-tQ1usqsM,27364
8
- sqladmin/authentication.py,sha256=QdN3rLZFU25ihaIIi7iisb55-oFi4GtV_k1xGFvnS6w,2482
7
+ sqladmin/application.py,sha256=4uRwZNbzGjVnlphU1MwRuKBSink5UXbtR1tlkD9XmyU,27827
8
+ sqladmin/authentication.py,sha256=VLNa38rzvQ774c_I1duI5dUVw3-LdcGFyIPafHgXtxg,2665
9
9
  sqladmin/exceptions.py,sha256=6-E8m7rbWE3A7hNaSmB6CVqFzkEuwUpmU5AdGbouPCw,154
10
10
  sqladmin/fields.py,sha256=1CWoVSMr1WkhBJww0-rakx71gRATeIGA6dKgc26z99M,11660
11
+ sqladmin/filters.py,sha256=E2G5imQb1RNyEHf3UjQw1Ev3TWSE5zpxHoN1-THhlcU,5795
11
12
  sqladmin/formatters.py,sha256=K06la0mm9-Bs5UA9L6KGJC_X_lV3UHdJ3ENI6j9j2Zg,480
12
13
  sqladmin/forms.py,sha256=5VhbRWbsG23eDAGz2c03HnED-titNkBdYzDr-TaBSi0,21541
13
- sqladmin/helpers.py,sha256=VTPOFbWkiC1My6MYYyVUCAdg7UmnViiYE2ZigiXx628,8615
14
- sqladmin/models.py,sha256=Ygm5UetuHdn62n1zU5rukNs3dbjypgNC8NfVte9IIXY,40743
14
+ sqladmin/helpers.py,sha256=3ItzR-bRqfyDhD7bw68k7Jl87O7VIZ14xPmjL-SCg_M,8670
15
+ sqladmin/models.py,sha256=TEwiPTgvQu9JvzIgp1ZE_gT-6syJYqGsw95kgphsHjg,42681
15
16
  sqladmin/pagination.py,sha256=zg_bAvqZd2Rf0wKJ7uiVfNV9vR0hrsilmi9Ak0SOG_U,2600
16
17
  sqladmin/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
18
  sqladmin/templating.py,sha256=o-QMikTrOEgrneLonqCWR3SpAthr-9DoMwOmobM9zq0,2252
18
19
  sqladmin/widgets.py,sha256=xl9tGhj--KTRPmNhFn3WVbvN_tQfkYSNVBGicsFhrgM,3189
19
20
  sqladmin/statics/css/flatpickr.min.css,sha256=GzSkJVLJbxDk36qko2cnawOGiqz_Y8GsQv_jMTUrx1Q,16166
20
21
  sqladmin/statics/css/fontawesome.min.css,sha256=CTSx_A06dm1B063156EVh15m6Y67pAjZZaQc89LLSrU,102217
21
- sqladmin/statics/css/main.css,sha256=XziO3HgOtX6JmGb0lHSNzktBjevR_3XcmqmkV7E9jpc,45
22
+ sqladmin/statics/css/main.css,sha256=BeLxW6X9i_hoGHiXR-AQsdTRi7QPjBG1S2XUo0iFnS4,133
22
23
  sqladmin/statics/css/select2.min.css,sha256=FdatTf20PQr_rWg-cAKfl6j4_IY3oohFAJ7gVC3M34E,14966
23
24
  sqladmin/statics/css/tabler-icons.min.css,sha256=zfxHk87DofHBVVYIurCX27i9Sp-HvSIRAeObJLBdqyg,225500
24
25
  sqladmin/statics/css/tabler-icons.min.css.map,sha256=L2JU7eBUBXadlt31MqbN3ktHqPEg6ZkdkCo3twFFcs0,92491
@@ -34,20 +35,20 @@ sqladmin/statics/webfonts/fa-brands-400.woff2,sha256=-q5vwKqUzFveUHZkfIF6IyBglqH
34
35
  sqladmin/statics/webfonts/fa-regular-400.woff2,sha256=jn5eobFfYqsU29QXaOj7zSHMhZpOpdqBJFfucUKZ-zU,24948
35
36
  sqladmin/statics/webfonts/fa-solid-900.woff2,sha256=cVKmkz7j1pDsKvPQnanXAXI9Fqo0EKbYDyj_iGbzuIA,150124
36
37
  sqladmin/statics/webfonts/tabler-icons.woff2,sha256=26WaCnr1NnLG_uLQ3DMhEkenPiJJsvt_DxjafQUE6sY,837436
37
- sqladmin/templates/sqladmin/_macros.html,sha256=QK--kje7XrH5Flhumf4luLafvTCvTioPMfrsrgTnWEo,2809
38
+ sqladmin/templates/sqladmin/_macros.html,sha256=79GzkgJAaRbzhjm5WIRFkgl8W0U_5hH4gLteQan1rpo,2983
38
39
  sqladmin/templates/sqladmin/base.html,sha256=u6rdmdI6Kg7JteTFmLwh7UhIo2Z1yvZwCNvPnLrHAsg,1783
39
40
  sqladmin/templates/sqladmin/create.html,sha256=Vaj_OHLDIqnZF1HOz_g3ogTaCGZqPEfTJhaxezD1wjM,1396
40
- sqladmin/templates/sqladmin/details.html,sha256=RuWdlsZw5m_gm24Tdn3APNlfKtTHfYw7BnZB0_Tj6Hw,3932
41
+ sqladmin/templates/sqladmin/details.html,sha256=CiH4qAFRl5PaHRWHLwCwABHvk5ghbkTPj51fEt_JQ28,4176
41
42
  sqladmin/templates/sqladmin/edit.html,sha256=geKD5j8ZLcMSciI80-qg_dQM3Sn_7g3_DeIONmSzQt8,1641
42
43
  sqladmin/templates/sqladmin/error.html,sha256=gb-172SMuQKncv0QE8DQdQXeM-fw7oXC0LPLO3ia0IM,290
43
44
  sqladmin/templates/sqladmin/index.html,sha256=vh_IhhYmHPOkdZNrXSEc4e9gXXeZ-nsRBCsJQ_mC7YI,71
44
45
  sqladmin/templates/sqladmin/layout.html,sha256=iBIhypkXp6O3hAHDdMNc4pWd9yxt5mQy7o2lBQD-6Ec,1994
45
- sqladmin/templates/sqladmin/list.html,sha256=OqH4ZtxBWIARJnHqmE9jy58iGH8FgobHlM2XlG4208s,10309
46
+ sqladmin/templates/sqladmin/list.html,sha256=7Vi1vq632RrbSAaOWQrhG1qkzZ-h3vUJPiaTucqYLqY,13573
46
47
  sqladmin/templates/sqladmin/login.html,sha256=Y_hlcIapfVFPNbSIbCe4Tbj5DLLD46emkSlL5-RP4iY,1514
47
48
  sqladmin/templates/sqladmin/modals/delete.html,sha256=jTuv6geT-AhK5HTgRmntrJ8CEi98-kwKrVDrzkOQWhw,1092
48
49
  sqladmin/templates/sqladmin/modals/details_action_confirmation.html,sha256=mN8LJ5OqypxNLAg2_GYZgQmGeK4E6t7JL5RmOEYuliM,1020
49
50
  sqladmin/templates/sqladmin/modals/list_action_confirmation.html,sha256=U52LLNmpLaMuUZSVtGK15oLXsEu6m2S3l9zj9sjN6uM,1078
50
- sqladmin-0.20.1.dist-info/METADATA,sha256=ypbLpPnHwEMZZov5kKz416eplyFOOBwkp1BhWICYHWM,5270
51
- sqladmin-0.20.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
52
- sqladmin-0.20.1.dist-info/licenses/LICENSE.md,sha256=4zzpHQMPtND4hzIgJA5qnb4R_wRBWJlYGqNrZolBeP8,1488
53
- sqladmin-0.20.1.dist-info/RECORD,,
51
+ sqladmin-0.21.0.dist-info/METADATA,sha256=Trh6OTEv-lmEr46u6YeoAGO2JNzVkfqlGOCovomSIp4,5289
52
+ sqladmin-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ sqladmin-0.21.0.dist-info/licenses/LICENSE.md,sha256=4zzpHQMPtND4hzIgJA5qnb4R_wRBWJlYGqNrZolBeP8,1488
54
+ sqladmin-0.21.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any