django-unfold 0.49.1__py3-none-any.whl → 0.51.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.51.0.dist-info/METADATA +81 -0
- {django_unfold-0.49.1.dist-info → django_unfold-0.51.0.dist-info}/RECORD +34 -30
- {django_unfold-0.49.1.dist-info → django_unfold-0.51.0.dist-info}/WHEEL +1 -1
- unfold/admin.py +4 -0
- unfold/contrib/filters/admin/dropdown_filters.py +1 -0
- unfold/contrib/filters/admin/mixins.py +10 -4
- unfold/contrib/filters/forms.py +2 -2
- unfold/dataclasses.py +3 -0
- unfold/decorators.py +8 -0
- unfold/enums.py +10 -0
- unfold/mixins/action_model_admin.py +4 -0
- unfold/sections.py +82 -0
- unfold/settings.py +1 -0
- unfold/sites.py +3 -0
- unfold/static/admin/js/inlines.js +18 -5
- unfold/static/unfold/css/styles.css +1 -1
- unfold/static/unfold/js/select2.init.js +29 -0
- unfold/styles.css +1 -1
- unfold/templates/admin/change_list.html +16 -16
- unfold/templates/admin/change_list_results.html +38 -40
- unfold/templates/admin/filter.html +2 -2
- unfold/templates/unfold/components/card.html +1 -1
- unfold/templates/unfold/components/table.html +42 -26
- unfold/templates/unfold/helpers/change_list_filter_actions.html +1 -1
- unfold/templates/unfold/helpers/empty_results.html +35 -0
- unfold/templates/unfold/helpers/tab_action.html +24 -19
- unfold/templates/unfold/helpers/tab_actions.html +19 -0
- unfold/templates/unfold/helpers/tab_items.html +47 -0
- unfold/templates/unfold/helpers/tab_list.html +3 -66
- unfold/templates/unfold/layouts/skeleton.html +1 -2
- unfold/templatetags/unfold.py +81 -2
- unfold/templatetags/unfold_list.py +10 -10
- unfold/views.py +14 -1
- django_unfold-0.49.1.dist-info/METADATA +0 -75
- unfold/contrib/filters/admin.py +0 -619
- {django_unfold-0.49.1.dist-info → django_unfold-0.51.0.dist-info}/LICENSE.md +0 -0
unfold/contrib/filters/admin.py
DELETED
@@ -1,619 +0,0 @@
|
|
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
|
-
DateField,
|
11
|
-
DateTimeField,
|
12
|
-
DecimalField,
|
13
|
-
Field,
|
14
|
-
FloatField,
|
15
|
-
IntegerField,
|
16
|
-
)
|
17
|
-
from django.forms import ValidationError
|
18
|
-
from django.http import HttpRequest
|
19
|
-
from django.utils.translation import gettext_lazy as _
|
20
|
-
|
21
|
-
from unfold.utils import parse_date_str, parse_datetime_str
|
22
|
-
|
23
|
-
from .forms import (
|
24
|
-
DropdownForm,
|
25
|
-
RangeDateForm,
|
26
|
-
RangeDateTimeForm,
|
27
|
-
RangeNumericForm,
|
28
|
-
SearchForm,
|
29
|
-
SingleNumericForm,
|
30
|
-
SliderNumericForm,
|
31
|
-
)
|
32
|
-
|
33
|
-
|
34
|
-
class ValueMixin:
|
35
|
-
def value(self) -> Optional[str]:
|
36
|
-
return (
|
37
|
-
self.lookup_val[0]
|
38
|
-
if self.lookup_val not in EMPTY_VALUES
|
39
|
-
and isinstance(self.lookup_val, list)
|
40
|
-
and len(self.lookup_val) > 0
|
41
|
-
else self.lookup_val
|
42
|
-
)
|
43
|
-
|
44
|
-
|
45
|
-
class MultiValueMixin:
|
46
|
-
def value(self) -> Optional[list[str]]:
|
47
|
-
return (
|
48
|
-
self.lookup_val
|
49
|
-
if self.lookup_val not in EMPTY_VALUES
|
50
|
-
and isinstance(self.lookup_val, list)
|
51
|
-
and len(self.lookup_val) > 0
|
52
|
-
else self.lookup_val
|
53
|
-
)
|
54
|
-
|
55
|
-
|
56
|
-
class DropdownMixin:
|
57
|
-
template = "unfold/filters/filters_field.html"
|
58
|
-
form_class = DropdownForm
|
59
|
-
all_option = ["", _("All")]
|
60
|
-
|
61
|
-
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
|
62
|
-
if self.value() not in EMPTY_VALUES:
|
63
|
-
return super().queryset(request, queryset)
|
64
|
-
|
65
|
-
return queryset
|
66
|
-
|
67
|
-
|
68
|
-
class TextFilter(admin.SimpleListFilter):
|
69
|
-
template = "unfold/filters/filters_field.html"
|
70
|
-
form_class = SearchForm
|
71
|
-
|
72
|
-
def has_output(self) -> bool:
|
73
|
-
return True
|
74
|
-
|
75
|
-
def lookups(self, request: HttpRequest, model_admin: ModelAdmin) -> tuple:
|
76
|
-
return ()
|
77
|
-
|
78
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
79
|
-
return (
|
80
|
-
{
|
81
|
-
"form": self.form_class(
|
82
|
-
name=self.parameter_name,
|
83
|
-
label=_("By %(filter_title)s") % {"filter_title": self.title},
|
84
|
-
data={self.parameter_name: self.value()},
|
85
|
-
),
|
86
|
-
},
|
87
|
-
)
|
88
|
-
|
89
|
-
|
90
|
-
class FieldTextFilter(ValueMixin, admin.FieldListFilter):
|
91
|
-
template = "unfold/filters/filters_field.html"
|
92
|
-
form_class = SearchForm
|
93
|
-
|
94
|
-
def __init__(self, field, request, params, model, model_admin, field_path):
|
95
|
-
self.lookup_kwarg = f"{field_path}__icontains"
|
96
|
-
self.lookup_val = params.get(self.lookup_kwarg)
|
97
|
-
super().__init__(field, request, params, model, model_admin, field_path)
|
98
|
-
|
99
|
-
def expected_parameters(self) -> list[str]:
|
100
|
-
return [self.lookup_kwarg]
|
101
|
-
|
102
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
103
|
-
return (
|
104
|
-
{
|
105
|
-
"form": self.form_class(
|
106
|
-
label=_("By %(filter_title)s") % {"filter_title": self.title},
|
107
|
-
name=self.lookup_kwarg,
|
108
|
-
data={self.lookup_kwarg: self.value()},
|
109
|
-
),
|
110
|
-
},
|
111
|
-
)
|
112
|
-
|
113
|
-
|
114
|
-
class DropdownFilter(admin.SimpleListFilter):
|
115
|
-
template = "unfold/filters/filters_field.html"
|
116
|
-
form_class = DropdownForm
|
117
|
-
all_option = ["", _("All")]
|
118
|
-
|
119
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
120
|
-
return (
|
121
|
-
{
|
122
|
-
"form": self.form_class(
|
123
|
-
label=_("By %(filter_title)s") % {"filter_title": self.title},
|
124
|
-
name=self.parameter_name,
|
125
|
-
choices=[self.all_option, *self.lookup_choices],
|
126
|
-
data={self.parameter_name: self.value()},
|
127
|
-
multiple=self.multiple if hasattr(self, "multiple") else False,
|
128
|
-
),
|
129
|
-
},
|
130
|
-
)
|
131
|
-
|
132
|
-
|
133
|
-
class MultipleDropdownFilter(DropdownFilter):
|
134
|
-
multiple = True
|
135
|
-
|
136
|
-
def __init__(self, request, params, model, model_admin):
|
137
|
-
self.request = request
|
138
|
-
super().__init__(request, params, model, model_admin)
|
139
|
-
|
140
|
-
def value(self):
|
141
|
-
return self.request.GET.getlist(self.parameter_name)
|
142
|
-
|
143
|
-
|
144
|
-
class ChoicesDropdownFilter(ValueMixin, DropdownMixin, admin.ChoicesFieldListFilter):
|
145
|
-
def choices(self, changelist: ChangeList):
|
146
|
-
choices = [self.all_option, *self.field.flatchoices]
|
147
|
-
|
148
|
-
yield {
|
149
|
-
"form": self.form_class(
|
150
|
-
label=_("By %(filter_title)s") % {"filter_title": self.title},
|
151
|
-
name=self.lookup_kwarg,
|
152
|
-
choices=choices,
|
153
|
-
data={self.lookup_kwarg: self.value()},
|
154
|
-
multiple=self.multiple if hasattr(self, "multiple") else False,
|
155
|
-
),
|
156
|
-
}
|
157
|
-
|
158
|
-
|
159
|
-
class MultipleChoicesDropdownFilter(MultiValueMixin, ChoicesDropdownFilter):
|
160
|
-
multiple = True
|
161
|
-
|
162
|
-
|
163
|
-
class RelatedDropdownFilter(ValueMixin, DropdownMixin, admin.RelatedFieldListFilter):
|
164
|
-
def choices(self, changelist: ChangeList):
|
165
|
-
yield {
|
166
|
-
"form": self.form_class(
|
167
|
-
label=_("By %(filter_title)s") % {"filter_title": self.title},
|
168
|
-
name=self.lookup_kwarg,
|
169
|
-
choices=[self.all_option, *self.lookup_choices],
|
170
|
-
data={self.lookup_kwarg: self.value()},
|
171
|
-
multiple=self.multiple if hasattr(self, "multiple") else False,
|
172
|
-
),
|
173
|
-
}
|
174
|
-
|
175
|
-
|
176
|
-
class MultipleRelatedDropdownFilter(MultiValueMixin, RelatedDropdownFilter):
|
177
|
-
multiple = True
|
178
|
-
|
179
|
-
|
180
|
-
class SingleNumericFilter(admin.FieldListFilter):
|
181
|
-
request = None
|
182
|
-
parameter_name = None
|
183
|
-
template = "unfold/filters/filters_numeric_single.html"
|
184
|
-
|
185
|
-
def __init__(
|
186
|
-
self,
|
187
|
-
field: Field,
|
188
|
-
request: HttpRequest,
|
189
|
-
params: dict[str, str],
|
190
|
-
model: type[Model],
|
191
|
-
model_admin: ModelAdmin,
|
192
|
-
field_path: str,
|
193
|
-
) -> None:
|
194
|
-
super().__init__(field, request, params, model, model_admin, field_path)
|
195
|
-
|
196
|
-
if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
|
197
|
-
raise TypeError(
|
198
|
-
f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
|
199
|
-
)
|
200
|
-
|
201
|
-
self.request = request
|
202
|
-
|
203
|
-
if self.parameter_name is None:
|
204
|
-
self.parameter_name = self.field_path
|
205
|
-
|
206
|
-
if self.parameter_name in params:
|
207
|
-
value = params.pop(self.parameter_name)
|
208
|
-
value = value[0] if isinstance(value, list) else value
|
209
|
-
|
210
|
-
if value not in EMPTY_VALUES:
|
211
|
-
self.used_parameters[self.parameter_name] = value
|
212
|
-
|
213
|
-
def queryset(
|
214
|
-
self, request: HttpRequest, queryset: QuerySet[Any]
|
215
|
-
) -> Optional[QuerySet]:
|
216
|
-
if self.value():
|
217
|
-
try:
|
218
|
-
return queryset.filter(**{self.parameter_name: self.value()})
|
219
|
-
except (ValueError, ValidationError):
|
220
|
-
return None
|
221
|
-
|
222
|
-
def value(self) -> Any:
|
223
|
-
return self.used_parameters.get(self.parameter_name, None)
|
224
|
-
|
225
|
-
def expected_parameters(self) -> list[Optional[str]]:
|
226
|
-
return [self.parameter_name]
|
227
|
-
|
228
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
229
|
-
return (
|
230
|
-
{
|
231
|
-
"request": self.request,
|
232
|
-
"parameter_name": self.parameter_name,
|
233
|
-
"form": SingleNumericForm(
|
234
|
-
name=self.parameter_name, data={self.parameter_name: self.value()}
|
235
|
-
),
|
236
|
-
},
|
237
|
-
)
|
238
|
-
|
239
|
-
|
240
|
-
class RangeNumericMixin:
|
241
|
-
request = None
|
242
|
-
parameter_name = None
|
243
|
-
template = "unfold/filters/filters_numeric_range.html"
|
244
|
-
|
245
|
-
def init_used_parameters(self, params: dict[str, Any]) -> None:
|
246
|
-
if self.parameter_name + "_from" in params:
|
247
|
-
value = params.pop(self.parameter_name + "_from")
|
248
|
-
|
249
|
-
self.used_parameters[self.parameter_name + "_from"] = (
|
250
|
-
value[0] if isinstance(value, list) else value
|
251
|
-
)
|
252
|
-
|
253
|
-
if self.parameter_name + "_to" in params:
|
254
|
-
value = params.pop(self.parameter_name + "_to")
|
255
|
-
self.used_parameters[self.parameter_name + "_to"] = (
|
256
|
-
value[0] if isinstance(value, list) else value
|
257
|
-
)
|
258
|
-
|
259
|
-
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
|
260
|
-
filters = {}
|
261
|
-
|
262
|
-
value_from = self.used_parameters.get(self.parameter_name + "_from", None)
|
263
|
-
if value_from is not None and value_from != "":
|
264
|
-
filters.update(
|
265
|
-
{
|
266
|
-
self.parameter_name + "__gte": self.used_parameters.get(
|
267
|
-
self.parameter_name + "_from", None
|
268
|
-
),
|
269
|
-
}
|
270
|
-
)
|
271
|
-
|
272
|
-
value_to = self.used_parameters.get(self.parameter_name + "_to", None)
|
273
|
-
if value_to is not None and value_to != "":
|
274
|
-
filters.update(
|
275
|
-
{
|
276
|
-
self.parameter_name + "__lte": self.used_parameters.get(
|
277
|
-
self.parameter_name + "_to", None
|
278
|
-
),
|
279
|
-
}
|
280
|
-
)
|
281
|
-
|
282
|
-
try:
|
283
|
-
return queryset.filter(**filters)
|
284
|
-
except (ValueError, ValidationError):
|
285
|
-
return None
|
286
|
-
|
287
|
-
def expected_parameters(self) -> list[str]:
|
288
|
-
return [
|
289
|
-
f"{self.parameter_name}_from",
|
290
|
-
f"{self.parameter_name}_to",
|
291
|
-
]
|
292
|
-
|
293
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
294
|
-
return (
|
295
|
-
{
|
296
|
-
"request": self.request,
|
297
|
-
"parameter_name": self.parameter_name,
|
298
|
-
"form": RangeNumericForm(
|
299
|
-
name=self.parameter_name,
|
300
|
-
data={
|
301
|
-
self.parameter_name + "_from": self.used_parameters.get(
|
302
|
-
self.parameter_name + "_from", None
|
303
|
-
),
|
304
|
-
self.parameter_name + "_to": self.used_parameters.get(
|
305
|
-
self.parameter_name + "_to", None
|
306
|
-
),
|
307
|
-
},
|
308
|
-
),
|
309
|
-
},
|
310
|
-
)
|
311
|
-
|
312
|
-
|
313
|
-
class RangeNumericListFilter(RangeNumericMixin, admin.SimpleListFilter):
|
314
|
-
def __init__(
|
315
|
-
self,
|
316
|
-
request: HttpRequest,
|
317
|
-
params: dict[str, str],
|
318
|
-
model: type[Model],
|
319
|
-
model_admin: ModelAdmin,
|
320
|
-
) -> None:
|
321
|
-
super().__init__(request, params, model, model_admin)
|
322
|
-
if not self.parameter_name:
|
323
|
-
raise ValueError("Parameter name cannot be None")
|
324
|
-
|
325
|
-
self.request = request
|
326
|
-
self.init_used_parameters(params)
|
327
|
-
|
328
|
-
def lookups(
|
329
|
-
self, request: HttpRequest, model_admin: ModelAdmin
|
330
|
-
) -> tuple[tuple[str, str], ...]:
|
331
|
-
return (("dummy", "dummy"),)
|
332
|
-
|
333
|
-
|
334
|
-
class RangeNumericFilter(RangeNumericMixin, admin.FieldListFilter):
|
335
|
-
def __init__(
|
336
|
-
self,
|
337
|
-
field: Field,
|
338
|
-
request: HttpRequest,
|
339
|
-
params: dict[str, str],
|
340
|
-
model: type[Model],
|
341
|
-
model_admin: ModelAdmin,
|
342
|
-
field_path: str,
|
343
|
-
) -> None:
|
344
|
-
super().__init__(field, request, params, model, model_admin, field_path)
|
345
|
-
if not isinstance(field, (DecimalField, IntegerField, FloatField, AutoField)):
|
346
|
-
raise TypeError(
|
347
|
-
f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
|
348
|
-
)
|
349
|
-
|
350
|
-
self.request = request
|
351
|
-
if self.parameter_name is None:
|
352
|
-
self.parameter_name = self.field_path
|
353
|
-
|
354
|
-
self.init_used_parameters(params)
|
355
|
-
|
356
|
-
|
357
|
-
class SliderNumericFilter(RangeNumericFilter):
|
358
|
-
MAX_DECIMALS = 7
|
359
|
-
STEP = None
|
360
|
-
|
361
|
-
template = "unfold/filters/filters_numeric_slider.html"
|
362
|
-
field = None
|
363
|
-
form_class = SliderNumericForm
|
364
|
-
|
365
|
-
def __init__(
|
366
|
-
self,
|
367
|
-
field: Field,
|
368
|
-
request: HttpRequest,
|
369
|
-
params: dict[str, str],
|
370
|
-
model: type[Model],
|
371
|
-
model_admin: ModelAdmin,
|
372
|
-
field_path: str,
|
373
|
-
) -> None:
|
374
|
-
super().__init__(field, request, params, model, model_admin, field_path)
|
375
|
-
|
376
|
-
self.field = field
|
377
|
-
self.q = model_admin.get_queryset(request)
|
378
|
-
|
379
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
380
|
-
total = self.q.all().count()
|
381
|
-
min_value = self.q.all().aggregate(min=Min(self.parameter_name)).get("min", 0)
|
382
|
-
|
383
|
-
if total > 1:
|
384
|
-
max_value = (
|
385
|
-
self.q.all().aggregate(max=Max(self.parameter_name)).get("max", 0)
|
386
|
-
)
|
387
|
-
else:
|
388
|
-
max_value = None
|
389
|
-
|
390
|
-
if isinstance(self.field, (FloatField, DecimalField)):
|
391
|
-
decimals = self.MAX_DECIMALS
|
392
|
-
step = self.STEP if self.STEP else self._get_min_step(self.MAX_DECIMALS)
|
393
|
-
else:
|
394
|
-
decimals = 0
|
395
|
-
step = self.STEP if self.STEP else 1
|
396
|
-
|
397
|
-
return (
|
398
|
-
{
|
399
|
-
"decimals": decimals,
|
400
|
-
"step": step,
|
401
|
-
"parameter_name": self.parameter_name,
|
402
|
-
"request": self.request,
|
403
|
-
"min": min_value,
|
404
|
-
"max": max_value,
|
405
|
-
"value_from": self.used_parameters.get(
|
406
|
-
self.parameter_name + "_from", min_value
|
407
|
-
),
|
408
|
-
"value_to": self.used_parameters.get(
|
409
|
-
self.parameter_name + "_to", max_value
|
410
|
-
),
|
411
|
-
"form": self.form_class(
|
412
|
-
name=self.parameter_name,
|
413
|
-
data={
|
414
|
-
self.parameter_name + "_from": self.used_parameters.get(
|
415
|
-
self.parameter_name + "_from", min_value
|
416
|
-
),
|
417
|
-
self.parameter_name + "_to": self.used_parameters.get(
|
418
|
-
self.parameter_name + "_to", max_value
|
419
|
-
),
|
420
|
-
},
|
421
|
-
),
|
422
|
-
},
|
423
|
-
)
|
424
|
-
|
425
|
-
def _get_min_step(self, precision: int) -> float:
|
426
|
-
result_format = f"{{:.{precision - 1}f}}"
|
427
|
-
return float(result_format.format(0) + "1")
|
428
|
-
|
429
|
-
|
430
|
-
class RangeDateFilter(admin.FieldListFilter):
|
431
|
-
request = None
|
432
|
-
parameter_name = None
|
433
|
-
form_class = RangeDateForm
|
434
|
-
template = "unfold/filters/filters_date_range.html"
|
435
|
-
|
436
|
-
def __init__(
|
437
|
-
self,
|
438
|
-
field: Field,
|
439
|
-
request: HttpRequest,
|
440
|
-
params: dict[str, str],
|
441
|
-
model: type[Model],
|
442
|
-
model_admin: ModelAdmin,
|
443
|
-
field_path: str,
|
444
|
-
) -> None:
|
445
|
-
super().__init__(field, request, params, model, model_admin, field_path)
|
446
|
-
if not isinstance(field, DateField):
|
447
|
-
raise TypeError(
|
448
|
-
f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
|
449
|
-
)
|
450
|
-
|
451
|
-
self.request = request
|
452
|
-
if self.parameter_name is None:
|
453
|
-
self.parameter_name = self.field_path
|
454
|
-
|
455
|
-
if self.parameter_name + "_from" in params:
|
456
|
-
value = params.pop(self.field_path + "_from")
|
457
|
-
value = value[0] if isinstance(value, list) else value
|
458
|
-
|
459
|
-
if value not in EMPTY_VALUES:
|
460
|
-
self.used_parameters[self.field_path + "_from"] = value
|
461
|
-
|
462
|
-
if self.parameter_name + "_to" in params:
|
463
|
-
value = params.pop(self.field_path + "_to")
|
464
|
-
value = value[0] if isinstance(value, list) else value
|
465
|
-
|
466
|
-
if value not in EMPTY_VALUES:
|
467
|
-
self.used_parameters[self.field_path + "_to"] = value
|
468
|
-
|
469
|
-
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
|
470
|
-
filters = {}
|
471
|
-
|
472
|
-
value_from = self.used_parameters.get(self.parameter_name + "_from")
|
473
|
-
if value_from not in EMPTY_VALUES:
|
474
|
-
filters.update({self.parameter_name + "__gte": parse_date_str(value_from)})
|
475
|
-
|
476
|
-
value_to = self.used_parameters.get(self.parameter_name + "_to")
|
477
|
-
if value_to not in EMPTY_VALUES:
|
478
|
-
filters.update({self.parameter_name + "__lte": parse_date_str(value_to)})
|
479
|
-
|
480
|
-
try:
|
481
|
-
return queryset.filter(**filters)
|
482
|
-
except (ValueError, ValidationError):
|
483
|
-
return None
|
484
|
-
|
485
|
-
def expected_parameters(self) -> list[str]:
|
486
|
-
return [
|
487
|
-
f"{self.parameter_name}_from",
|
488
|
-
f"{self.parameter_name}_to",
|
489
|
-
]
|
490
|
-
|
491
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
492
|
-
return (
|
493
|
-
{
|
494
|
-
"request": self.request,
|
495
|
-
"parameter_name": self.parameter_name,
|
496
|
-
"form": self.form_class(
|
497
|
-
name=self.parameter_name,
|
498
|
-
data={
|
499
|
-
self.parameter_name + "_from": self.used_parameters.get(
|
500
|
-
self.parameter_name + "_from", None
|
501
|
-
),
|
502
|
-
self.parameter_name + "_to": self.used_parameters.get(
|
503
|
-
self.parameter_name + "_to", None
|
504
|
-
),
|
505
|
-
},
|
506
|
-
),
|
507
|
-
},
|
508
|
-
)
|
509
|
-
|
510
|
-
|
511
|
-
class RangeDateTimeFilter(admin.FieldListFilter):
|
512
|
-
request = None
|
513
|
-
parameter_name = None
|
514
|
-
template = "unfold/filters/filters_datetime_range.html"
|
515
|
-
form_class = RangeDateTimeForm
|
516
|
-
|
517
|
-
def __init__(
|
518
|
-
self,
|
519
|
-
field: Field,
|
520
|
-
request: HttpRequest,
|
521
|
-
params: dict[str, str],
|
522
|
-
model: type[Model],
|
523
|
-
model_admin: ModelAdmin,
|
524
|
-
field_path: str,
|
525
|
-
) -> None:
|
526
|
-
super().__init__(field, request, params, model, model_admin, field_path)
|
527
|
-
if not isinstance(field, DateTimeField):
|
528
|
-
raise TypeError(
|
529
|
-
f"Class {type(self.field)} is not supported for {self.__class__.__name__}."
|
530
|
-
)
|
531
|
-
|
532
|
-
self.request = request
|
533
|
-
if self.parameter_name is None:
|
534
|
-
self.parameter_name = self.field_path
|
535
|
-
|
536
|
-
if self.parameter_name + "_from_0" in params:
|
537
|
-
value = params.pop(self.field_path + "_from_0")
|
538
|
-
value = value[0] if isinstance(value, list) else value
|
539
|
-
self.used_parameters[self.field_path + "_from_0"] = value
|
540
|
-
|
541
|
-
if self.parameter_name + "_from_1" in params:
|
542
|
-
value = params.pop(self.field_path + "_from_1")
|
543
|
-
value = value[0] if isinstance(value, list) else value
|
544
|
-
self.used_parameters[self.field_path + "_from_1"] = value
|
545
|
-
|
546
|
-
if self.parameter_name + "_to_0" in params:
|
547
|
-
value = params.pop(self.field_path + "_to_0")
|
548
|
-
value = value[0] if isinstance(value, list) else value
|
549
|
-
self.used_parameters[self.field_path + "_to_0"] = value
|
550
|
-
|
551
|
-
if self.parameter_name + "_to_1" in params:
|
552
|
-
value = params.pop(self.field_path + "_to_1")
|
553
|
-
value = value[0] if isinstance(value, list) else value
|
554
|
-
self.used_parameters[self.field_path + "_to_1"] = value
|
555
|
-
|
556
|
-
def expected_parameters(self) -> list[str]:
|
557
|
-
return [
|
558
|
-
f"{self.parameter_name}_from_0",
|
559
|
-
f"{self.parameter_name}_from_1",
|
560
|
-
f"{self.parameter_name}_to_0",
|
561
|
-
f"{self.parameter_name}_to_1",
|
562
|
-
]
|
563
|
-
|
564
|
-
def queryset(self, request: HttpRequest, queryset: QuerySet) -> QuerySet:
|
565
|
-
filters = {}
|
566
|
-
|
567
|
-
date_value_from = self.used_parameters.get(self.parameter_name + "_from_0")
|
568
|
-
time_value_from = self.used_parameters.get(self.parameter_name + "_from_1")
|
569
|
-
|
570
|
-
date_value_to = self.used_parameters.get(self.parameter_name + "_to_0")
|
571
|
-
time_value_to = self.used_parameters.get(self.parameter_name + "_to_1")
|
572
|
-
|
573
|
-
if date_value_from not in EMPTY_VALUES and time_value_from not in EMPTY_VALUES:
|
574
|
-
filters.update(
|
575
|
-
{
|
576
|
-
f"{self.parameter_name}__gte": parse_datetime_str(
|
577
|
-
f"{date_value_from} {time_value_from}"
|
578
|
-
),
|
579
|
-
}
|
580
|
-
)
|
581
|
-
|
582
|
-
if date_value_to not in EMPTY_VALUES and time_value_to not in EMPTY_VALUES:
|
583
|
-
filters.update(
|
584
|
-
{
|
585
|
-
f"{self.parameter_name}__lte": parse_datetime_str(
|
586
|
-
f"{date_value_to} {time_value_to}"
|
587
|
-
),
|
588
|
-
}
|
589
|
-
)
|
590
|
-
|
591
|
-
try:
|
592
|
-
return queryset.filter(**filters)
|
593
|
-
except (ValueError, ValidationError):
|
594
|
-
return None
|
595
|
-
|
596
|
-
def choices(self, changelist: ChangeList) -> tuple[dict[str, Any], ...]:
|
597
|
-
return (
|
598
|
-
{
|
599
|
-
"request": self.request,
|
600
|
-
"parameter_name": self.parameter_name,
|
601
|
-
"form": self.form_class(
|
602
|
-
name=self.parameter_name,
|
603
|
-
data={
|
604
|
-
self.parameter_name + "_from_0": self.used_parameters.get(
|
605
|
-
self.parameter_name + "_from_0"
|
606
|
-
),
|
607
|
-
self.parameter_name + "_from_1": self.used_parameters.get(
|
608
|
-
self.parameter_name + "_from_1"
|
609
|
-
),
|
610
|
-
self.parameter_name + "_to_0": self.used_parameters.get(
|
611
|
-
self.parameter_name + "_to_0"
|
612
|
-
),
|
613
|
-
self.parameter_name + "_to_1": self.used_parameters.get(
|
614
|
-
self.parameter_name + "_to_1"
|
615
|
-
),
|
616
|
-
},
|
617
|
-
),
|
618
|
-
},
|
619
|
-
)
|
File without changes
|