diff --git a/CHANGELOG.txt b/CHANGELOG.txt index be9d076f97..a321093080 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -27,6 +27,7 @@ Changelog * Draft page view is now restricted to users with edit / publish permission over the page (Kees Hink) * Added the option to delete a previously saved focal point on a image (Maarten Kling) * Page explorer menu item, search and summary panel are now hidden for users with no page permissions (Tim Heap) + * Added support for custom date and datetime formats in input fields (Bojan Mihelac) * Fix: Marked 'Date from' / 'Date to' strings in wagtailforms for translation (Vorlif) * Fix: "File" field label on image edit form is now translated (Stein Strindhaug) * Fix: Unreliable preview is now reliable by always opening in a new window (Kjartan Sverrisson) diff --git a/docs/advanced_topics/settings.rst b/docs/advanced_topics/settings.rst index 55b65c8937..27a9858e72 100644 --- a/docs/advanced_topics/settings.rst +++ b/docs/advanced_topics/settings.rst @@ -383,6 +383,18 @@ When enabled Wagtail shows where a particular image, document or snippet is bein The usage count only applies to direct (database) references. Using documents, images and snippets within StreamFields or rich text fields will not be taken into account. +Date and DateTime inputs +------------------------ + +.. code-block:: python + + WAGTAIL_DATE_FORMAT = '%d.%m.%Y.' + WAGTAIL_DATETIME_FORMAT = '%d.%m.%Y. %H:%M' + + +Specifies the date and datetime format to be used in input fields in the Wagtail admin. The format is specified in `Python datetime module syntax `_, and must be one of the recognised formats listed in the ``DATE_INPUT_FORMATS`` or ``DATETIME_INPUT_FORMATS`` setting respectively (see `DATE_INPUT_FORMATS `_). + + URL Patterns ~~~~~~~~~~~~ diff --git a/docs/releases/1.10.rst b/docs/releases/1.10.rst index 564ef228e1..49610e5b9f 100644 --- a/docs/releases/1.10.rst +++ b/docs/releases/1.10.rst @@ -34,6 +34,7 @@ Other features * Draft page view is now restricted to users with edit / publish permission over the page (Kees Hink) * Added the option to delete a previously saved focal point on a image (Maarten Kling) * Page explorer menu item, search and summary panel are now hidden for users with no page permissions (Tim Heap) + * Added support for custom date and datetime formats in input fields (Bojan Mihelac) Bug fixes diff --git a/docs/topics/streamfield.rst b/docs/topics/streamfield.rst index 23d1783c50..59963e7f9a 100644 --- a/docs/topics/streamfield.rst +++ b/docs/topics/streamfield.rst @@ -157,7 +157,10 @@ DateBlock ``wagtail.wagtailcore.blocks.DateBlock`` -A date picker. The keyword arguments ``required`` and ``help_text`` are accepted. +A date picker. The keyword arguments ``required``, ``help_text`` and ``format`` are accepted. + +``format`` (default: None) + Date format. If not specifed Wagtail will use ``WAGTAIL_DATE_FORMAT`` setting with fallback to '%Y-%m-%d'. TimeBlock ~~~~~~~~~ @@ -171,7 +174,10 @@ DateTimeBlock ``wagtail.wagtailcore.blocks.DateTimeBlock`` -A combined date / time picker. The keyword arguments ``required`` and ``help_text`` are accepted. +A combined date / time picker. The keyword arguments ``required``, ``help_text`` and ``format`` are accepted. + +``format`` (default: None) + Date format. If not specifed Wagtail will use ``WAGTAIL_DATETIME_FORMAT`` setting with fallback to '%Y-%m-%d %H:%M'. RichTextBlock ~~~~~~~~~~~~~ diff --git a/wagtail/wagtailadmin/datetimepicker.py b/wagtail/wagtailadmin/datetimepicker.py new file mode 100644 index 0000000000..d0ed17f787 --- /dev/null +++ b/wagtail/wagtailadmin/datetimepicker.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import, unicode_literals + + +# Adapted from https://djangosnippets.org/snippets/10563/ +# original author bernd-wechner +def to_datetimepicker_format(python_format_string): + """ + Given a python datetime format string, attempts to convert it to + the nearest PHP datetime format string possible. + """ + python2PHP = { + "%a": "D", + "%A": "l", + "%b": "M", + "%B": "F", + "%c": "", + "%d": "d", + "%H": "H", + "%I": "h", + "%j": "z", + "%m": "m", + "%M": "i", + "%p": "A", + "%S": "s", + "%U": "", + "%w": "w", + "%W": "W", + "%x": "", + "%X": "", + "%y": "y", + "%Y": "Y", + "%Z": "e", + } + + php_format_string = python_format_string + for py, php in python2PHP.items(): + php_format_string = php_format_string.replace(py, php) + + return php_format_string diff --git a/wagtail/wagtailadmin/tests/test_widgets.py b/wagtail/wagtailadmin/tests/test_widgets.py index 4450647845..0df03bbecf 100644 --- a/wagtail/wagtailadmin/tests/test_widgets.py +++ b/wagtail/wagtailadmin/tests/test_widgets.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, unicode_literals from django.test import TestCase +from django.test.utils import override_settings from wagtail.tests.testapp.models import EventPage, SimplePage from wagtail.wagtailadmin import widgets @@ -80,3 +81,71 @@ class TestAdminPageChooserWidget(TestCase): self.assertEqual( js_init, "createPageChooser(\"test-id\", [\"wagtailcore.page\"], %d, true);" % self.root_page.id ) + + +class TestAdminDateInput(TestCase): + + def test_render_js_init(self): + widget = widgets.AdminDateInput() + + js_init = widget.render_js_init('test-id', 'test', None) + + # we should see the JS initialiser code: + # initDateChooser("test-id", {"dayOfWeekStart": 0, "format": "Y-m-d"}); + # except that we can't predict the order of the config options + self.assertIn('initDateChooser("test-id", {', js_init) + self.assertIn('"dayOfWeekStart": 0', js_init) + self.assertIn('"format": "Y-m-d"', js_init) + + def test_render_js_init_with_format(self): + widget = widgets.AdminDateInput(format='%d.%m.%Y.') + + js_init = widget.render_js_init('test-id', 'test', None) + self.assertIn( + '"format": "d.m.Y."', + js_init, + ) + + @override_settings(WAGTAIL_DATE_FORMAT='%d.%m.%Y.') + def test_render_js_init_with_format_from_settings(self): + widget = widgets.AdminDateInput() + + js_init = widget.render_js_init('test-id', 'test', None) + self.assertIn( + '"format": "d.m.Y."', + js_init, + ) + + +class TestAdminDateTimeInput(TestCase): + + def test_render_js_init(self): + widget = widgets.AdminDateTimeInput() + + js_init = widget.render_js_init('test-id', 'test', None) + + # we should see the JS initialiser code: + # initDateTimeChooser("test-id", {"dayOfWeekStart": 0, "format": "Y-m-d H:i"}); + # except that we can't predict the order of the config options + self.assertIn('initDateTimeChooser("test-id", {', js_init) + self.assertIn('"dayOfWeekStart": 0', js_init) + self.assertIn('"format": "Y-m-d H:i"', js_init) + + def test_render_js_init_with_format(self): + widget = widgets.AdminDateTimeInput(format='%d.%m.%Y. %H:%M') + + js_init = widget.render_js_init('test-id', 'test', None) + self.assertIn( + '"format": "d.m.Y. H:i"', + js_init, + ) + + @override_settings(WAGTAIL_DATETIME_FORMAT='%d.%m.%Y. %H:%M') + def test_render_js_init_with_format_from_settings(self): + widget = widgets.AdminDateTimeInput() + + js_init = widget.render_js_init('test-id', 'test', None) + self.assertIn( + '"format": "d.m.Y. H:i"', + js_init, + ) diff --git a/wagtail/wagtailadmin/widgets.py b/wagtail/wagtailadmin/widgets.py index c57b6aaada..8718dafe18 100644 --- a/wagtail/wagtailadmin/widgets.py +++ b/wagtail/wagtailadmin/widgets.py @@ -17,10 +17,15 @@ from django.utils.translation import ugettext_lazy as _ from taggit.forms import TagWidget from wagtail.utils.widgets import WidgetWithScript +from wagtail.wagtailadmin.datetimepicker import to_datetimepicker_format from wagtail.wagtailcore import hooks from wagtail.wagtailcore.models import Page +DEFAULT_DATE_FORMAT = '%Y-%m-%d' +DEFAULT_DATETIME_FORMAT = '%Y-%m-%d %H:%M' + + class AdminAutoHeightTextInput(WidgetWithScript, widgets.Textarea): def __init__(self, attrs=None): # Use more appropriate rows default, given autoheight will alter this anyway @@ -35,16 +40,21 @@ class AdminAutoHeightTextInput(WidgetWithScript, widgets.Textarea): class AdminDateInput(WidgetWithScript, widgets.DateInput): - # Set a default date format to match the one that our JS date picker expects - - # it can still be overridden explicitly, but this way it won't be affected by - # the DATE_INPUT_FORMATS setting - def __init__(self, attrs=None, format='%Y-%m-%d'): - super(AdminDateInput, self).__init__(attrs=attrs, format=format) + def __init__(self, attrs=None, format=None): + fmt = format + if fmt is None: + fmt = getattr(settings, 'WAGTAIL_DATE_FORMAT', DEFAULT_DATE_FORMAT) + self.js_format = to_datetimepicker_format(fmt) + super(AdminDateInput, self).__init__(attrs=attrs, format=fmt) def render_js_init(self, id_, name, value): + config = { + 'dayOfWeekStart': get_format('FIRST_DAY_OF_WEEK'), + 'format': self.js_format, + } return 'initDateChooser({0}, {1});'.format( json.dumps(id_), - json.dumps({'dayOfWeekStart': get_format('FIRST_DAY_OF_WEEK')}) + json.dumps(config) ) @@ -57,13 +67,21 @@ class AdminTimeInput(WidgetWithScript, widgets.TimeInput): class AdminDateTimeInput(WidgetWithScript, widgets.DateTimeInput): - def __init__(self, attrs=None, format='%Y-%m-%d %H:%M'): - super(AdminDateTimeInput, self).__init__(attrs=attrs, format=format) + def __init__(self, attrs=None, format=None): + fmt = format + if fmt is None: + fmt = getattr(settings, 'WAGTAIL_DATETIME_FORMAT', DEFAULT_DATETIME_FORMAT) + self.js_format = to_datetimepicker_format(fmt) + super(AdminDateTimeInput, self).__init__(attrs=attrs, format=fmt) def render_js_init(self, id_, name, value): + config = { + 'dayOfWeekStart': get_format('FIRST_DAY_OF_WEEK'), + 'format': self.js_format, + } return 'initDateTimeChooser({0}, {1});'.format( json.dumps(id_), - json.dumps({'dayOfWeekStart': get_format('FIRST_DAY_OF_WEEK')}) + json.dumps(config) ) diff --git a/wagtail/wagtailcore/blocks/field_block.py b/wagtail/wagtailcore/blocks/field_block.py index 435cd7a130..554301dbb0 100644 --- a/wagtail/wagtailcore/blocks/field_block.py +++ b/wagtail/wagtailcore/blocks/field_block.py @@ -231,14 +231,21 @@ class BooleanBlock(FieldBlock): class DateBlock(FieldBlock): - def __init__(self, required=True, help_text=None, **kwargs): + def __init__(self, required=True, help_text=None, format=None, **kwargs): self.field_options = {'required': required, 'help_text': help_text} + try: + self.field_options['input_formats'] = kwargs.pop('input_formats') + except KeyError: + pass + self.format = format super(DateBlock, self).__init__(**kwargs) @cached_property def field(self): from wagtail.wagtailadmin.widgets import AdminDateInput - field_kwargs = {'widget': AdminDateInput} + field_kwargs = { + 'widget': AdminDateInput(format=self.format), + } field_kwargs.update(self.field_options) return forms.DateField(**field_kwargs) @@ -280,14 +287,17 @@ class TimeBlock(FieldBlock): class DateTimeBlock(FieldBlock): - def __init__(self, required=True, help_text=None, **kwargs): + def __init__(self, required=True, help_text=None, format=None, **kwargs): self.field_options = {'required': required, 'help_text': help_text} + self.format = format super(DateTimeBlock, self).__init__(**kwargs) @cached_property def field(self): from wagtail.wagtailadmin.widgets import AdminDateTimeInput - field_kwargs = {'widget': AdminDateTimeInput} + field_kwargs = { + 'widget': AdminDateTimeInput(format=self.format), + } field_kwargs.update(self.field_options) return forms.DateTimeField(**field_kwargs) diff --git a/wagtail/wagtailcore/tests/test_blocks.py b/wagtail/wagtailcore/tests/test_blocks.py index 074abb77e1..b5205b3b58 100644 --- a/wagtail/wagtailcore/tests/test_blocks.py +++ b/wagtail/wagtailcore/tests/test_blocks.py @@ -6,6 +6,7 @@ import collections import json import unittest import warnings +from datetime import date, datetime from decimal import Decimal # non-standard import name for ugettext_lazy, to prevent strings from being picked up for translation @@ -2438,6 +2439,55 @@ class TestStaticBlock(unittest.TestCase): self.assertEqual(result, None) +class TestDateBlock(TestCase): + + def test_render_form(self): + block = blocks.DateBlock() + value = date(2015, 8, 13) + result = block.render_form(value, prefix='dateblock') + + # we should see the JS initialiser code: + # + # except that we can't predict the order of the config options + self.assertIn('