django-unfold 0.46.0__py3-none-any.whl → 0.47.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.
- {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/METADATA +5 -6
- {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/RECORD +38 -31
- {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/WHEEL +1 -1
- unfold/admin.py +15 -16
- unfold/checks.py +4 -4
- unfold/components.py +5 -5
- unfold/contrib/filters/admin/__init__.py +43 -0
- unfold/contrib/filters/admin/autocomplete_filters.py +16 -0
- unfold/contrib/filters/admin/datetime_filters.py +212 -0
- unfold/contrib/filters/admin/dropdown_filters.py +100 -0
- unfold/contrib/filters/admin/mixins.py +146 -0
- unfold/contrib/filters/admin/numeric_filters.py +196 -0
- unfold/contrib/filters/admin/text_filters.py +65 -0
- unfold/contrib/filters/admin.py +32 -32
- unfold/contrib/filters/forms.py +68 -17
- unfold/contrib/forms/widgets.py +9 -9
- unfold/contrib/inlines/checks.py +2 -4
- unfold/contrib/simple_history/templates/simple_history/object_history.html +17 -1
- unfold/contrib/simple_history/templates/simple_history/object_history_list.html +1 -1
- unfold/dataclasses.py +2 -2
- unfold/decorators.py +4 -3
- unfold/settings.py +2 -2
- unfold/sites.py +156 -140
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/app.js +2 -2
- unfold/templates/admin/filter.html +1 -1
- unfold/templates/unfold/helpers/change_list_filter.html +2 -2
- unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
- unfold/templates/unfold/helpers/header_back_button.html +2 -2
- unfold/templates/unfold/helpers/tab_list.html +7 -1
- unfold/templates/unfold/layouts/skeleton.html +1 -1
- unfold/templatetags/unfold.py +55 -22
- unfold/templatetags/unfold_list.py +2 -2
- unfold/typing.py +5 -4
- unfold/utils.py +3 -2
- unfold/views.py +2 -2
- unfold/widgets.py +27 -27
- {django_unfold-0.46.0.dist-info → django_unfold-0.47.0.dist-info}/LICENSE.md +0 -0
unfold/contrib/filters/admin.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any,
|
1
|
+
from typing import Any, Optional
|
2
2
|
|
3
3
|
from django.contrib import admin
|
4
4
|
from django.contrib.admin.options import ModelAdmin
|
@@ -36,18 +36,18 @@ class ValueMixin:
|
|
36
36
|
return (
|
37
37
|
self.lookup_val[0]
|
38
38
|
if self.lookup_val not in EMPTY_VALUES
|
39
|
-
and isinstance(self.lookup_val,
|
39
|
+
and isinstance(self.lookup_val, list)
|
40
40
|
and len(self.lookup_val) > 0
|
41
41
|
else self.lookup_val
|
42
42
|
)
|
43
43
|
|
44
44
|
|
45
45
|
class MultiValueMixin:
|
46
|
-
def value(self) -> Optional[
|
46
|
+
def value(self) -> Optional[list[str]]:
|
47
47
|
return (
|
48
48
|
self.lookup_val
|
49
49
|
if self.lookup_val not in EMPTY_VALUES
|
50
|
-
and isinstance(self.lookup_val,
|
50
|
+
and isinstance(self.lookup_val, list)
|
51
51
|
and len(self.lookup_val) > 0
|
52
52
|
else self.lookup_val
|
53
53
|
)
|
@@ -72,10 +72,10 @@ class TextFilter(admin.SimpleListFilter):
|
|
72
72
|
def has_output(self) -> bool:
|
73
73
|
return True
|
74
74
|
|
75
|
-
def lookups(self, request: HttpRequest, model_admin: ModelAdmin) ->
|
75
|
+
def lookups(self, request: HttpRequest, model_admin: ModelAdmin) -> tuple:
|
76
76
|
return ()
|
77
77
|
|
78
|
-
def choices(self, changelist: ChangeList) ->
|
78
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
79
79
|
return (
|
80
80
|
{
|
81
81
|
"form": self.form_class(
|
@@ -96,10 +96,10 @@ class FieldTextFilter(ValueMixin, admin.FieldListFilter):
|
|
96
96
|
self.lookup_val = params.get(self.lookup_kwarg)
|
97
97
|
super().__init__(field, request, params, model, model_admin, field_path)
|
98
98
|
|
99
|
-
def expected_parameters(self) ->
|
99
|
+
def expected_parameters(self) -> list[str]:
|
100
100
|
return [self.lookup_kwarg]
|
101
101
|
|
102
|
-
def choices(self, changelist: ChangeList) ->
|
102
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
103
103
|
return (
|
104
104
|
{
|
105
105
|
"form": self.form_class(
|
@@ -116,7 +116,7 @@ class DropdownFilter(admin.SimpleListFilter):
|
|
116
116
|
form_class = DropdownForm
|
117
117
|
all_option = ["", _("All")]
|
118
118
|
|
119
|
-
def choices(self, changelist: ChangeList) ->
|
119
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
120
120
|
return (
|
121
121
|
{
|
122
122
|
"form": self.form_class(
|
@@ -186,8 +186,8 @@ class SingleNumericFilter(admin.FieldListFilter):
|
|
186
186
|
self,
|
187
187
|
field: Field,
|
188
188
|
request: HttpRequest,
|
189
|
-
params:
|
190
|
-
model:
|
189
|
+
params: dict[str, str],
|
190
|
+
model: type[Model],
|
191
191
|
model_admin: ModelAdmin,
|
192
192
|
field_path: str,
|
193
193
|
) -> None:
|
@@ -222,10 +222,10 @@ class SingleNumericFilter(admin.FieldListFilter):
|
|
222
222
|
def value(self) -> Any:
|
223
223
|
return self.used_parameters.get(self.parameter_name, None)
|
224
224
|
|
225
|
-
def expected_parameters(self) ->
|
225
|
+
def expected_parameters(self) -> list[Optional[str]]:
|
226
226
|
return [self.parameter_name]
|
227
227
|
|
228
|
-
def choices(self, changelist: ChangeList) ->
|
228
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
229
229
|
return (
|
230
230
|
{
|
231
231
|
"request": self.request,
|
@@ -242,7 +242,7 @@ class RangeNumericMixin:
|
|
242
242
|
parameter_name = None
|
243
243
|
template = "unfold/filters/filters_numeric_range.html"
|
244
244
|
|
245
|
-
def init_used_parameters(self, params:
|
245
|
+
def init_used_parameters(self, params: dict[str, Any]) -> None:
|
246
246
|
if self.parameter_name + "_from" in params:
|
247
247
|
value = params.pop(self.parameter_name + "_from")
|
248
248
|
|
@@ -284,13 +284,13 @@ class RangeNumericMixin:
|
|
284
284
|
except (ValueError, ValidationError):
|
285
285
|
return None
|
286
286
|
|
287
|
-
def expected_parameters(self) ->
|
287
|
+
def expected_parameters(self) -> list[str]:
|
288
288
|
return [
|
289
289
|
f"{self.parameter_name}_from",
|
290
290
|
f"{self.parameter_name}_to",
|
291
291
|
]
|
292
292
|
|
293
|
-
def choices(self, changelist: ChangeList) ->
|
293
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
294
294
|
return (
|
295
295
|
{
|
296
296
|
"request": self.request,
|
@@ -314,8 +314,8 @@ class RangeNumericListFilter(RangeNumericMixin, admin.SimpleListFilter):
|
|
314
314
|
def __init__(
|
315
315
|
self,
|
316
316
|
request: HttpRequest,
|
317
|
-
params:
|
318
|
-
model:
|
317
|
+
params: dict[str, str],
|
318
|
+
model: type[Model],
|
319
319
|
model_admin: ModelAdmin,
|
320
320
|
) -> None:
|
321
321
|
super().__init__(request, params, model, model_admin)
|
@@ -327,7 +327,7 @@ class RangeNumericListFilter(RangeNumericMixin, admin.SimpleListFilter):
|
|
327
327
|
|
328
328
|
def lookups(
|
329
329
|
self, request: HttpRequest, model_admin: ModelAdmin
|
330
|
-
) ->
|
330
|
+
) -> tuple[tuple[str, str], ...]:
|
331
331
|
return (("dummy", "dummy"),)
|
332
332
|
|
333
333
|
|
@@ -336,8 +336,8 @@ class RangeNumericFilter(RangeNumericMixin, admin.FieldListFilter):
|
|
336
336
|
self,
|
337
337
|
field: Field,
|
338
338
|
request: HttpRequest,
|
339
|
-
params:
|
340
|
-
model:
|
339
|
+
params: dict[str, str],
|
340
|
+
model: type[Model],
|
341
341
|
model_admin: ModelAdmin,
|
342
342
|
field_path: str,
|
343
343
|
) -> None:
|
@@ -366,8 +366,8 @@ class SliderNumericFilter(RangeNumericFilter):
|
|
366
366
|
self,
|
367
367
|
field: Field,
|
368
368
|
request: HttpRequest,
|
369
|
-
params:
|
370
|
-
model:
|
369
|
+
params: dict[str, str],
|
370
|
+
model: type[Model],
|
371
371
|
model_admin: ModelAdmin,
|
372
372
|
field_path: str,
|
373
373
|
) -> None:
|
@@ -376,7 +376,7 @@ class SliderNumericFilter(RangeNumericFilter):
|
|
376
376
|
self.field = field
|
377
377
|
self.q = model_admin.get_queryset(request)
|
378
378
|
|
379
|
-
def choices(self, changelist: ChangeList) ->
|
379
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
380
380
|
total = self.q.all().count()
|
381
381
|
min_value = self.q.all().aggregate(min=Min(self.parameter_name)).get("min", 0)
|
382
382
|
|
@@ -437,8 +437,8 @@ class RangeDateFilter(admin.FieldListFilter):
|
|
437
437
|
self,
|
438
438
|
field: Field,
|
439
439
|
request: HttpRequest,
|
440
|
-
params:
|
441
|
-
model:
|
440
|
+
params: dict[str, str],
|
441
|
+
model: type[Model],
|
442
442
|
model_admin: ModelAdmin,
|
443
443
|
field_path: str,
|
444
444
|
) -> None:
|
@@ -482,13 +482,13 @@ class RangeDateFilter(admin.FieldListFilter):
|
|
482
482
|
except (ValueError, ValidationError):
|
483
483
|
return None
|
484
484
|
|
485
|
-
def expected_parameters(self) ->
|
485
|
+
def expected_parameters(self) -> list[str]:
|
486
486
|
return [
|
487
487
|
f"{self.parameter_name}_from",
|
488
488
|
f"{self.parameter_name}_to",
|
489
489
|
]
|
490
490
|
|
491
|
-
def choices(self, changelist: ChangeList) ->
|
491
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
492
492
|
return (
|
493
493
|
{
|
494
494
|
"request": self.request,
|
@@ -518,8 +518,8 @@ class RangeDateTimeFilter(admin.FieldListFilter):
|
|
518
518
|
self,
|
519
519
|
field: Field,
|
520
520
|
request: HttpRequest,
|
521
|
-
params:
|
522
|
-
model:
|
521
|
+
params: dict[str, str],
|
522
|
+
model: type[Model],
|
523
523
|
model_admin: ModelAdmin,
|
524
524
|
field_path: str,
|
525
525
|
) -> None:
|
@@ -553,7 +553,7 @@ class RangeDateTimeFilter(admin.FieldListFilter):
|
|
553
553
|
value = value[0] if isinstance(value, list) else value
|
554
554
|
self.used_parameters[self.field_path + "_to_1"] = value
|
555
555
|
|
556
|
-
def expected_parameters(self) ->
|
556
|
+
def expected_parameters(self) -> list[str]:
|
557
557
|
return [
|
558
558
|
f"{self.parameter_name}_from_0",
|
559
559
|
f"{self.parameter_name}_from_1",
|
@@ -593,7 +593,7 @@ class RangeDateTimeFilter(admin.FieldListFilter):
|
|
593
593
|
except (ValueError, ValidationError):
|
594
594
|
return None
|
595
595
|
|
596
|
-
def choices(self, changelist: ChangeList) ->
|
596
|
+
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
597
597
|
return (
|
598
598
|
{
|
599
599
|
"request": self.request,
|
unfold/contrib/filters/forms.py
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
from django import forms
|
2
|
-
from django.
|
2
|
+
from django.contrib.admin.widgets import AutocompleteSelect, AutocompleteSelectMultiple
|
3
|
+
from django.db.models import Field as ModelField
|
4
|
+
from django.forms import ChoiceField, ModelMultipleChoiceField, MultipleChoiceField
|
5
|
+
from django.http import HttpRequest
|
3
6
|
from django.utils.translation import gettext_lazy as _
|
4
7
|
|
8
|
+
from unfold.admin import ModelAdmin
|
9
|
+
|
5
10
|
from ...widgets import (
|
6
11
|
INPUT_CLASSES,
|
7
12
|
UnfoldAdminSelectMultipleWidget,
|
@@ -12,7 +17,7 @@ from ...widgets import (
|
|
12
17
|
|
13
18
|
|
14
19
|
class SearchForm(forms.Form):
|
15
|
-
def __init__(self, name, label, *args, **kwargs):
|
20
|
+
def __init__(self, name: str, label: str, *args, **kwargs) -> None:
|
16
21
|
super().__init__(*args, **kwargs)
|
17
22
|
|
18
23
|
self.fields[name] = forms.CharField(
|
@@ -22,6 +27,50 @@ class SearchForm(forms.Form):
|
|
22
27
|
)
|
23
28
|
|
24
29
|
|
30
|
+
class AutocompleteDropdownForm(forms.Form):
|
31
|
+
field = forms.ModelChoiceField
|
32
|
+
widget = AutocompleteSelect
|
33
|
+
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
request: HttpRequest,
|
37
|
+
name: str,
|
38
|
+
label: str,
|
39
|
+
choices: tuple,
|
40
|
+
field: ModelField,
|
41
|
+
model_admin: ModelAdmin,
|
42
|
+
multiple: bool = False,
|
43
|
+
*args,
|
44
|
+
**kwargs,
|
45
|
+
) -> None:
|
46
|
+
super().__init__(*args, **kwargs)
|
47
|
+
|
48
|
+
if multiple:
|
49
|
+
self.field = ModelMultipleChoiceField
|
50
|
+
self.widget = AutocompleteSelectMultiple
|
51
|
+
|
52
|
+
self.fields[name] = self.field(
|
53
|
+
label=label,
|
54
|
+
required=False,
|
55
|
+
queryset=field.remote_field.model.objects.all(),
|
56
|
+
widget=self.widget(field, model_admin.admin_site),
|
57
|
+
)
|
58
|
+
|
59
|
+
class Media:
|
60
|
+
js = (
|
61
|
+
"admin/js/vendor/jquery/jquery.js",
|
62
|
+
"admin/js/vendor/select2/select2.full.js",
|
63
|
+
"admin/js/jquery.init.js",
|
64
|
+
"admin/js/autocomplete.js",
|
65
|
+
)
|
66
|
+
css = {
|
67
|
+
"screen": (
|
68
|
+
"admin/css/vendor/select2/select2.css",
|
69
|
+
"admin/css/autocomplete.css",
|
70
|
+
),
|
71
|
+
}
|
72
|
+
|
73
|
+
|
25
74
|
class DropdownForm(forms.Form):
|
26
75
|
widget = UnfoldAdminSelectWidget(
|
27
76
|
attrs={
|
@@ -31,7 +80,15 @@ class DropdownForm(forms.Form):
|
|
31
80
|
)
|
32
81
|
field = ChoiceField
|
33
82
|
|
34
|
-
def __init__(
|
83
|
+
def __init__(
|
84
|
+
self,
|
85
|
+
name: str,
|
86
|
+
label: str,
|
87
|
+
choices: tuple,
|
88
|
+
multiple: bool = False,
|
89
|
+
*args,
|
90
|
+
**kwargs,
|
91
|
+
) -> None:
|
35
92
|
super().__init__(*args, **kwargs)
|
36
93
|
|
37
94
|
if multiple:
|
@@ -66,8 +123,8 @@ class DropdownForm(forms.Form):
|
|
66
123
|
|
67
124
|
|
68
125
|
class SingleNumericForm(forms.Form):
|
69
|
-
def __init__(self, *args, **kwargs):
|
70
|
-
name =
|
126
|
+
def __init__(self, name: str, *args, **kwargs) -> None:
|
127
|
+
self.name = name
|
71
128
|
super().__init__(*args, **kwargs)
|
72
129
|
|
73
130
|
self.fields[name] = forms.FloatField(
|
@@ -80,10 +137,8 @@ class SingleNumericForm(forms.Form):
|
|
80
137
|
|
81
138
|
|
82
139
|
class RangeNumericForm(forms.Form):
|
83
|
-
name
|
84
|
-
|
85
|
-
def __init__(self, *args, **kwargs):
|
86
|
-
self.name = kwargs.pop("name")
|
140
|
+
def __init__(self, name: str, *args, **kwargs) -> None:
|
141
|
+
self.name = name
|
87
142
|
super().__init__(*args, **kwargs)
|
88
143
|
|
89
144
|
self.fields[self.name + "_from"] = forms.FloatField(
|
@@ -113,16 +168,14 @@ class SliderNumericForm(RangeNumericForm):
|
|
113
168
|
|
114
169
|
|
115
170
|
class RangeDateForm(forms.Form):
|
116
|
-
name = None
|
117
|
-
|
118
171
|
class Media:
|
119
172
|
js = [
|
120
173
|
"admin/js/calendar.js",
|
121
174
|
"unfold/filters/js/DateTimeShortcuts.js",
|
122
175
|
]
|
123
176
|
|
124
|
-
def __init__(self, *args, **kwargs):
|
125
|
-
self.name =
|
177
|
+
def __init__(self, name: str, *args, **kwargs) -> None:
|
178
|
+
self.name = name
|
126
179
|
super().__init__(*args, **kwargs)
|
127
180
|
|
128
181
|
self.fields[self.name + "_from"] = forms.DateField(
|
@@ -148,16 +201,14 @@ class RangeDateForm(forms.Form):
|
|
148
201
|
|
149
202
|
|
150
203
|
class RangeDateTimeForm(forms.Form):
|
151
|
-
name = None
|
152
|
-
|
153
204
|
class Media:
|
154
205
|
js = [
|
155
206
|
"admin/js/calendar.js",
|
156
207
|
"unfold/filters/js/DateTimeShortcuts.js",
|
157
208
|
]
|
158
209
|
|
159
|
-
def __init__(self, *args, **kwargs):
|
160
|
-
self.name =
|
210
|
+
def __init__(self, name: str, *args, **kwargs) -> None:
|
211
|
+
self.name = name
|
161
212
|
super().__init__(*args, **kwargs)
|
162
213
|
|
163
214
|
self.fields[self.name + "_from"] = forms.SplitDateTimeField(
|
unfold/contrib/forms/widgets.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any,
|
1
|
+
from typing import Any, Optional, Union
|
2
2
|
|
3
3
|
from django.core.validators import EMPTY_VALUES
|
4
4
|
from django.forms import MultiWidget, Widget
|
@@ -46,7 +46,7 @@ class ArrayWidget(MultiWidget):
|
|
46
46
|
|
47
47
|
return UnfoldAdminTextInputWidget()
|
48
48
|
|
49
|
-
def get_context(self, name: str, value: str, attrs:
|
49
|
+
def get_context(self, name: str, value: str, attrs: dict) -> dict:
|
50
50
|
self._resolve_widgets(value)
|
51
51
|
context = super().get_context(name, value, attrs)
|
52
52
|
context.update(
|
@@ -56,7 +56,7 @@ class ArrayWidget(MultiWidget):
|
|
56
56
|
|
57
57
|
def value_from_datadict(
|
58
58
|
self, data: QueryDict, files: MultiValueDict, name: str
|
59
|
-
) ->
|
59
|
+
) -> list:
|
60
60
|
values = []
|
61
61
|
|
62
62
|
for item in data.getlist(name):
|
@@ -67,22 +67,22 @@ class ArrayWidget(MultiWidget):
|
|
67
67
|
|
68
68
|
def value_omitted_from_data(
|
69
69
|
self, data: QueryDict, files: MultiValueDict, name: str
|
70
|
-
) ->
|
70
|
+
) -> list:
|
71
71
|
return data.getlist(name) not in [[""], *EMPTY_VALUES]
|
72
72
|
|
73
|
-
def decompress(self, value: Union[str,
|
74
|
-
if isinstance(value,
|
73
|
+
def decompress(self, value: Union[str, list]) -> list:
|
74
|
+
if isinstance(value, list):
|
75
75
|
return value
|
76
76
|
elif isinstance(value, str):
|
77
77
|
return value.split(",")
|
78
78
|
|
79
79
|
return []
|
80
80
|
|
81
|
-
def _resolve_widgets(self, value: Optional[Union[
|
81
|
+
def _resolve_widgets(self, value: Optional[Union[list, str]]) -> None:
|
82
82
|
if value is None:
|
83
83
|
value = []
|
84
84
|
|
85
|
-
elif isinstance(value,
|
85
|
+
elif isinstance(value, list):
|
86
86
|
self.widgets = [self.get_widget_instance() for item in value]
|
87
87
|
else:
|
88
88
|
self.widgets = [self.get_widget_instance() for item in value.split(",")]
|
@@ -101,7 +101,7 @@ class WysiwygWidget(Widget):
|
|
101
101
|
"unfold/forms/js/trix.config.js",
|
102
102
|
)
|
103
103
|
|
104
|
-
def __init__(self, attrs: Optional[
|
104
|
+
def __init__(self, attrs: Optional[dict[str, Any]] = None) -> None:
|
105
105
|
super().__init__(attrs)
|
106
106
|
|
107
107
|
self.attrs.update(
|
unfold/contrib/inlines/checks.py
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
from typing import List
|
2
|
-
|
3
1
|
from django.contrib.admin.checks import InlineModelAdminChecks
|
4
2
|
from django.contrib.admin.options import InlineModelAdmin
|
5
3
|
from django.core.checks import CheckMessage
|
@@ -9,10 +7,10 @@ from django.db.models import Model
|
|
9
7
|
class NonrelatedModelAdminChecks(InlineModelAdminChecks):
|
10
8
|
def _check_exclude_of_parent_model(
|
11
9
|
self, obj: InlineModelAdmin, parent_model: Model
|
12
|
-
) ->
|
10
|
+
) -> list[CheckMessage]:
|
13
11
|
return []
|
14
12
|
|
15
13
|
def _check_relation(
|
16
14
|
self, obj: InlineModelAdmin, parent_model: Model
|
17
|
-
) ->
|
15
|
+
) -> list[CheckMessage]:
|
18
16
|
return []
|
@@ -9,8 +9,24 @@
|
|
9
9
|
</p>
|
10
10
|
{% endif %}
|
11
11
|
|
12
|
-
{% if
|
12
|
+
{% if page_obj.object_list %}
|
13
13
|
{% include object_history_list_template %}
|
14
|
+
<div class="bg-base-50 flex my-4 items-center p-3 rounded dark:bg-base-800">
|
15
|
+
{% if pagination_required %}
|
16
|
+
{% for i in page_range %}
|
17
|
+
<span class="pr-4">
|
18
|
+
{% if i == page_obj.paginator.ELLIPSIS %}
|
19
|
+
{{ page_obj.paginator.ELLIPSIS }}
|
20
|
+
{% elif i == page_obj.number %}
|
21
|
+
<span class="font-medium text-primary-600">{{ i }}</span>
|
22
|
+
{% else %}
|
23
|
+
<a href="?{{ page_var }}={{ i }}" {% if i == page_obj.paginator.num_pages %} class="end" {% endif %}>{{ i }}</a>
|
24
|
+
{% endif %}
|
25
|
+
</span>
|
26
|
+
{% endfor %}
|
27
|
+
{% endif %}
|
28
|
+
{{ page_obj.paginator.count }} {% blocktranslate count counter=page_obj.paginator.count %}entry{% plural %}entries{% endblocktranslate %}
|
29
|
+
</div>
|
14
30
|
{% else %}
|
15
31
|
<p class="mb-3 px-3 py-3 rounded text-sm last:mb-8 bg-amber-100 text-amber-600 dark:bg-amber-600/20 dark:border-amber-600/10">
|
16
32
|
{% trans "This object doesn't have a change history." %}
|
@@ -39,7 +39,7 @@
|
|
39
39
|
</thead>
|
40
40
|
|
41
41
|
<tbody>
|
42
|
-
{% for record in
|
42
|
+
{% for record in page_obj %}
|
43
43
|
<tr class="block border mb-3 rounded shadow-sm lg:table-row lg:border-none lg:mb-0 lg:shadow-none dark:border-base-800">
|
44
44
|
<td class="align-middle flex border-t border-base-200 font-normal px-3 py-2 text-left before:flex before:capitalize before:content-[attr(data-label)] before:items-center before:mr-auto first:border-t-0 lg:before:hidden lg:first:border-t lg:py-3 lg:table-cell dark:border-base-800" data-label="{% trans 'Object' %}">
|
45
45
|
<a href="{% url opts|admin_urlname:'simple_history' object.pk record.pk %}">
|
unfold/dataclasses.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
-
from typing import Callable,
|
2
|
+
from typing import Callable, Optional, Union
|
3
3
|
|
4
4
|
from .typing import ActionFunction
|
5
5
|
|
@@ -10,7 +10,7 @@ class UnfoldAction:
|
|
10
10
|
method: ActionFunction
|
11
11
|
description: str
|
12
12
|
path: str
|
13
|
-
attrs: Optional[
|
13
|
+
attrs: Optional[dict] = None
|
14
14
|
object_id: Optional[Union[int, str]] = None
|
15
15
|
|
16
16
|
|
unfold/decorators.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
from
|
1
|
+
from collections.abc import Iterable
|
2
|
+
from typing import Any, Callable, Optional, Union
|
2
3
|
|
3
4
|
from django.contrib.admin.options import BaseModelAdmin
|
4
5
|
from django.core.exceptions import PermissionDenied
|
@@ -15,7 +16,7 @@ def action(
|
|
15
16
|
permissions: Optional[Iterable[str]] = None,
|
16
17
|
description: Optional[str] = None,
|
17
18
|
url_path: Optional[str] = None,
|
18
|
-
attrs: Optional[
|
19
|
+
attrs: Optional[dict[str, Any]] = None,
|
19
20
|
) -> ActionFunction:
|
20
21
|
def decorator(func: Callable) -> ActionFunction:
|
21
22
|
def inner(
|
@@ -62,7 +63,7 @@ def display(
|
|
62
63
|
ordering: Optional[Union[str, Combinable, BaseExpression]] = None,
|
63
64
|
description: Optional[str] = None,
|
64
65
|
empty_value: Optional[str] = None,
|
65
|
-
label: Optional[Union[bool, str,
|
66
|
+
label: Optional[Union[bool, str, dict[str, str]]] = None,
|
66
67
|
header: Optional[bool] = None,
|
67
68
|
) -> Callable:
|
68
69
|
def decorator(func: Callable[[Model], Any]) -> Callable:
|
unfold/settings.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Any
|
1
|
+
from typing import Any
|
2
2
|
|
3
3
|
from django.conf import settings
|
4
4
|
|
@@ -72,7 +72,7 @@ def get_config(settings_name=None):
|
|
72
72
|
if settings_name is None:
|
73
73
|
settings_name = "UNFOLD"
|
74
74
|
|
75
|
-
def merge_dicts(dict1:
|
75
|
+
def merge_dicts(dict1: dict[str, Any], dict2: dict[str, Any]) -> dict[str, Any]:
|
76
76
|
result = dict1.copy()
|
77
77
|
|
78
78
|
for key, value in dict2.items():
|