Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.rst
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
django-admin-rangefilter
========================

A Django app that adds a filter by date range and numeric range to the admin UI.
A Django app that adds a filter by date range, numeric range, and time range to the admin UI.

.. image:: https://raw.githubusercontent.com/silentsokolov/django-admin-rangefilter/master/docs/images/screenshot.png

Expand Down Expand Up @@ -58,6 +58,7 @@ In admin
DateTimeRangeFilterBuilder,
NumericRangeFilterBuilder,
DateRangeQuickSelectListFilterBuilder,
TimeRangeFilter, # Added TimeRangeFilter import
)

from .models import Post
Expand All @@ -77,9 +78,9 @@ In admin
),
("num_value", NumericRangeFilterBuilder()),
("created_at", DateRangeQuickSelectListFilterBuilder()), # Range + QuickSelect Filter
("event_time", TimeRangeFilter), # Added TimeRangeFilter usage
)


Support Content-Security-Policy
-------------------------------

Expand Down
83 changes: 83 additions & 0 deletions rangefilter/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from django.contrib import admin
from django.contrib.admin.widgets import AdminDateWidget
from django.contrib.admin.widgets import AdminSplitDateTime as BaseAdminSplitDateTime
from django.contrib.admin.widgets import AdminTimeWidget
from django.template.defaultfilters import slugify
from django.templatetags.static import StaticNode
from django.utils import timezone
Expand Down Expand Up @@ -470,3 +471,85 @@ def DateRangeQuickSelectListFilterBuilder(title=None, default_start=None, defaul
)

return filter_cls

class TimeRangeFilter(BaseRangeFilter):
def choices(self, changelist):
yield {
"system_name": force_str(
slugify(self.title) if slugify(self.title) else id(self.title)
),
"query_string": changelist.get_query_string({}, remove=self.expected_parameters()),
}

def queryset(self, request, queryset):
if self.form.is_valid():
validated_data = dict(self.form.cleaned_data.items())
if validated_data:
return queryset.filter(**self._make_query_filter(request, validated_data))
return queryset

def expected_parameters(self):
return [self.lookup_kwarg_gte, self.lookup_kwarg_lte]

def get_facet_counts(self, pk_attname, filtered_qs):
return {}

def get_template(self):
return "rangefilter/time_filter.html"

template = property(get_template)

def get_form(self, _request):
form_class = self._get_form_class()

if django.VERSION[:2] >= (5, 0):
for name, value in self.used_parameters.items():
if isinstance(value, list):
self.used_parameters[name] = value[-1]

return form_class(self.used_parameters or None)

def _get_form_fields(self):
return OrderedDict(
(
(
self.lookup_kwarg_gte,
forms.TimeField(
label="",
widget=AdminTimeWidget(attrs={"placeholder": _("From time")}),
localize=True,
required=False,
initial=self.default_gte,
),
),
(
self.lookup_kwarg_lte,
forms.TimeField(
label="",
widget=AdminTimeWidget(attrs={"placeholder": _("To time")}),
localize=True,
required=False,
initial=self.default_lte,
),
),
)
)

def _get_form_class(self):
fields = self._get_form_fields()

form_class = type(str("TimeRangeForm"), (forms.BaseForm,), {"base_fields": fields})

return form_class

def _make_query_filter(self, _request, validated_data):
query_params = {}
time_value_gte = validated_data.get(self.lookup_kwarg_gte, None)
time_value_lte = validated_data.get(self.lookup_kwarg_lte, None)

if time_value_gte is not None:
query_params["{0}__time__gte".format(self.field_path)] = time_value_gte
if time_value_lte is not None:
query_params["{0}__time__lte".format(self.field_path)] = time_value_lte

return query_params
98 changes: 98 additions & 0 deletions tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
DateTimeRangeFilter,
NumericRangeFilter,
OnceCallMedia,
TimeRangeFilter, # Added import for TimeRangeFilter
)
from rangefilter.templatetags.rangefilter_compat import static

Expand Down Expand Up @@ -55,6 +56,11 @@ class RangeModelFloatAdmin(ModelAdmin):
ordering = ("-id",)


class RangeModelTimeAdmin(ModelAdmin): # New admin class for TimeRangeFilter
list_filter = (("created_at", TimeRangeFilter),)
ordering = ("-id",)


def select_by(dictlist):
return list(dictlist)[0]

Expand Down Expand Up @@ -567,3 +573,95 @@ def test_call(self):
self.assertNotEqual(self.media(), [])
self.assertTrue(self.media._is_rendered) # pylint: disable=protected-access
self.assertEqual(self.media(), [])

# New test cases for TimeRangeFilter
class TimeRangeFilterTestCase(TestCase):
def setUp(self):
self.now = timezone.now()
self.midnight = datetime.time(0, 0, 0)
self.noon = datetime.time(12, 0, 0)
self.evening = datetime.time(18, 0, 0)

self.morning_event = RangeModelDT.objects.create(created_at=self.now.replace(hour=9, minute=0))
self.afternoon_event = RangeModelDT.objects.create(created_at=self.now.replace(hour=15, minute=0))
self.evening_event = RangeModelDT.objects.create(created_at=self.now.replace(hour=20, minute=0))

self.username = "testuser"
self.email = "testuser@example.com"
self.password = "secret"
self.user = User.objects.create_user(self.username, self.email, self.password)

def get_changelist(self, request, model, modeladmin):
if getattr(modeladmin, "get_changelist_instance", None):
return modeladmin.get_changelist_instance(request)

return ChangeList(
request,
model,
modeladmin.list_display,
modeladmin.list_display_links,
modeladmin.list_filter,
modeladmin.date_hierarchy,
modeladmin.search_fields,
modeladmin.list_select_related,
modeladmin.list_per_page,
modeladmin.list_max_show_all,
modeladmin.list_editable,
modeladmin,
)

def test_timefilter(self):
request_factory = RequestFactory()
modeladmin = RangeModelTimeAdmin(RangeModelDT, site)

request = request_factory.get("/")
request.user = self.user

changelist = self.get_changelist(request, RangeModelDT, modeladmin)

queryset = changelist.get_queryset(request)

self.assertEqual(list(queryset), [self.morning_event, self.afternoon_event, self.evening_event])
filterspec = changelist.get_filters(request)[0][0]
self.assertEqual(force_str(filterspec.title), "created at")

def test_timefilter_filtered_morning(self):
request_factory = RequestFactory()
modeladmin = RangeModelTimeAdmin(RangeModelDT, site)

request = request_factory.get(
"/",
{
"created_at__range__gte": self.midnight,
"created_at__range__lte": self.noon,
},
)
request.user = self.user

changelist = self.get_changelist(request, RangeModelDT, modeladmin)

queryset = changelist.get_queryset(request)

self.assertEqual(list(queryset), [self.morning_event])
filterspec = changelist.get_filters(request)[0][0]
self.assertEqual(force_str(filterspec.title), "created at")

def test_timefilter_filtered_evening(self):
request_factory = RequestFactory()
modeladmin = RangeModelTimeAdmin(RangeModelDT, site)

request = request_factory.get(
"/",
{
"created_at__range__gte": self.evening,
},
)
request.user = self.user

changelist = self.get_changelist(request, RangeModelDT, modeladmin)

queryset = changelist.get_queryset(request)

self.assertEqual(list(queryset), [self.evening_event])
filterspec = changelist.get_filters(request)[0][0]
self.assertEqual(force_str(filterspec.title), "created at")