Adds support for custom date and datetime formats (#2595)

It is possible to set default format for date/datetime inputs. This works together
with standard django localization.

    # django settings
    USE_I18N = True
    LANGUAGE_CODE = 'sl'

    # wagtail settings
    WAGTAIL_DATE_FORMAT = '%d.%m.%Y.'
    WAGTAIL_DATETIME_FORMAT = '%d.%m.%Y. %H:%M'

DateBlock, DateTimeBlock accepts additional keyword argument `format`.
pull/3366/merge
Bojan Mihelac 2016-09-06 16:11:18 +02:00 zatwierdzone przez Matt Westcott
rodzic 48949e69a7
commit 0f53afc5a6
9 zmienionych plików z 221 dodań i 15 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 <https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior>`_, and must be one of the recognised formats listed in the ``DATE_INPUT_FORMATS`` or ``DATETIME_INPUT_FORMATS`` setting respectively (see `DATE_INPUT_FORMATS <https://docs.djangoproject.com/en/1.10/ref/settings/#std:setting-DATE_INPUT_FORMATS>`_).
URL Patterns
~~~~~~~~~~~~

Wyświetl plik

@ -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

Wyświetl plik

@ -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
~~~~~~~~~~~~~

Wyświetl plik

@ -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

Wyświetl plik

@ -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,
)

Wyświetl plik

@ -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)
)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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:
# <script>initDateChooser("dateblock", {"dayOfWeekStart": 0, "format": "Y-m-d"});</script>
# except that we can't predict the order of the config options
self.assertIn('<script>initDateChooser("dateblock", {', result)
self.assertIn('"dayOfWeekStart": 0', result)
self.assertIn('"format": "Y-m-d"', result)
self.assertInHTML(
'<input id="dateblock" name="dateblock" placeholder="" type="text" value="2015-08-13" />',
result
)
def test_render_form_with_format(self):
block = blocks.DateBlock(format='%d.%m.%Y')
value = date(2015, 8, 13)
result = block.render_form(value, prefix='dateblock')
self.assertIn('<script>initDateChooser("dateblock", {', result)
self.assertIn('"dayOfWeekStart": 0', result)
self.assertIn('"format": "d.m.Y"', result)
self.assertInHTML(
'<input id="dateblock" name="dateblock" placeholder="" type="text" value="13.08.2015" />',
result
)
class TestDateTimeBlock(TestCase):
def test_render_form_with_format(self):
block = blocks.DateTimeBlock(format='%d.%m.%Y %H:%M')
value = datetime(2015, 8, 13, 10, 0)
result = block.render_form(value, prefix='datetimeblock')
self.assertIn(
'"format": "d.m.Y H:i"',
result
)
self.assertInHTML(
'<input id="datetimeblock" name="datetimeblock" placeholder="" type="text" value="13.08.2015 10:00" />',
result
)
class TestSystemCheck(TestCase):
def test_name_must_be_nonempty(self):
block = blocks.StreamBlock([