diff --git a/README.md b/README.md
index 8f23336c..ea3b4f58 100644
--- a/README.md
+++ b/README.md
@@ -284,6 +284,7 @@ take precedence over the contents of an `.env` file.**
- [`NAP_START_MIN`](#nap_start_min)
- [`SECRET_KEY`](#secret_key)
- [`TIME_ZONE`](#time_zone)
+- [`USE_24_HOUR_TIME_FORMAT`](#use_24_hour_time_format)
### `ALLOWED_HOSTS`
@@ -369,6 +370,21 @@ The default time zone to use for the instance. See [List of tz database time zon
for all possible values. This value can be overridden per use from the user
settings form.
+### `USE_24_HOUR_TIME_FORMAT`
+
+*Default: False*
+
+Whether to force 24-hour time format for locales that do not ordinarily use it
+(e.g. `en`). Support for this feature must implemented on a per-locale basis.
+See format files under [`babybuddy/formats`](babybuddy/formats) for supported
+locales.
+
+Note: This value for this setting is interpreted as a boolean from a string
+using Python's built-in [`strtobool`](https://docs.python.org/3/distutils/apiref.html#distutils.util.strtobool)
+tool. Only certain strings are supported (e.g. "True" for `True` and "False" for
+`False`), other unrecognized strings will cause a `ValueError` and prevent Baby
+Buddy from loading.
+
## Languages
Baby Buddy includes translation support as of v1.2.2. Language can be set on a
diff --git a/babybuddy/formats/en/formats.py b/babybuddy/formats/en/formats.py
index 67c4c5c2..45146826 100644
--- a/babybuddy/formats/en/formats.py
+++ b/babybuddy/formats/en/formats.py
@@ -1,24 +1,22 @@
-# This files adds two new date input formats to support Baby Buddy's frontend
-# library formats:
-# - %m/%d/%Y %I:%M:%S %p
-# - %m/%d/%Y %I:%M %p
-#
-# The remaining formats come from the base Django formats.
-#
-# See django.cong.locale.en.
-DATETIME_INPUT_FORMATS = [
- '%m/%d/%Y %I:%M:%S %p', # '10/25/2006 2:30:59 PM' (new)
- '%m/%d/%Y %I:%M %p', # '10/25/2006 2:30 PM' (new)
- '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59'
- '%Y-%m-%d %H:%M:%S.%f', # '2006-10-25 14:30:59.000200'
- '%Y-%m-%d %H:%M', # '2006-10-25 14:30'
- '%Y-%m-%d', # '2006-10-25'
- '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59'
- '%m/%d/%Y %H:%M:%S.%f', # '10/25/2006 14:30:59.000200'
- '%m/%d/%Y %H:%M', # '10/25/2006 14:30'
- '%m/%d/%Y', # '10/25/2006'
- '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59'
- '%m/%d/%y %H:%M:%S.%f', # '10/25/06 14:30:59.000200'
- '%m/%d/%y %H:%M', # '10/25/06 14:30'
- '%m/%d/%y', # '10/25/06'
-]
+from django.conf import settings
+from django.conf.locale.en import formats
+
+# Override the regular locale settings to support 24 hour time.
+if settings.USE_24_HOUR_TIME_FORMAT:
+ DATETIME_FORMAT = 'N j, Y, H:i:s'
+ CUSTOM_INPUT_FORMATS = [
+ '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59'
+ '%m/%d/%Y %H:%M', # '10/25/2006 14:30'
+ ]
+ SHORT_DATETIME_FORMAT = 'm/d/Y G:i:s'
+ TIME_FORMAT = 'H:i:s'
+else:
+ # These formats are added to support the locale style of Baby Buddy's
+ # frontend library, which uses momentjs.
+ CUSTOM_INPUT_FORMATS = [
+ '%m/%d/%Y %I:%M:%S %p', # '10/25/2006 2:30:59 PM'
+ '%m/%d/%Y %I:%M %p', # '10/25/2006 2:30 PM'
+ ]
+
+# Append all other input formats from the base locale.
+DATETIME_INPUT_FORMATS = CUSTOM_INPUT_FORMATS + formats.DATETIME_INPUT_FORMATS
diff --git a/babybuddy/settings/base.py b/babybuddy/settings/base.py
index f86275a8..8349033a 100644
--- a/babybuddy/settings/base.py
+++ b/babybuddy/settings/base.py
@@ -1,4 +1,5 @@
import os
+from distutils.util import strtobool
from django.utils.translation import gettext_lazy as _
from dotenv import load_dotenv, find_dotenv
@@ -166,6 +167,14 @@ LANGUAGES = [
USE_L10N = True
+# Custom setting that can be used to override the locale-based time set by
+# USE_L10N _for specific locales_ to use 24-hour format. In order for this to
+# work with a given locale it must be set at the FORMAT_MODULE_PATH with
+# conditionals on this setting. See babybuddy/forms/en/formats.py for an example
+# implementation for the English locale.
+
+USE_24_HOUR_TIME_FORMAT = strtobool(os.environ.get('USE_24_HOUR_TIME_FORMAT') or 'False')
+
FORMAT_MODULE_PATH = ['babybuddy.formats']
diff --git a/babybuddy/tests/tests_formats.py b/babybuddy/tests/tests_formats.py
index 6cc7a153..23fd58e8 100644
--- a/babybuddy/tests/tests_formats.py
+++ b/babybuddy/tests/tests_formats.py
@@ -3,7 +3,8 @@ import datetime
from django.core.exceptions import ValidationError
from django.forms.fields import DateTimeField
-from django.test import TestCase
+from django.test import TestCase, override_settings, tag
+from django.utils.formats import date_format, time_format
class FormatsTestCase(TestCase):
@@ -17,8 +18,43 @@ class FormatsTestCase(TestCase):
]
for example in supported_custom_examples:
- result = field.to_python(example)
- self.assertIsInstance(result, datetime.datetime)
+ try:
+ result = field.to_python(example)
+ self.assertIsInstance(result, datetime.datetime)
+ except ValidationError:
+ self.fail('Format of "{}" not recognized!'.format(example))
with self.assertRaises(ValidationError):
field.to_python('invalid date string!')
+
+ @tag('isolate')
+ @override_settings(LANGUAGE_CODE='en', USE_24_HOUR_TIME_FORMAT=True)
+ def test_use_24_hour_time_format_en(self):
+ field = DateTimeField()
+ supported_custom_examples = [
+ '10/25/2006 2:30:59',
+ '10/25/2006 2:30',
+ '10/25/2006 14:30:59',
+ '10/25/2006 14:30',
+ ]
+
+ for example in supported_custom_examples:
+ try:
+ result = field.to_python(example)
+ self.assertIsInstance(result, datetime.datetime)
+ except ValidationError:
+ self.fail('Format of "{}" not recognized!'.format(example))
+
+ with self.assertRaises(ValidationError):
+ field.to_python('invalid date string!')
+
+ dt = datetime.datetime.fromisoformat('2011-11-04 23:05:59')
+ self.assertEqual(
+ date_format(dt, 'DATETIME_FORMAT'), 'Nov. 4, 2011, 23:05:59')
+
+ dt = datetime.datetime.fromisoformat('2011-11-04 02:05:59')
+ self.assertEqual(
+ date_format(dt, 'SHORT_DATETIME_FORMAT'), '11/04/2011 2:05:59')
+
+ t = datetime.time.fromisoformat('16:02:25')
+ self.assertEqual(time_format(t), '16:02:25')
diff --git a/core/templates/core/diaperchange_form.html b/core/templates/core/diaperchange_form.html
index 885af8db..e0de13c1 100644
--- a/core/templates/core/diaperchange_form.html
+++ b/core/templates/core/diaperchange_form.html
@@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %}
-{% load i18n %}
+{% load datetimepicker i18n %}
{% block title %}
{% if request.resolver_match.url_name == 'diaperchange-update' %}
@@ -31,6 +31,8 @@
{% block javascript %}
{% endblock %}
\ No newline at end of file
diff --git a/core/templates/core/feeding_form.html b/core/templates/core/feeding_form.html
index d4e51e82..01f5d4e2 100644
--- a/core/templates/core/feeding_form.html
+++ b/core/templates/core/feeding_form.html
@@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %}
-{% load i18n %}
+{% load datetimepicker i18n %}
{% block title %}
{% if request.resolver_match.url_name == 'feeding-update' %}
@@ -32,12 +32,15 @@
{% block javascript %}
{% endblock %}
\ No newline at end of file
diff --git a/core/templates/core/temperature_form.html b/core/templates/core/temperature_form.html
index 74d1ef89..e20be1f3 100644
--- a/core/templates/core/temperature_form.html
+++ b/core/templates/core/temperature_form.html
@@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %}
-{% load i18n %}
+{% load datetimepicker i18n %}
{% block title %}
{% if object %}
@@ -31,6 +31,8 @@
{% block javascript %}
{% endblock %}
\ No newline at end of file
diff --git a/core/templates/core/timer_form.html b/core/templates/core/timer_form.html
index 2fdb3569..15a33810 100644
--- a/core/templates/core/timer_form.html
+++ b/core/templates/core/timer_form.html
@@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %}
-{% load duration i18n %}
+{% load datetimepicker duration i18n %}
{% block title %}{% trans "Timer" %}{% endblock %}
@@ -27,7 +27,7 @@
{% block javascript %}
{% endblock %}
\ No newline at end of file
diff --git a/core/templates/core/tummytime_form.html b/core/templates/core/tummytime_form.html
index 08690fde..e88bf77d 100644
--- a/core/templates/core/tummytime_form.html
+++ b/core/templates/core/tummytime_form.html
@@ -1,5 +1,5 @@
{% extends 'babybuddy/page.html' %}
-{% load i18n %}
+{% load datetimepicker i18n %}
{% block title %}
{% if request.resolver_match.url_name == 'tummytime-update' %}
@@ -33,10 +33,10 @@
{% endblock %}
\ No newline at end of file
diff --git a/core/templatetags/datetimepicker.py b/core/templatetags/datetimepicker.py
new file mode 100644
index 00000000..3df17198
--- /dev/null
+++ b/core/templatetags/datetimepicker.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+from django import template
+from django.conf import settings
+
+register = template.Library()
+
+
+@register.simple_tag()
+def datetimepicker_format(format_string='L LT'):
+ """
+ Return a datetime format string for momentjs, with support for 24 hour time
+ override setting.
+ :param format_string: the default format string (locale based)
+ :return: the format string to use, as 24 hour time if configured.
+ """
+ if settings.USE_24_HOUR_TIME_FORMAT:
+ if format_string == 'L LT':
+ return 'L HH:mm'
+ elif format_string == 'L LTS':
+ return 'L HH:mm:ss'
+ return format_string
diff --git a/core/tests/tests_templatetags.py b/core/tests/tests_templatetags.py
index e9e751aa..a54435fa 100644
--- a/core/tests/tests_templatetags.py
+++ b/core/tests/tests_templatetags.py
@@ -4,7 +4,7 @@ from django.test import TestCase
from django.utils import timezone
from core.models import Child, Timer
-from core.templatetags import bootstrap, duration, timers
+from core.templatetags import bootstrap, datetimepicker, duration, timers
class TemplateTagsTestCase(TestCase):
@@ -60,3 +60,16 @@ class TemplateTagsTestCase(TestCase):
url = timers.instance_add_url({'timer': timer}, 'core:sleep-add')
self.assertEqual(url, '/sleep/add/?timer={}&child={}'.format(
timer.id, child.slug))
+
+ def test_datetimepicker_format(self):
+ self.assertEqual(datetimepicker.datetimepicker_format(), 'L LT')
+ self.assertEqual(datetimepicker.datetimepicker_format('L LT'), 'L LT')
+ self.assertEqual(
+ datetimepicker.datetimepicker_format('L LTS'), 'L LTS')
+
+ with self.settings(USE_24_HOUR_TIME_FORMAT=True):
+ self.assertEqual(datetimepicker.datetimepicker_format(), 'L HH:mm')
+ self.assertEqual(
+ datetimepicker.datetimepicker_format('L LT'), 'L HH:mm')
+ self.assertEqual(
+ datetimepicker.datetimepicker_format('L LTS'), 'L HH:mm:ss')
diff --git a/gulpfile.config.js b/gulpfile.config.js
index 2e2b67cd..7487fe80 100644
--- a/gulpfile.config.js
+++ b/gulpfile.config.js
@@ -58,6 +58,11 @@ module.exports = {
'babybuddy.scss'
]
},
+ testsConfig: {
+ isolated: [
+ 'babybuddy.tests.tests_formats.FormatsTestCase.test_use_24_hour_time_format_en'
+ ],
+ },
watchConfig: {
scriptsGlob: [
'*/static_src/js/**/*.js',
diff --git a/gulpfile.js b/gulpfile.js
index 0e812043..f98e43c1 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -2,6 +2,7 @@ var gulp = require('gulp');
var concat = require('gulp-concat');
var del = require('del');
+var es = require('child_process').execSync;
var flatten = require('gulp-flatten');
var pump = require('pump');
var sass = require('gulp-sass');
@@ -40,12 +41,23 @@ function coverage(cb) {
'coverage',
'run',
'manage.py',
- 'test'
+ 'test',
+ '--exclude-tag',
+ 'isolate'
],
{
stdio: 'inherit'
}
- ).on('exit', cb);
+ ).on('exit', function() {
+ // Add coverage for isolated tests.
+ config.testsConfig.isolated.forEach(function(test_name) {
+ es(
+ 'pipenv run coverage run -a manage.py test ' + test_name,
+ {stdio: 'inherit'}
+ );
+ })
+ cb();
+ });
}
/**
@@ -160,14 +172,30 @@ function styles(cb) {
}
/**
- * Runs all tests.
+ * Runs all tests _not_ tagged "isolate".
*
* @param cb
*/
function test(cb) {
- var command = ['run', 'python', 'manage.py', 'test'];
+ var command = [
+ 'run',
+ 'python',
+ 'manage.py',
+ 'test',
+ '--exclude-tag',
+ 'isolate'
+ ];
command = command.concat(process.argv.splice(3));
- spawn('pipenv', command, { stdio: 'inherit' }).on('exit', cb);
+ spawn('pipenv', command, { stdio: 'inherit' }).on('exit', function() {
+ // Run isolated tests.
+ config.testsConfig.isolated.forEach(function(test_name) {
+ es(
+ 'pipenv run python manage.py test ' + test_name,
+ {stdio: 'inherit'}
+ );
+ })
+ cb();
+ });
}
/**