django-unfold 0.46.0__py3-none-any.whl → 0.48.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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")