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(); + }); } /**