diff --git a/README.md b/README.md
index d06dafb..1fb2b43 100644
--- a/README.md
+++ b/README.md
@@ -296,6 +296,21 @@ TextFilter(
)
```
+#### DateFilter
+
+A datepicker using Wagtail's default picker.
+
+```python
+from admin_list_controls.filters import DateFilter
+
+DateFilter(
+ name='date',
+ label='Date',
+ format='%d/%m/%Y',
+ apply_to_queryset=lambda queryset, value: queryset.filter(date__gte=value),
+)
+```
+
#### BooleanFilter
A checkbox input.
diff --git a/admin_list_controls/filters.py b/admin_list_controls/filters.py
index 512b4f5..cc49550 100644
--- a/admin_list_controls/filters.py
+++ b/admin_list_controls/filters.py
@@ -1,3 +1,8 @@
+import datetime
+from django.conf import settings
+from django.core import validators
+from django.utils import datetime_safe, formats
+from wagtail.admin.datetimepicker import to_datetimepicker_format
from .components import BaseComponent
from .actions import RemoveValue, SubmitForm
@@ -93,6 +98,54 @@ def clean(self, *args, **kwargs):
return bool(value)
+class DateFilter(BaseFilter):
+ format_key = 'DATE_INPUT_FORMATS'
+ filter_type = 'date'
+ empty_values = list(validators.EMPTY_VALUES)
+
+ def __init__(self, format=None, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self.format = format or getattr(settings, 'WAGTAIL_DATE_FORMAT', None)
+ fallback_format = formats.get_format(self.format_key)[0]
+ self.format = fallback_format if self.format is None else self.format
+ self.js_format = to_datetimepicker_format(self.format)
+
+ def format_value(self, value):
+ return formats.localize_input(value, self.format)
+
+ def to_python(self, value):
+ """
+ Validate that the input can be converted to a date. Return a Python
+ datetime.date object.
+ """
+ if value in self.empty_values:
+ return None
+
+ value = value.strip()
+ # Try to strptime against each input format.
+ try:
+ return datetime.datetime.strptime(value, self.format).date()
+ except (ValueError, TypeError):
+ return None
+
+ def clean(self, *args, **kwargs):
+ value = super().clean(*args, **kwargs)
+ return self.to_python(value) if value else None
+
+ def get_summary_display_value_for_value(self, value):
+ """
+ Returns the corresponding display value for the raw value
+ """
+ return self.format_value(value)
+
+ def serialize(self):
+ return dict(super().serialize(), **{
+ 'value': self.format_value(self.cleaned_value),
+ 'format': self.js_format,
+ })
+
+
class BaseChoiceFilter(BaseFilter):
def __init__(self, choices, *args, **kwargs):
super().__init__(*args, **kwargs)
diff --git a/admin_list_controls/src/admin_list_controls/components/filters/date.js b/admin_list_controls/src/admin_list_controls/components/filters/date.js
new file mode 100644
index 0000000..d64810e
--- /dev/null
+++ b/admin_list_controls/src/admin_list_controls/components/filters/date.js
@@ -0,0 +1,72 @@
+import React, {useState, useEffect} from "react";
+import {SET_VALUE} from '../../constants';
+import {store} from '../../state';
+import c from "classnames";
+import {submit_form} from "../../index";
+
+export function DateFilter({control}) {
+ const [value, set_value] = useState(control.value);
+
+ const input_id = `alc__filter-${control.component_id}-${control.name}`;
+ const inputChange = event => {
+ const value = event.target.value;
+ set_value(value);
+ store.dispatch({
+ type: SET_VALUE,
+ name: control.name,
+ value: value,
+ });
+ }
+
+ useEffect(() => {
+ let start = null;
+ window.initDateChooser(input_id, {
+ "dayOfWeekStart": 1, "format": control.format,
+ onGenerate: function(current, input) {
+ start = start || current;
+ let picker = this[0];
+ if (start && !dateEqual(start, current)) {
+ picker.querySelectorAll(
+ '.xdsoft_datepicker .xdsoft_current:not(.xdsoft_today)'
+ ).forEach(
+ i => i.classList.remove("xdsoft_current")
+ );
+ }
+ },
+ onChangeDateTime(current, $el) {
+ start = current;
+ let el = $el.get(0);
+ el.dispatchEvent(new Event('change'));
+ inputChange({'target': el});
+ }
+ });
+ }, [input_id] // avoid init on every render
+ );
+
+ return (
+
+ {/* A form element allows for keyboards to trigger submit events (enter keypress, etc) */}
+
+
+ );
+}
diff --git a/admin_list_controls/src/admin_list_controls/components/root.js b/admin_list_controls/src/admin_list_controls/components/root.js
index 2ae4077..108390e 100644
--- a/admin_list_controls/src/admin_list_controls/components/root.js
+++ b/admin_list_controls/src/admin_list_controls/components/root.js
@@ -9,6 +9,7 @@ import {Divider} from "./divider";
import {Block} from "./block";
import {Selector} from "./selector";
import {TextFilter} from "./filters/text";
+import {DateFilter} from "./filters/date";
import {BooleanFilter} from "./filters/boolean";
import {RadioFilter} from "./filters/radio";
import {ChoiceFilter} from "./filters/choice";
@@ -56,6 +57,8 @@ export function render_control(control) {
return ;
case 'choice':
return ;
+ case 'date':
+ return ;
default:
console.error('Unknown filter type', control.filter_type, control);
throw new Error(`Unknown filter type "${control.filter_type}`);
@@ -65,4 +68,4 @@ export function render_control(control) {
console.error('Unknown control type', control);
throw new Error(`Unknown control ${control.object_type}`);
}
-}
\ No newline at end of file
+}
diff --git a/admin_list_controls/tests/test_filters.py b/admin_list_controls/tests/test_filters.py
index 891fd47..b64e86f 100644
--- a/admin_list_controls/tests/test_filters.py
+++ b/admin_list_controls/tests/test_filters.py
@@ -1,7 +1,8 @@
+from datetime import date
from django.test import RequestFactory
from django_webtest import WebTest
from admin_list_controls.filters import BaseFilter, TextFilter, BooleanFilter, ChoiceFilter, \
- RadioFilter
+ RadioFilter, DateFilter
from admin_list_controls.tests.utils import BaseTestCase
@@ -54,6 +55,18 @@ def test_text_filter_value(self):
filter_.handle_request(self.factory.get('/?test_name=test_value'))
self.assertEqual(filter_.cleaned_value, 'test_value')
+ def test_date_filter_value(self):
+ filter_ = DateFilter(
+ name='test_name',
+ label='test_label',
+ )
+ filter_.handle_request(self.factory.get('/'))
+ self.assertEqual(filter_.cleaned_value, None)
+ filter_.handle_request(self.factory.get('/?test_name='))
+ self.assertEqual(filter_.cleaned_value, None)
+ filter_.handle_request(self.factory.get('/?test_name=2021-01-01'))
+ self.assertEqual(filter_.cleaned_value, date(2021, 1, 1))
+
def test_boolean_filter_value(self):
filter_ = BooleanFilter(
name='test_name',
diff --git a/admin_list_controls/views.py b/admin_list_controls/views.py
index d8eb11d..26978d4 100644
--- a/admin_list_controls/views.py
+++ b/admin_list_controls/views.py
@@ -1,9 +1,12 @@
-import os
import json
+import os
from collections import Iterable
+from django import forms
from django.conf import settings
+from django.core.serializers.json import DjangoJSONEncoder
from django.utils.safestring import mark_safe
+from wagtail.admin.staticfiles import versioned_static
from wagtail.contrib.modeladmin.views import IndexView
from .components import ListControls
from .selectors import LayoutSelector
@@ -84,7 +87,7 @@ def get_context_data(self, **kwargs):
# Consumed by the front-end code to build the UI
'initial_state': json.dumps({
'admin_list_controls': self.get_list_controls().serialize(),
- }),
+ }, cls=DjangoJSONEncoder),
'selected_layout_template': selected_layout_template,
'widget_js': self.get_list_controls_widget_js(),
}
@@ -158,6 +161,17 @@ def prepare_list_controls(self):
if hasattr(obj, 'derive_from_components'):
obj.derive_from_components(flattened_tree)
+ @property
+ def media(self):
+ js = self.model_admin.get_index_view_extra_js()
+ # @TODO only inject datechooser when needed
+ js += [versioned_static(
+ 'wagtailadmin/js/date-time-chooser.js')]
+
+ return forms.Media(
+ css={'all': self.model_admin.get_index_view_extra_css()},
+ js=js
+ )
class ListControlsIndexView(ListControlsIndexViewMixin, IndexView):
pass
diff --git a/test_project/shop/wagtail_hooks.py b/test_project/shop/wagtail_hooks.py
index 1263b32..5f4a166 100644
--- a/test_project/shop/wagtail_hooks.py
+++ b/test_project/shop/wagtail_hooks.py
@@ -1,10 +1,10 @@
-from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from admin_list_controls.selectors import LayoutSelector
from admin_list_controls.views import ListControlsIndexView
from admin_list_controls.components import Button, Icon, Text, Panel, Divider, Block, Spacer, \
Columns, Summary
from admin_list_controls.actions import TogglePanel, CollapsePanel, SubmitForm, Link
-from admin_list_controls.filters import TextFilter, ChoiceFilter, RadioFilter, BooleanFilter
+from admin_list_controls.filters import TextFilter, ChoiceFilter, RadioFilter, BooleanFilter, DateFilter
+from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from .models import Product
@@ -94,6 +94,13 @@ def build_list_controls(self):
Panel(ref='panel_2', collapsed=True)(
Text('medium text ', size=Text.MEDIUM),
Text('large text', size=Text.LARGE),
+ DateFilter(
+ name='date_start',
+ label='Date',
+ format='%d/%m/%Y',
+ ),
+ Spacer(),
+ Button(action=SubmitForm())('Apply filters'),
),
Summary(),
]