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.
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/METADATA +5 -6
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.0.dist-info}/RECORD +52 -43
- {django_unfold-0.46.0.dist-info → django_unfold-0.48.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 +9 -2
- unfold/decorators.py +4 -3
- unfold/settings.py +4 -2
- unfold/sites.py +176 -140
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/app.js +2 -2
- unfold/templates/admin/app_index.html +1 -5
- unfold/templates/admin/base_site.html +1 -1
- unfold/templates/admin/filter.html +1 -1
- unfold/templates/admin/index.html +1 -5
- unfold/templates/admin/login.html +1 -1
- unfold/templates/admin/search_form.html +4 -2
- unfold/templates/unfold/helpers/account_links.html +1 -1
- unfold/templates/unfold/helpers/actions_row.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/language_switch.html +1 -1
- unfold/templates/unfold/helpers/navigation_header.html +15 -5
- unfold/templates/unfold/helpers/site_branding.html +9 -0
- unfold/templates/unfold/helpers/site_dropdown.html +19 -0
- unfold/templates/unfold/helpers/site_icon.html +10 -2
- unfold/templates/unfold/helpers/tab_list.html +7 -1
- unfold/templates/unfold/helpers/theme_switch.html +1 -1
- unfold/templates/unfold/layouts/base.html +1 -5
- 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.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")
|