django-unfold 0.46.0__py3-none-any.whl → 0.48.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.
Files changed (52) hide show
  1. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/METADATA +5 -6
  2. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/RECORD +52 -43
  3. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/WHEEL +1 -1
  4. unfold/admin.py +15 -16
  5. unfold/checks.py +4 -4
  6. unfold/components.py +5 -5
  7. unfold/contrib/filters/admin/__init__.py +43 -0
  8. unfold/contrib/filters/admin/autocomplete_filters.py +16 -0
  9. unfold/contrib/filters/admin/datetime_filters.py +212 -0
  10. unfold/contrib/filters/admin/dropdown_filters.py +100 -0
  11. unfold/contrib/filters/admin/mixins.py +146 -0
  12. unfold/contrib/filters/admin/numeric_filters.py +196 -0
  13. unfold/contrib/filters/admin/text_filters.py +65 -0
  14. unfold/contrib/filters/admin.py +32 -32
  15. unfold/contrib/filters/forms.py +68 -17
  16. unfold/contrib/forms/widgets.py +9 -9
  17. unfold/contrib/inlines/checks.py +2 -4
  18. unfold/contrib/simple_history/templates/simple_history/object_history.html +17 -1
  19. unfold/contrib/simple_history/templates/simple_history/object_history_list.html +1 -1
  20. unfold/dataclasses.py +9 -2
  21. unfold/decorators.py +4 -3
  22. unfold/settings.py +4 -2
  23. unfold/sites.py +176 -140
  24. unfold/static/unfold/css/styles.css +1 -1
  25. unfold/static/unfold/js/app.js +2 -2
  26. unfold/templates/admin/app_index.html +1 -5
  27. unfold/templates/admin/base_site.html +1 -1
  28. unfold/templates/admin/filter.html +1 -1
  29. unfold/templates/admin/index.html +1 -5
  30. unfold/templates/admin/login.html +1 -1
  31. unfold/templates/admin/search_form.html +4 -2
  32. unfold/templates/unfold/helpers/account_links.html +1 -1
  33. unfold/templates/unfold/helpers/actions_row.html +1 -1
  34. unfold/templates/unfold/helpers/change_list_filter.html +2 -2
  35. unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
  36. unfold/templates/unfold/helpers/header_back_button.html +2 -2
  37. unfold/templates/unfold/helpers/language_switch.html +1 -1
  38. unfold/templates/unfold/helpers/navigation_header.html +15 -5
  39. unfold/templates/unfold/helpers/site_branding.html +9 -0
  40. unfold/templates/unfold/helpers/site_dropdown.html +19 -0
  41. unfold/templates/unfold/helpers/site_icon.html +10 -2
  42. unfold/templates/unfold/helpers/tab_list.html +7 -1
  43. unfold/templates/unfold/helpers/theme_switch.html +1 -1
  44. unfold/templates/unfold/layouts/base.html +1 -5
  45. unfold/templates/unfold/layouts/skeleton.html +1 -1
  46. unfold/templatetags/unfold.py +55 -22
  47. unfold/templatetags/unfold_list.py +2 -2
  48. unfold/typing.py +5 -4
  49. unfold/utils.py +3 -2
  50. unfold/views.py +2 -2
  51. unfold/widgets.py +27 -27
  52. {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/LICENSE.md +0 -0
@@ -0,0 +1,212 @@
1
+ from typing import Any
2
+
3
+ from django.contrib import admin
4
+ from django.contrib.admin.options import ModelAdmin
5
+ from django.contrib.admin.views.main import ChangeList
6
+ from django.core.validators import EMPTY_VALUES
7
+ from django.db.models import Model, QuerySet
8
+ from django.db.models.fields import (
9
+ DateField,
10
+ DateTimeField,
11
+ Field,
12
+ )
13
+ from django.forms import ValidationError
14
+ from django.http import HttpRequest
15
+
16
+ from unfold.contrib.filters.forms import (
17
+ RangeDateForm,
18
+ RangeDateTimeForm,
19
+ )
20
+ from unfold.utils import parse_date_str, parse_datetime_str
21
+
22
+
23
+ class RangeDateFilter(admin.FieldListFilter):
24
+ request = None
25
+ parameter_name = None
26
+ form_class = RangeDateForm
27
+ template = "unfold/filters/filters_date_range.html"
28
+
29
+ def __init__(
30
+ self,
31
+ field: Field,
32
+ request: HttpRequest,
33
+ params: dict[str, str],
34
+ model: type[Model],
35
+ model_admin: ModelAdmin,
36
+ field_path: str,
37
+ ) -> None:
38
+ super().__init__(field, request, params, model, model_admin, field_path)
39
+ if not isinstance(field, DateField):
40
+ raise TypeError(
41
+ f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
42
+ )
43
+
44
+ self.request = request
45
+ if self.parameter_name is None:
46
+ self.parameter_name = self.field_path
47
+
48
+ if self.parameter_name + "_from" in params:
49
+ value = params.pop(self.field_path + "_from")
50
+ value = value[0] if isinstance(value, list) else value
51
+
52
+ if value not in EMPTY_VALUES:
53
+ self.used_parameters[self.field_path + "_from"] = value
54
+
55
+ if self.parameter_name + "_to" in params:
56
+ value = params.pop(self.field_path + "_to")
57
+ value = value[0] if isinstance(value, list) else value
58
+
59
+ if value not in EMPTY_VALUES:
60
+ self.used_parameters[self.field_path + "_to"] = value
61
+
62
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
63
+ filters = {}
64
+
65
+ value_from = self.used_parameters.get(self.parameter_name + "_from")
66
+ if value_from not in EMPTY_VALUES:
67
+ filters.update({self.parameter_name + "__gte": parse_date_str(value_from)})
68
+
69
+ value_to = self.used_parameters.get(self.parameter_name + "_to")
70
+ if value_to not in EMPTY_VALUES:
71
+ filters.update({self.parameter_name + "__lte": parse_date_str(value_to)})
72
+
73
+ try:
74
+ return queryset.filter(**filters)
75
+ except (ValueError, ValidationError):
76
+ return None
77
+
78
+ def expected_parameters(self) -> list[str]:
79
+ return [
80
+ f"{self.parameter_name}_from",
81
+ f"{self.parameter_name}_to",
82
+ ]
83
+
84
+ def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
85
+ return (
86
+ {
87
+ "request": self.request,
88
+ "parameter_name": self.parameter_name,
89
+ "form": self.form_class(
90
+ name=self.parameter_name,
91
+ data={
92
+ self.parameter_name + "_from": self.used_parameters.get(
93
+ self.parameter_name + "_from", None
94
+ ),
95
+ self.parameter_name + "_to": self.used_parameters.get(
96
+ self.parameter_name + "_to", None
97
+ ),
98
+ },
99
+ ),
100
+ },
101
+ )
102
+
103
+
104
+ class RangeDateTimeFilter(admin.FieldListFilter):
105
+ request = None
106
+ parameter_name = None
107
+ template = "unfold/filters/filters_datetime_range.html"
108
+ form_class = RangeDateTimeForm
109
+
110
+ def __init__(
111
+ self,
112
+ field: Field,
113
+ request: HttpRequest,
114
+ params: dict[str, str],
115
+ model: type[Model],
116
+ model_admin: ModelAdmin,
117
+ field_path: str,
118
+ ) -> None:
119
+ super().__init__(field, request, params, model, model_admin, field_path)
120
+ if not isinstance(field, DateTimeField):
121
+ raise TypeError(
122
+ f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
123
+ )
124
+
125
+ self.request = request
126
+ if self.parameter_name is None:
127
+ self.parameter_name = self.field_path
128
+
129
+ if self.parameter_name + "_from_0" in params:
130
+ value = params.pop(self.field_path + "_from_0")
131
+ value = value[0] if isinstance(value, list) else value
132
+ self.used_parameters[self.field_path + "_from_0"] = value
133
+
134
+ if self.parameter_name + "_from_1" in params:
135
+ value = params.pop(self.field_path + "_from_1")
136
+ value = value[0] if isinstance(value, list) else value
137
+ self.used_parameters[self.field_path + "_from_1"] = value
138
+
139
+ if self.parameter_name + "_to_0" in params:
140
+ value = params.pop(self.field_path + "_to_0")
141
+ value = value[0] if isinstance(value, list) else value
142
+ self.used_parameters[self.field_path + "_to_0"] = value
143
+
144
+ if self.parameter_name + "_to_1" in params:
145
+ value = params.pop(self.field_path + "_to_1")
146
+ value = value[0] if isinstance(value, list) else value
147
+ self.used_parameters[self.field_path + "_to_1"] = value
148
+
149
+ def expected_parameters(self) -> list[str]:
150
+ return [
151
+ f"{self.parameter_name}_from_0",
152
+ f"{self.parameter_name}_from_1",
153
+ f"{self.parameter_name}_to_0",
154
+ f"{self.parameter_name}_to_1",
155
+ ]
156
+
157
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
158
+ filters = {}
159
+
160
+ date_value_from = self.used_parameters.get(self.parameter_name + "_from_0")
161
+ time_value_from = self.used_parameters.get(self.parameter_name + "_from_1")
162
+
163
+ date_value_to = self.used_parameters.get(self.parameter_name + "_to_0")
164
+ time_value_to = self.used_parameters.get(self.parameter_name + "_to_1")
165
+
166
+ if date_value_from not in EMPTY_VALUES and time_value_from not in EMPTY_VALUES:
167
+ filters.update(
168
+ {
169
+ f"{self.parameter_name}__gte": parse_datetime_str(
170
+ f"{date_value_from} {time_value_from}"
171
+ ),
172
+ }
173
+ )
174
+
175
+ if date_value_to not in EMPTY_VALUES and time_value_to not in EMPTY_VALUES:
176
+ filters.update(
177
+ {
178
+ f"{self.parameter_name}__lte": parse_datetime_str(
179
+ f"{date_value_to} {time_value_to}"
180
+ ),
181
+ }
182
+ )
183
+
184
+ try:
185
+ return queryset.filter(**filters)
186
+ except (ValueError, ValidationError):
187
+ return None
188
+
189
+ def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
190
+ return (
191
+ {
192
+ "request": self.request,
193
+ "parameter_name": self.parameter_name,
194
+ "form": self.form_class(
195
+ name=self.parameter_name,
196
+ data={
197
+ self.parameter_name + "_from_0": self.used_parameters.get(
198
+ self.parameter_name + "_from_0"
199
+ ),
200
+ self.parameter_name + "_from_1": self.used_parameters.get(
201
+ self.parameter_name + "_from_1"
202
+ ),
203
+ self.parameter_name + "_to_0": self.used_parameters.get(
204
+ self.parameter_name + "_to_0"
205
+ ),
206
+ self.parameter_name + "_to_1": self.used_parameters.get(
207
+ self.parameter_name + "_to_1"
208
+ ),
209
+ },
210
+ ),
211
+ },
212
+ )
@@ -0,0 +1,100 @@
1
+ from collections.abc import Generator
2
+ from typing import Any
3
+
4
+ from django.contrib import admin
5
+ from django.contrib.admin.views.main import ChangeList
6
+ from django.db.models import Field, Model
7
+ from django.http import HttpRequest
8
+ from django.utils.translation import gettext_lazy as _
9
+
10
+ from unfold.admin import ModelAdmin
11
+ from unfold.contrib.filters.admin.mixins import (
12
+ DropdownMixin,
13
+ MultiValueMixin,
14
+ ValueMixin,
15
+ )
16
+ from unfold.contrib.filters.forms import DropdownForm
17
+
18
+
19
+ class DropdownFilter(admin.SimpleListFilter):
20
+ template = "unfold/filters/filters_field.html"
21
+ form_class = DropdownForm
22
+ all_option = ["", _("All")]
23
+
24
+ def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
25
+ return (
26
+ {
27
+ "form": self.form_class(
28
+ label=_("By %(filter_title)s") % {"filter_title": self.title},
29
+ name=self.parameter_name,
30
+ choices=[self.all_option, *self.lookup_choices],
31
+ data={self.parameter_name: self.value()},
32
+ multiple=self.multiple if hasattr(self, "multiple") else False,
33
+ ),
34
+ },
35
+ )
36
+
37
+
38
+ class MultipleDropdownFilter(DropdownFilter):
39
+ multiple = True
40
+
41
+ def __init__(
42
+ self,
43
+ request: HttpRequest,
44
+ params: dict[str, Any],
45
+ model: type[Model],
46
+ model_admin: ModelAdmin,
47
+ ) -> None:
48
+ self.request = request
49
+ super().__init__(request, params, model, model_admin)
50
+
51
+ def value(self) -> list[Any]:
52
+ return self.request.GET.getlist(self.parameter_name)
53
+
54
+
55
+ class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFilter):
56
+ def choices(self, changelist: ChangeList) -> Generator[dict[str, Any], None, None]:
57
+ choices = [self.all_option, *self.field.flatchoices]
58
+
59
+ yield {
60
+ "form": self.form_class(
61
+ label=_("By %(filter_title)s") % {"filter_title": self.title},
62
+ name=self.lookup_kwarg,
63
+ choices=choices,
64
+ data={self.lookup_kwarg: self.value()},
65
+ multiple=self.multiple if hasattr(self, "multiple") else False,
66
+ ),
67
+ }
68
+
69
+
70
+ class MultipleChoicesDropdownFilter(MultiValueMixin, ChoicesDropdownFilter):
71
+ multiple = True
72
+
73
+
74
+ class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFilter):
75
+ def __init__(
76
+ self,
77
+ field: Field,
78
+ request: HttpRequest,
79
+ params: dict[str, str],
80
+ model: type[Model],
81
+ model_admin: ModelAdmin,
82
+ field_path: str,
83
+ ) -> None:
84
+ super().__init__(field, request, params, model, model_admin, field_path)
85
+ self.model_admin = model_admin
86
+
87
+ def choices(self, changelist: ChangeList) -> Generator[dict[str, Any], None, None]:
88
+ yield {
89
+ "form": self.form_class(
90
+ label=_("By %(filter_title)s") % {"filter_title": self.title},
91
+ name=self.lookup_kwarg,
92
+ choices=[self.all_option, *self.lookup_choices],
93
+ data={self.lookup_kwarg: self.value()},
94
+ multiple=self.multiple if hasattr(self, "multiple") else False,
95
+ ),
96
+ }
97
+
98
+
99
+ class MultipleRelatedDropdownFilter(MultiValueMixin, RelatedDropdownFilter):
100
+ multiple = True
@@ -0,0 +1,146 @@
1
+ from collections.abc import Generator
2
+ from typing import Any, Optional
3
+
4
+ from django.contrib.admin.views.main import ChangeList
5
+ from django.core.validators import EMPTY_VALUES
6
+ from django.db.models import QuerySet
7
+ from django.forms import ValidationError
8
+ from django.http import HttpRequest
9
+ from django.utils.translation import gettext_lazy as _
10
+
11
+ from unfold.contrib.filters.forms import (
12
+ AutocompleteDropdownForm,
13
+ DropdownForm,
14
+ RangeNumericForm,
15
+ )
16
+
17
+
18
+ class ValueMixin:
19
+ def value(self) -> Optional[str]:
20
+ return (
21
+ self.lookup_val[0]
22
+ if self.lookup_val not in EMPTY_VALUES
23
+ and isinstance(self.lookup_val, list)
24
+ and len(self.lookup_val) > 0
25
+ else self.lookup_val
26
+ )
27
+
28
+
29
+ class MultiValueMixin:
30
+ def value(self) -> Optional[list[str]]:
31
+ return (
32
+ self.lookup_val
33
+ if self.lookup_val not in EMPTY_VALUES
34
+ and isinstance(self.lookup_val, list)
35
+ and len(self.lookup_val) > 0
36
+ else self.lookup_val
37
+ )
38
+
39
+
40
+ class DropdownMixin:
41
+ template = "unfold/filters/filters_field.html"
42
+ form_class = DropdownForm
43
+ all_option = ["", _("All")]
44
+
45
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
46
+ if self.value() not in EMPTY_VALUES:
47
+ return super().queryset(request, queryset)
48
+
49
+ return queryset
50
+
51
+
52
+ class RangeNumericMixin:
53
+ request = None
54
+ parameter_name = None
55
+ template = "unfold/filters/filters_numeric_range.html"
56
+
57
+ def init_used_parameters(self, params: dict[str, Any]) -> None:
58
+ if self.parameter_name + "_from" in params:
59
+ value = params.pop(self.parameter_name + "_from")
60
+
61
+ self.used_parameters[self.parameter_name + "_from"] = (
62
+ value[0] if isinstance(value, list) else value
63
+ )
64
+
65
+ if self.parameter_name + "_to" in params:
66
+ value = params.pop(self.parameter_name + "_to")
67
+ self.used_parameters[self.parameter_name + "_to"] = (
68
+ value[0] if isinstance(value, list) else value
69
+ )
70
+
71
+ def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
72
+ filters = {}
73
+
74
+ value_from = self.used_parameters.get(self.parameter_name + "_from", None)
75
+ if value_from is not None and value_from != "":
76
+ filters.update(
77
+ {
78
+ self.parameter_name + "__gte": self.used_parameters.get(
79
+ self.parameter_name + "_from", None
80
+ ),
81
+ }
82
+ )
83
+
84
+ value_to = self.used_parameters.get(self.parameter_name + "_to", None)
85
+ if value_to is not None and value_to != "":
86
+ filters.update(
87
+ {
88
+ self.parameter_name + "__lte": self.used_parameters.get(
89
+ self.parameter_name + "_to", None
90
+ ),
91
+ }
92
+ )
93
+
94
+ try:
95
+ return queryset.filter(**filters)
96
+ except (ValueError, ValidationError):
97
+ return None
98
+
99
+ def expected_parameters(self) -> list[str]:
100
+ return [
101
+ f"{self.parameter_name}_from",
102
+ f"{self.parameter_name}_to",
103
+ ]
104
+
105
+ def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
106
+ return (
107
+ {
108
+ "request": self.request,
109
+ "parameter_name": self.parameter_name,
110
+ "form": RangeNumericForm(
111
+ name=self.parameter_name,
112
+ data={
113
+ self.parameter_name + "_from": self.used_parameters.get(
114
+ self.parameter_name + "_from", None
115
+ ),
116
+ self.parameter_name + "_to": self.used_parameters.get(
117
+ self.parameter_name + "_to", None
118
+ ),
119
+ },
120
+ ),
121
+ },
122
+ )
123
+
124
+
125
+ class AutocompleteMixin:
126
+ def __init__(self, *args, **kwargs) -> None:
127
+ super().__init__(*args, **kwargs)
128
+
129
+ if "request" in kwargs:
130
+ self.request = kwargs["request"]
131
+
132
+ def choices(
133
+ self, changelist: ChangeList
134
+ ) -> Generator[dict[str, AutocompleteDropdownForm], None, None]:
135
+ yield {
136
+ "form": self.form_class(
137
+ request=self.request,
138
+ label=_("By %(filter_title)s") % {"filter_title": self.title},
139
+ name=self.lookup_kwarg,
140
+ choices=(),
141
+ field=self.field,
142
+ model_admin=self.model_admin,
143
+ data={self.lookup_kwarg: self.value()},
144
+ multiple=self.multiple if hasattr(self, "multiple") else False,
145
+ ),
146
+ }
@@ -0,0 +1,196 @@
1
+ from typing import Any, Optional
2
+
3
+ from django.contrib import admin
4
+ from django.contrib.admin.options import ModelAdmin
5
+ from django.contrib.admin.views.main import ChangeList
6
+ from django.core.validators import EMPTY_VALUES
7
+ from django.db.models import Max, Min, Model, QuerySet
8
+ from django.db.models.fields import (
9
+ AutoField,
10
+ DecimalField,
11
+ Field,
12
+ FloatField,
13
+ IntegerField,
14
+ )
15
+ from django.forms import ValidationError
16
+ from django.http import HttpRequest
17
+
18
+ from unfold.contrib.filters.admin.mixins import RangeNumericMixin
19
+ from unfold.contrib.filters.forms import SingleNumericForm, SliderNumericForm
20
+
21
+
22
+ class SingleNumericFilter(admin.FieldListFilter):
23
+ request = None
24
+ parameter_name = None
25
+ template = "unfold/filters/filters_numeric_single.html"
26
+
27
+ def __init__(
28
+ self,
29
+ field: Field,
30
+ request: HttpRequest,
31
+ params: dict[str, str],
32
+ model: type[Model],
33
+ model_admin: ModelAdmin,
34
+ field_path: str,
35
+ ) -> None:
36
+ super().__init__(field, request, params, model, model_admin, field_path)
37
+
38
+ if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
39
+ raise TypeError(
40
+ f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
41
+ )
42
+
43
+ self.request = request
44
+
45
+ if self.parameter_name is None:
46
+ self.parameter_name = self.field_path
47
+
48
+ if self.parameter_name in params:
49
+ value = params.pop(self.parameter_name)
50
+ value = value[0] if isinstance(value, list) else value
51
+
52
+ if value not in EMPTY_VALUES:
53
+ self.used_parameters[self.parameter_name] = value
54
+
55
+ def queryset(
56
+ self, request: HttpRequest, queryset: QuerySet[Any]
57
+ ) -> Optional[QuerySet]:
58
+ if self.value():
59
+ try:
60
+ return queryset.filter(**{self.parameter_name: self.value()})
61
+ except (ValueError, ValidationError):
62
+ return None
63
+
64
+ def value(self) -> Any:
65
+ return self.used_parameters.get(self.parameter_name, None)
66
+
67
+ def expected_parameters(self) -> list[Optional[str]]:
68
+ return [self.parameter_name]
69
+
70
+ def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
71
+ return (
72
+ {
73
+ "request": self.request,
74
+ "parameter_name": self.parameter_name,
75
+ "form": SingleNumericForm(
76
+ name=self.parameter_name, data={self.parameter_name: self.value()}
77
+ ),
78
+ },
79
+ )
80
+
81
+
82
+ class RangeNumericListFilter(RangeNumericMixin, admin.SimpleListFilter):
83
+ def __init__(
84
+ self,
85
+ request: HttpRequest,
86
+ params: dict[str, str],
87
+ model: type[Model],
88
+ model_admin: ModelAdmin,
89
+ ) -> None:
90
+ super().__init__(request, params, model, model_admin)
91
+ if not self.parameter_name:
92
+ raise ValueError("Parameter name cannot be None")
93
+
94
+ self.request = request
95
+ self.init_used_parameters(params)
96
+
97
+ def lookups(
98
+ self, request: HttpRequest, model_admin: ModelAdmin
99
+ ) -> tuple[tuple[str, str], ...]:
100
+ return (("dummy", "dummy"),)
101
+
102
+
103
+ class RangeNumericFilter(RangeNumericMixin, admin.FieldListFilter):
104
+ def __init__(
105
+ self,
106
+ field: Field,
107
+ request: HttpRequest,
108
+ params: dict[str, str],
109
+ model: type[Model],
110
+ model_admin: ModelAdmin,
111
+ field_path: str,
112
+ ) -> None:
113
+ super().__init__(field, request, params, model, model_admin, field_path)
114
+ if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
115
+ raise TypeError(
116
+ f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
117
+ )
118
+
119
+ self.request = request
120
+ if self.parameter_name is None:
121
+ self.parameter_name = self.field_path
122
+
123
+ self.init_used_parameters(params)
124
+
125
+
126
+ class SliderNumericFilter(RangeNumericFilter):
127
+ MAX_DECIMALS = 7
128
+ STEP = None
129
+
130
+ template = "unfold/filters/filters_numeric_slider.html"
131
+ field = None
132
+ form_class = SliderNumericForm
133
+
134
+ def __init__(
135
+ self,
136
+ field: Field,
137
+ request: HttpRequest,
138
+ params: dict[str, str],
139
+ model: type[Model],
140
+ model_admin: ModelAdmin,
141
+ field_path: str,
142
+ ) -> None:
143
+ super().__init__(field, request, params, model, model_admin, field_path)
144
+
145
+ self.field = field
146
+ self.q = model_admin.get_queryset(request)
147
+
148
+ def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
149
+ total = self.q.all().count()
150
+ min_value = self.q.all().aggregate(min=Min(self.parameter_name)).get("min", 0)
151
+
152
+ if total > 1:
153
+ max_value = (
154
+ self.q.all().aggregate(max=Max(self.parameter_name)).get("max", 0)
155
+ )
156
+ else:
157
+ max_value = None
158
+
159
+ if isinstance(self.field, (FloatField, DecimalField)):
160
+ decimals = self.MAX_DECIMALS
161
+ step = self.STEP if self.STEP else self._get_min_step(self.MAX_DECIMALS)
162
+ else:
163
+ decimals = 0
164
+ step = self.STEP if self.STEP else 1
165
+
166
+ return (
167
+ {
168
+ "decimals": decimals,
169
+ "step": step,
170
+ "parameter_name": self.parameter_name,
171
+ "request": self.request,
172
+ "min": min_value,
173
+ "max": max_value,
174
+ "value_from": self.used_parameters.get(
175
+ self.parameter_name + "_from", min_value
176
+ ),
177
+ "value_to": self.used_parameters.get(
178
+ self.parameter_name + "_to", max_value
179
+ ),
180
+ "form": self.form_class(
181
+ name=self.parameter_name,
182
+ data={
183
+ self.parameter_name + "_from": self.used_parameters.get(
184
+ self.parameter_name + "_from", min_value
185
+ ),
186
+ self.parameter_name + "_to": self.used_parameters.get(
187
+ self.parameter_name + "_to", max_value
188
+ ),
189
+ },
190
+ ),
191
+ },
192
+ )
193
+
194
+ def _get_min_step(self, precision: int) -> float:
195
+ result_format = f"{{:.{precision - 1}f}}"
196
+ return float(result_format.format(0) + "1")