django-unfold 0.46.0__py3-none-any.whl → 0.47.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- {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():
|