Skip to content

Commit

Permalink
feat: daterange filters
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasvinclav authored Oct 22, 2022
1 parent de83a70 commit e096bd3
Show file tree
Hide file tree
Showing 21 changed files with 854 additions and 55 deletions.
24 changes: 14 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,30 +302,33 @@ Currently, Unfold implements numeric filters inside `unfold.contrib.filters` app
# admin.py

from django.contrib import admin
from django.contrib.auth.models import User

from unfold.admin import ModelAdmin
from unfold.contrib.admin import (
NumericFilterModelAdmin,
RangeNumericFilter,
SingleNumericFilter,
SliderNumericFilter,
RangeDateFilter,
RangeDateTimeFilter,
)

from .models import YourModel


class CustomSliderNumericFilter(SliderNumericFilter):
MAX_DECIMALS = 2
STEP = 10


@admin.register(YourModel)
class YourModelAdmin(NumericFilterModelAdmin):
@admin.register(User)
class YourModelAdmin(ModelAdmin):
list_filter = (
("field_A", SingleNumericFilter), # Single field search, __gte lookup
("field_B", RangeNumericFilter), # Range search, __gte and __lte lookup
("field_C", SliderNumericFilter), # Same as range above but with slider
("field_D", CustomSliderNumericFilter), # Filter with custom attributes
("field_A", SingleNumericFilter), # Numeric single field search, __gte lookup
("field_B", RangeNumericFilter), # Numeric range search, __gte and __lte lookup
("field_C", SliderNumericFilter), # Numeric range filter but with slider
("field_D", CustomSliderNumericFilter), # Numeric filter with custom attributes
("field_E", RangeDateFilter), # Date filter
("field_F", RangeDateTimeFilter), # Datetime filter
)
list_filter_submit = True # Display submit button
```

## User Admin Form
Expand All @@ -336,6 +339,7 @@ User's admin in Django is specific as it contains several forms which are requir
# models.py

from django.contrib.admin import register
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin

from unfold.admin import ModelAdmin
Expand Down
20 changes: 18 additions & 2 deletions src/unfold/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
UnfoldAdminNullBooleanSelectWidget,
UnfoldAdminSingleDateWidget,
UnfoldAdminSingleTimeWidget,
UnfoldAdminSplitDateTime,
UnfoldAdminSplitDateTimeWidget,
UnfoldAdminTextareaWidget,
UnfoldAdminTextInputWidget,
UnfoldAdminUUIDInputWidget,
Expand All @@ -58,7 +58,7 @@
FORMFIELD_OVERRIDES = {
models.DateTimeField: {
"form_class": forms.SplitDateTimeField,
"widget": UnfoldAdminSplitDateTime,
"widget": UnfoldAdminSplitDateTimeWidget,
},
models.DateField: {"widget": UnfoldAdminSingleDateWidget},
models.TimeField: {"widget": UnfoldAdminSingleTimeWidget},
Expand Down Expand Up @@ -265,6 +265,22 @@ class ModelAdmin(ModelAdminMixin, BaseModelAdmin):
def __init__(self, model, admin_site):
super().__init__(model, admin_site)

@property
def media(self):
media = super().media
additional_media = forms.Media()

for filter in self.list_filter:
if not isinstance(filter, (tuple, list)):
continue

if hasattr(filter[1], "form_class") and hasattr(
filter[1].form_class, "Media"
):
additional_media += forms.Media(filter[1].form_class.Media)

return media + additional_media

def get_fieldsets(self, request, obj=None):
if not obj and self.add_fieldsets:
return self.add_fieldsets
Expand Down
221 changes: 207 additions & 14 deletions src/unfold/contrib/filters/admin.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
from django.contrib import admin
from django.core.validators import EMPTY_VALUES
from django.db.models import Max, Min
from django.db.models.fields import AutoField, DecimalField, FloatField, IntegerField

from .forms import RangeNumericForm, SingleNumericForm, SliderNumericForm


class NumericFilterModelAdmin(admin.ModelAdmin):
class Media:
css = {"all": ("css/nouislider.min.css",)}
js = (
"js/wNumb.min.js",
"js/nouislider.min.js",
"js/admin-numeric-filter.js",
)
from django.db.models.fields import (
AutoField,
DateField,
DateTimeField,
DecimalField,
FloatField,
IntegerField,
)
from django.utils.dateparse import parse_datetime

from .forms import (
RangeDateForm,
RangeDateTimeForm,
RangeNumericForm,
SingleNumericForm,
SliderNumericForm,
)


class SingleNumericFilter(admin.FieldListFilter):
Expand Down Expand Up @@ -148,6 +153,7 @@ class SliderNumericFilter(RangeNumericFilter):

template = "unfold/filters/filters_numeric_slider.html"
field = None
form_class = SliderNumericForm

def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
Expand Down Expand Up @@ -187,7 +193,7 @@ def choices(self, changelist):
"value_to": self.used_parameters.get(
self.parameter_name + "_to", max_value
),
"form": SliderNumericForm(
"form": self.form_class(
name=self.parameter_name,
data={
self.parameter_name
Expand All @@ -206,3 +212,190 @@ def choices(self, changelist):
def _get_min_step(self, precision):
result_format = f"{{:.{precision - 1}f}}"
return float(result_format.format(0) + "1")


class RangeDateFilter(admin.FieldListFilter):
request = None
parameter_name = None
form_class = RangeDateForm
template = "unfold/filters/filters_date_range.html"

def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
if not isinstance(field, DateField):
raise TypeError(
"Class {} is not supported for {}.".format(
type(self.field), self.__class__.__name__
)
)

self.request = request
if self.parameter_name is None:
self.parameter_name = self.field_path

if self.parameter_name + "_from" in params:
value = params.pop(self.field_path + "_from")
self.used_parameters[self.field_path + "_from"] = value

if self.parameter_name + "_to" in params:
value = params.pop(self.field_path + "_to")
self.used_parameters[self.field_path + "_to"] = value

def queryset(self, request, queryset):
filters = {}

value_from = self.used_parameters.get(self.parameter_name + "_from", None)
if value_from not in EMPTY_VALUES:
filters.update(
{
self.parameter_name
+ "__gte": self.used_parameters.get(
self.parameter_name + "_from", None
),
}
)

value_to = self.used_parameters.get(self.parameter_name + "_to", None)
if value_to not in EMPTY_VALUES:
filters.update(
{
self.parameter_name
+ "__lte": self.used_parameters.get(
self.parameter_name + "_to", None
),
}
)

return queryset.filter(**filters)

def expected_parameters(self):
return [
f"{self.parameter_name}_from",
f"{self.parameter_name}_to",
]

def choices(self, changelist):
return (
{
"request": self.request,
"parameter_name": self.parameter_name,
"form": self.form_class(
name=self.parameter_name,
data={
self.parameter_name
+ "_from": self.used_parameters.get(
self.parameter_name + "_from", None
),
self.parameter_name
+ "_to": self.used_parameters.get(
self.parameter_name + "_to", None
),
},
),
},
)


class RangeDateTimeFilter(admin.FieldListFilter):
request = None
parameter_name = None
template = "unfold/filters/filters_datetime_range.html"
form_class = RangeDateTimeForm

def __init__(self, field, request, params, model, model_admin, field_path):
super().__init__(field, request, params, model, model_admin, field_path)
if not isinstance(field, DateTimeField):
raise TypeError(
"Class {} is not supported for {}.".format(
type(self.field), self.__class__.__name__
)
)

self.request = request
if self.parameter_name is None:
self.parameter_name = self.field_path

if self.parameter_name + "_from_0" in params:
value = params.pop(self.field_path + "_from_0")
self.used_parameters[self.field_path + "_from_0"] = value

if self.parameter_name + "_from_1" in params:
value = params.pop(self.field_path + "_from_1")
self.used_parameters[self.field_path + "_from_1"] = value

if self.parameter_name + "_to_0" in params:
value = params.pop(self.field_path + "_to_0")
self.used_parameters[self.field_path + "_to_0"] = value

if self.parameter_name + "_to_1" in params:
value = params.pop(self.field_path + "_to_1")
self.used_parameters[self.field_path + "_to_1"] = value

def expected_parameters(self):
return [
f"{self.parameter_name}_from_0",
f"{self.parameter_name}_from_1",
f"{self.parameter_name}_to_0",
f"{self.parameter_name}_to_1",
]

def queryset(self, request, queryset):
filters = {}

date_value_from = self.used_parameters.get(
self.parameter_name + "_from_0", None
)
time_value_from = self.used_parameters.get(
self.parameter_name + "_from_1", None
)
date_value_to = self.used_parameters.get(self.parameter_name + "_to_0", None)
time_value_to = self.used_parameters.get(self.parameter_name + "_to_1", None)

if date_value_from not in EMPTY_VALUES and time_value_from not in EMPTY_VALUES:
filters.update(
{
f"{self.parameter_name}__gte": parse_datetime(
f"{date_value_from}T{time_value_from}"
),
}
)

if date_value_to not in EMPTY_VALUES and time_value_to not in EMPTY_VALUES:
filters.update(
{
f"{self.parameter_name}__lte": parse_datetime(
f"{date_value_to}T{time_value_to}"
),
}
)

return queryset.filter(**filters)

def choices(self, changelist):
return (
{
"request": self.request,
"parameter_name": self.parameter_name,
"form": self.form_class(
name=self.parameter_name,
data={
self.parameter_name
+ "_from_0": self.used_parameters.get(
self.parameter_name + "_from_0", None
),
self.parameter_name
+ "_from_1": self.used_parameters.get(
self.parameter_name + "_from_1", None
),
self.parameter_name
+ "_to_0": self.used_parameters.get(
self.parameter_name + "_to_0", None
),
self.parameter_name
+ "_to_1": self.used_parameters.get(
self.parameter_name + "_to_1", None
),
},
),
},
)
Loading

0 comments on commit e096bd3

Please sign in to comment.